# zidane/prompt

> Helpers for splitting a system prompt into a cacheable static half and a per-turn dynamic half.

Import: `import { … } from 'zidane/prompt'`. Source: [src/system-prompt.ts](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts).

Use `zidane/prompt` to build or edit system prompts so the static part stays byte-identical across turns and the provider prompt cache keeps hitting. A boundary marker separates the halves, and providers strip it before the request goes out. The helpers join, split, append to and replace either half without touching the other.

```ts
import { appendDynamicSection, joinSystemPrompt, renderSystemForWire, splitSystemPrompt } from 'zidane/prompt'

const system = joinSystemPrompt('You are a careful code reviewer.', 'Today is Monday.')
const withBranch = appendDynamicSection(system, 'Current branch: main')

const { static: doctrine, dynamic } = splitSystemPrompt(withBranch)
console.log(doctrine)
console.log(dynamic)
console.log(renderSystemForWire(withBranch))
```

## Functions

### `appendDynamicSection`

Function.

Append `extra` to the DYNAMIC half of a system prompt. Inserts the boundary
marker if the input didn't already carry one.

Used by hosts (typically the TUI) to inject per-turn state — current cwd,
IDE selection, project root — without forcing every caller to know the
marker format. The host's `system:transform` hook rewrites the dynamic
half each turn; the static doctrine above stays byte-stable and rides the
cache.

Returns a new string; the input is not mutated. When `extra` is empty,
returns the input verbatim.

```ts
function appendDynamicSection(system: string, extra: string): string
```

**Parameters**

- `system` (`string`): System prompt, with or without a boundary marker.
- `extra` (`string`): Text to append to the dynamic half.

**Returns** `string`

Source: [src/system-prompt.ts:172](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L172)

### `appendStaticSection`

Function.

Append `extra` to the STATIC half of a system prompt, preserving any
existing dynamic suffix.

Used by the agent to fold in run-stable content (skills catalog, lazy tool
catalog) without bumping it into the dynamic half — both catalogs are
built once per run and remain byte-stable for the duration, so they
belong in the cached prefix.

Returns a new string; the input is not mutated. When `extra` is empty,
returns the input verbatim.

```ts
function appendStaticSection(system: string, extra: string): string
```

**Parameters**

- `system` (`string`): System prompt, with or without a boundary marker.
- `extra` (`string`): Text to append to the static half.

**Returns** `string`

Source: [src/system-prompt.ts:148](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L148)

### `hasSystemPromptBoundary`

Function.

True when `system` contains the boundary marker.

```ts
function hasSystemPromptBoundary(system: string): boolean
```

**Parameters**

- `system` (`string`): System prompt to check.

**Returns** `boolean`

Source: [src/system-prompt.ts:225](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L225)

### `joinSystemPrompt`

Function.

Compose a system prompt from a static prefix and an optional dynamic
suffix. Inserts [`SYSTEM_PROMPT_BOUNDARY`](https://zidane.si/docs/reference/prompt.md#SYSTEM_PROMPT_BOUNDARY) between them only when the
dynamic side is non-empty — single-block prompts stay marker-free, so
callers that never opt in pay zero overhead and providers fall back to the
existing whole-string caching path.

Spacing is `\n\n` on both sides of the marker so doctrine fragments,
which conventionally separate sections with a blank line, read cleanly
around the boundary.

```ts
function joinSystemPrompt(staticPart: string, dynamicPart: string): string
```

**Parameters**

- `staticPart` (`string`): Cacheable prefix.
- `dynamicPart` (`string`): Per-turn suffix. Empty to omit the marker.

**Returns** `string`

Source: [src/system-prompt.ts:125](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L125)

### `renderSystemForWire`

Function.

Strip the boundary marker so it never reaches the wire — collapses
`<static>${BOUNDARY}<dynamic>` into `<static>\n\n<dynamic>` for providers
that can't honor `cache_control` (vanilla OpenAI Chat Completions, Codex,
Cerebras, ...) or for the cache-disabled path on any provider.

Cache-aware providers re-derive the split from the original (un-rendered)
system string via `splitSystemPrompt` — `renderSystemForWire` is the
marker-free counterpart used to build the actual wire bytes.

No-op when the input has no marker. Pure / no I/O.

```ts
function renderSystemForWire(system: string): string
```

**Parameters**

- `system` (`string`): System prompt, with or without a boundary marker.

**Returns** `string`

Source: [src/system-prompt.ts:209](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L209)

### `replaceDynamicSection`

Function.

Replace the entire dynamic half of a system prompt with `next`. Used by
the TUI's `<env>` rewriter where the entire dynamic section is regenerated
each turn rather than appended to.

When `next` is empty, drops the dynamic half (and the marker) entirely.

```ts
function replaceDynamicSection(system: string, next: string): string
```

**Parameters**

- `system` (`string`): System prompt, with or without a boundary marker.
- `next` (`string`): Replacement for the dynamic half. Empty drops it.

**Returns** `string`

Source: [src/system-prompt.ts:190](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L190)

### `splitSystemPrompt`

Function.

Split a system prompt around the first [`SYSTEM_PROMPT_BOUNDARY`](https://zidane.si/docs/reference/prompt.md#SYSTEM_PROMPT_BOUNDARY).

Splits on the FIRST occurrence — subsequent markers are folded into the
dynamic half. This way callers can append additional `<env>` style blocks
with extra markers without each one creating a new cache layer (Anthropic
caps breakpoints; we use the budget elsewhere). The marker itself is
stripped from both sides — providers attach `cache_control` directly to
the static half's text content.

A single blank line (`\n\n`) immediately adjacent to the marker on each
side is trimmed — callers conventionally write
`<doctrine>\n\n<MARKER>\n\n<env>` and expect the rendered wire bytes to
read as one logical paragraph. Blank lines beyond that immediate pair
(e.g. `\n\n\n<env>`) are preserved verbatim so callers composing their
own spacing don't lose intentional gaps.

Pure / no I/O / no allocation when the marker is absent (returns the input
verbatim on the static side and the empty string on the dynamic side).

```ts
function splitSystemPrompt(system: string): SystemPromptParts
```

**Parameters**

- `system` (`string`): System prompt, with or without a boundary marker.

**Returns** `SystemPromptParts`

Source: [src/system-prompt.ts:92](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L92)

## Constants

### `SYSTEM_PROMPT_BOUNDARY`

Constant.

Literal marker inserted in a system prompt to separate cacheable doctrine
from per-turn dynamic content. Providers split on this token.

Underscored on both sides for visual distinctiveness — a stray instance in
model-written prose is implausible. Don't change the value without shipping
a migration; existing sessions carry the old marker in their cached
prompts.

```ts
const SYSTEM_PROMPT_BOUNDARY = '__ZIDANE_SYSTEM_PROMPT_BOUNDARY__'
```

Source: [src/system-prompt.ts:60](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L60)

## Interfaces

### `SystemPromptParts`

Interface.

Result of [`splitSystemPrompt`](https://zidane.si/docs/reference/prompt.md#splitSystemPrompt) — both halves stripped of the marker.

```ts
interface SystemPromptParts
```

**Properties**

- `dynamic` (`string`): Bytes AFTER the marker. Empty when no marker present. NOT cached.
- `static` (`string`): Bytes BEFORE the marker (or the entire string when no marker present). Cacheable.

Source: [src/system-prompt.ts:63](https://github.com/Tahul/zidane/blob/main/src/system-prompt.ts#L63)
