# zidane/types

> Type-only access to the public agent, session, context, decision and hook types, plus the typed error classes.

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

Use `zidane/types` when you only need to name a type, for example in a shared package that never creates an agent. Import with `import type` and the entry adds nothing to your bundle. The page lists agent options and hooks, session turns and usage, execution contexts, decisions and MCP connections.

```ts
import type { Agent, AgentOptions, SessionTurn } from 'zidane/types'
import { createAgent } from 'zidane'
import { basic } from 'zidane/presets'
import { anthropic } from 'zidane/providers/anthropic'

const options: AgentOptions = { ...basic, provider: anthropic() }
const agent: Agent = createAgent(options)

function lastTurn(turns: SessionTurn[]): SessionTurn | undefined {
  return turns[turns.length - 1]
}

console.log(lastTurn([]), agent.hooks !== undefined)
```

## Functions

### `flattenTurns`

Function.

Depth-first walk over the stats tree, returning every `TurnUsage` entry
— parent loop first, then each child subtree in completion order.

`AgentStats.totalIn` and the other `total*` fields already sum the whole
tree. Anything that needs per-turn data across the tree, such as a
per-model rollup, walks through this.

```ts
function flattenTurns(stats: AgentStats): TurnUsage[]
```

**Parameters**

- `stats` (`AgentStats`): Root of the stats tree, including any child agent stats.

**Returns** `TurnUsage[]`

Source: [src/stats.ts:152](https://github.com/Tahul/zidane/blob/main/src/stats.ts#L152)

### `statsByModel`

Function.

Group cumulative usage by `TurnUsage.modelId`. Each entry sums the input,
output, cache, cost, and turn-count across every turn the tree attributed
to that model — naturally handling cross-model runs (vision-fallback,
model-shifted subagents, mixed-provider workflows).

Turns missing `modelId` (mock providers, providers that don't echo a model
id) are bucketed under the literal string `'(unknown)'`.

```ts
function statsByModel(stats: AgentStats): Map<string, ModelUsage>
```

**Parameters**

- `stats` (`AgentStats`): Root of the stats tree, including any child agent stats.

**Returns** `Map<string, ModelUsage>`

Source: [src/stats.ts:178](https://github.com/Tahul/zidane/blob/main/src/stats.ts#L178)

### `toolOutputBudgetByteLength`

Function.

Approximate **text-budget size** of a tool output, in bytes.

Structured attachment blocks count as the short markers produced by
`toolResultToText`, not as raw base64 payload bytes. The per-turn
`toolOutputBudget` is a readability/context-loading steering guard; a
vision/PDF-capable provider can receive attachment bytes without needing the
model to summarize them as if they were text output.

```ts
function toolOutputBudgetByteLength(content: string | ToolResultContent[]): number
```

**Parameters**

- `content` (`string | ToolResultContent[]`): Plain string or structured tool-result blocks.

**Returns** `number`

Source: [src/types.ts:2881](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2881)

### `toolOutputByteLength`

Function.

Approximate **wire payload size** of a tool output, in bytes.

- Plain text: UTF-8 byte length.
- Structured content: text blocks contribute their UTF-8 byte length; image
  blocks contribute their **base64 character length** — a proxy for the
  serialized request-body footprint, NOT for tokens. Vision encoders
  tokenize decoded pixels (geometry-dependent; e.g. Anthropic ≈ `w·h/750`,
  OpenAI ≈ 85 + 170/tile), which has no meaningful relationship to base64
  length.

Used by the agent loop to populate `outputBytes` on `tool:after`,
`tool:transform`, `mcp:tool:after`, and `mcp:tool:transform` hooks so
consumers can size-budget tool output without re-counting bytes themselves.
Suitable for wire-size telemetry; the per-turn `toolOutputBudget` uses
`toolOutputBudgetByteLength` so attachments do not trigger text-output
summarization nudges by themselves. NOT a substitute for provider-side
context-window accounting — defer to server-side context management (e.g.
Anthropic's `context-management-*` beta) when token accuracy matters.

```ts
function toolOutputByteLength(content: string | ToolResultContent[]): number
```

**Parameters**

- `content` (`string | ToolResultContent[]`): Plain string or structured tool-result blocks.

**Returns** `number`

Source: [src/types.ts:2907](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2907)

### `toolResultToText`

Function.

Lossy flattener — converts `ToolResultContent[]` (or a plain string) to a single
string. Non-text blocks are replaced with markers such as
`[image: <media>, <n> b64 bytes]` (a `ref` block shows `ref <ref>` in place of the size).

Use at UI boundaries where a string is required; providers that understand
structured content should route the array through without flattening.

```ts
function toolResultToText(content: string | ToolResultContent[]): string
```

**Parameters**

- `content` (`string | ToolResultContent[]`): Plain string or structured tool-result blocks.

**Returns** `string`

Source: [src/types.ts:2794](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2794)

## Constants

### `DEFAULT_AGENT_CLOCK`

Constant.

Native clock backing the default (Date.now + crypto.randomUUID).

```ts
const DEFAULT_AGENT_CLOCK: AgentClock
```

Source: [src/types.ts:56](https://github.com/Tahul/zidane/blob/main/src/types.ts#L56)

## Interfaces

### `AgentBehavior`

Interface.

Run behavior settings: limits, guards, caching, compaction and recovery policies. Set agent-wide on `AgentOptions.behavior` and override per run.

```ts
interface AgentBehavior
```

**Properties**

- `autoCompact?` (`AutoCompact`): Loop-native auto-compaction. When enabled, the agent loop watches each
  turn's input usage and — once it crosses the configured fraction of the
  effective context window — runs a real LLM summary over the older turns
  mid-run, appends a `compact-summary` marker (+ optional file/skill
  restoration) to the session, and continues. The next turn's wire view
  is the summary plus the preserved tail.

  `true` enables with defaults (`threshold: 0.85`, restore on); an
  [`AutoCompactConfig`](https://zidane.si/docs/reference/index.md#AutoCompactConfig) overrides individual knobs. Requires a window
  source — `autoCompact.contextWindow` or `provider.meta.contextWindow` —
  or the trigger stays inert. Reuses the same `shouldAutoCompact` policy
  the chat hosts use.

  Distinct from `compactStrategy` (per-turn wire-only tool-output trim)
  and from the always-on reactive `context_exceeded` recovery. Observe
  via the `compact:start` / `compact:end` hooks.

  Default: `undefined` (off). The built-in chat profiles enable it.
- `backgroundOutputCap?` (`number`): Byte cap on each background task's output log. Past the cap the
  task KEEPS RUNNING; further output is dropped and a
  `<output-truncated bytes-dropped="N"/>` marker is appended to the
  log when the task settles. Guards long unattended runs (dev
  servers, training jobs) from filling the disk.

  Default: `undefined` (no cap).
- `backgroundStallWatchdogMs?` (`number`): Stall watchdog for background tasks: when a task produces no output
  for this many milliseconds, a `background:stall` hook fires and a
  one-shot `<task-stall>` notification is queued for the model's next
  turn — typically prompting it to inspect the log and kill a task
  stuck at an interactive `(y/n)` prompt. The process is NOT killed.
  The watchdog re-arms after fresh output.

  Default: `undefined` (no watchdog).
- `cache?` (`boolean`): Enable provider prompt caching. When on (default), the provider marks the
  system prompt, tools, and the last stable message with cache breakpoints so
  the shared prefix is served from cache across turns.

  - Anthropic: `cache_control: { type: 'ephemeral' }` on the last `system`
    content part, the last tool, and the last message content part.
  - OpenAI-compatible / OpenRouter: same shape — honored by Anthropic-backed
    OpenRouter routes and by Gemini; ignored (no-op) by providers that cache
    automatically (OpenAI, DeepSeek, Grok, Groq, Moonshot).

  Usage is surfaced via `TurnUsage.cacheRead` / `TurnUsage.cacheCreation`.

  Default: `true`.
- `cacheTtl?` (`CacheTtl`): Lifetime of the provider's prompt-cache entries.

  - `'5m'` (default) — the provider's standard ephemeral cache. Writes bill
    at 1.25x input; each hit refreshes the window for free.
  - `'1h'` — Anthropic's extended cache (`cache_control: { type: 'ephemeral',
    ttl: '1h' }` plus the `extended-cache-ttl-2025-04-11` beta header).
    Writes bill at 2x input; reads stay at \~0.1x. Also forwarded on
    `openrouter` (the `cacheBreakpoints` openai-compat route), which accepts
    the `ttl` field on Anthropic-family models without a beta header.

  The 5-minute window is the dominant cache cost for INTERACTIVE sessions: a
  user who reads output, thinks, and types for six minutes loses the entire
  cached prefix — a 100% re-bill on the next turn — no matter how
  byte-stable the wire is. `'1h'` converts that idle-loss into a cheap read
  for the price of costlier writes; it pays for itself as soon as one
  would-be expiry per hour is avoided. Headless back-to-back runs (seconds
  between turns) should stay on `'5m'` — they never idle long enough to
  benefit from the 2x write premium.

  Applied uniformly to every breakpoint (Anthropic requires 1h blocks to
  precede 5m blocks in a request; a uniform TTL sidesteps the ordering
  constraint). Ignored by providers without a TTL concept. No-op when
  `cache` is off, and degrades to `'5m'` on the Anthropic OAuth (Claude
  Code persona) path — only api-key auth has been verified against the
  extended-TTL beta, and an unaccepted beta would 400 every request.

  CAVEAT — verify the benefit on your account before relying on it. The
  request shape is byte-verified (`ttl` + beta header on the wire), but in
  our live measurement (2026-07, sonnet-4-6, direct API) a 1h entry did NOT
  survive a 6-minute idle; whether that is model support, account tier, or
  plain eviction is not distinguishable from outside. The gated idle test in
  `test-inference/cache-stability.inference.test.ts` (`INFERENCE_SLOW=1`)
  measures it for your credentials.

  Default: `'5m'`.
- `codemode?` (`boolean | { catalog?: 'full' | 'names'; directCallNotes?: boolean; inlineBudget?: number; mode?: 'on' | 'only'; streamProgress?: boolean }`): Opt in to the `codemode` tool: the model writes JavaScript that calls the
  run's other tools from a QuickJS sandbox, and only the script's output
  enters the context (see `src/tools/codemode/`). Needs the optional peer
  dependency `quickjs-wasi`.

  - `true` / `{ mode: 'on' }`: `codemode` is added next to the usual tools.
  - `{ mode: 'only' }`: the native tools scripts can call leave the wire
    and are declared inside the `codemode` description instead (within
    `inlineBudget` estimated tokens, default 3000), and the injected
    `tool_search` / `call_tool` are dropped — scripts discover MCP tools
    with `searchTools()`.
  - `catalog`: how the system prompt lists lazy (MCP) tools while codemode
    is on. `'names'` (default) lists names by server, since scripts find
    and describe tools with `searchTools()` / `describeTool()`; `'full'`
    keeps every tool's description.
  - `directCallNotes`: in `'only'` mode, a direct call to a tool declared for
    scripts still runs, and its text result gets one line naming the script
    form (at most twice per tool in a conversation; never on `read_file` or
    JSON results). `false` leaves results as the tools return them.
    Default: `true`.
  - `streamProgress`: report each script's progress live (`tool:progress`):
    what it prints, the output of its shell calls as they run, and the
    script line of each call (`scriptLine` on the nested calls' hooks,
    `line` on their records). The model's result does not change.
    Default: `false`.

  Not injected when `AgentRunOptions.tools` replaces the registry, or when
  the host registers its own `codemode`. Default: off.
- `compactKeepTurns?` (`number`): Number of trailing turns to leave untouched during tail compaction. The
  most-recent `compactKeepTurns` user/assistant messages are not eligible
  for elision so the model keeps the freshest tool context. Default: `4`.
- `compactLowWaterFraction?` (`number`): Hysteresis for `compactStrategy: 'tail'`. When set (`0 < f < 1`), once
  kept tool output exceeds the threshold the frontier trims it down to
  `f × threshold` in one step, then waits until it exceeds the threshold
  again, instead of advancing a chunk every few turns. The frontier is a
  pure function of the message prefix, so appending messages never moves an
  earlier step: each step re-bills the suffix once, and steps are spaced by
  about `(1 - f) × threshold` of new tool output. `compactKeepTurns` stays
  a floor. Out-of-range values are ignored. Default: `undefined` (keep the
  largest recent suffix under the threshold, re-evaluated every turn).
- `compactSkip?` (`{ minContextWindow?: number; providers?: readonly string[] }`): Skip client-side `compactStrategy: 'tail'` elision when the run matches:
  the serving provider's `name` is in `providers` (any provider when
  unset) AND the active context window is at least `minContextWindow`
  tokens (any window when unset; an unknown window never matches a set
  minimum). Auto-compaction stays the ceiling.

  Why: on Anthropic models with preserved thinking, every stubbed
  `tool_result` makes the API drop every later replayed thinking block, and
  on large windows each frontier step re-bills most of the prompt.
  Default: `undefined` (never skip).
- `compactStrategy?` (`'off' | 'tail' | CompactFunction`): Client-side context compaction strategy. Use this for non-Anthropic
  providers (OSS via cerebras / openai-compat / openrouter) that don't
  have a server-side equivalent. Anthropic users should prefer the
  server-side `context-management-2025-06-27` beta — see
  `AnthropicParams.contextManagement`.

  - `'off'` (default) — no client-side compaction.
  - `'tail'` — when total tool-output bytes in the persisted history
    exceed `compactThreshold`, replace older `tool_result` outputs with a
    short stub, keeping the newest `compactKeepTurns` turns intact. The
    compaction is applied to the wire-level message list only; the
    underlying session turns are not modified.
  - **Function** ([`CompactFunction`](https://zidane.si/docs/reference/index.md#CompactFunction)) — a pluggable replacement
    that the loop calls instead of the built-in tail elision. The
    function receives the post-`context:transform` wire messages and
    returns the compacted view (sync or async). Use this to plug in
    an LLM summarizer ("`<analysis>` / `<summary>`" style), a custom
    semantic-aware compactor, or anything else hosts need beyond the
    built-ins. The framework never modifies session turns — the same
    wire-only contract as `'tail'` applies.

  Preserved thinking (Anthropic): every wire rewrite of an earlier
  message also invalidates each replayed thinking block after it — the
  API drops them server-side (`provider:input-transformations`, reason
  `prefix_binding_mismatch`) and the model answers without that
  reasoning. The chunk-snapped frontier bounds this to once per chunk,
  but the cost of `'tail'` on Anthropic is cache rewrite + lost
  reasoning; server-side context management has neither.

  Default: `'off'`.
- `compactThreshold?` (`number`): Soft byte threshold that triggers tail compaction when
  `compactStrategy === 'tail'`. Counts the post-`context:transform` bytes
  of `tool_result` outputs across all messages. Default: `131_072` (128
  KiB). Ignored when compaction is off. See
  `compactThresholdWindowFraction` for the context-window-aware floor.
- `compactThresholdWindowFraction?` (`number`): Scale the tail-compaction threshold with the model's context window. When
  set and the run resolved a window (`autoCompact.contextWindow`, or the
  provider's `meta.contextWindow`), the effective threshold becomes
  `max(compactThreshold, fraction × contextWindow × 4 bytes/token)`; with
  no window the byte threshold stands alone.

  Why: every frontier step of tail compaction rewrites an early message on
  the wire, which re-bills the entire suffix at the cache-write rate. On a
  1M-context model a 128 KiB byte threshold trips on ordinary tool-heavy
  sessions and each step costs \~50x what keeping those bytes cached would
  (measured: 117k-token re-bills on a 157k-token session). A fraction of
  the window keeps small-context OSS models on the byte threshold while
  large-context runs leave compaction to the autocompact ceiling.

  `0.25` → 1M window: \~1 MiB; 200k: \~200 KiB; 128k: 128 KiB (byte floor
  wins). Default: `undefined` (byte threshold only).
- `contextWindow?` (`number`): Context window (tokens) of the model serving the run, for window-relative
  wire budgets such as `compactThresholdWindowFraction`. The SDK core
  has no model catalog; chat hosts inject the picked model's window here
  every run, independent of whether auto-compaction is enabled. Resolution
  order in the loop: this → `autoCompact.contextWindow` → the active
  `ModelTarget.contextWindow` → `provider.meta.contextWindow`. Does not by
  itself enable auto-compaction.
- `dedupReads?` (`boolean`): Deduplicate identical re-reads of the same file in `read_file`. When the
  model re-reads a file with the same slice and the bytes haven't changed
  since the last read in this session, the tool returns a short stub
  instead of re-emitting the full content. Pairs with the read-before-edit
  guard in `edit` / `multi_edit`.

  Requires a session (set via `createSession()`); without one, the flag is
  a no-op since per-session state has nowhere to live.

  Default: `true`.
- `dedupTools?` (`Record<string, DedupToolConfig>`): Generic per-tool argument deduplication. Keyed by the tool's **canonical**
  name (alias-stable). Each entry is a hasher: `(input) => string | undefined`.

  **Hasher contract** — three return values, three meanings:

  | Return | Meaning |
  | - | - |
  | a non-empty string | Cache key for this call. Equal keys (most-recent-only, this session) |
  | | replay the prior recorded result without re-dispatching the tool. |
  | `undefined` | **Skip dedup for this call.** The tool runs normally; nothing recorded. |
  | `''` / non-string | Treated identically to `undefined` (defensive: no dedup, no error). |

  The `undefined` opt-out is the way to say *"this specific call is not
  cacheable"* (timestamps in input, randomness baked in, debug flags). It
  is **not** the same as `JSON.stringify(input)` — that would dedup against
  the verbatim input. Pick one explicitly:

  ```ts
  // Always cache by full input — every identical re-call dedups.
  dedupTools: { my_pure_tool: input => JSON.stringify(input) }

  // Cache by a normalized subset; non-cacheable shapes opt out.
  dedupTools: {
    run_query: (input) => {
      const q = typeof input.query === 'string' ? input.query.trim().toLowerCase() : undefined
      if (!q || q.includes('now()') || q.includes('random()')) return undefined
      return q
    },
  }
  ```

  On a hit, the previously-recorded result is replayed as the tool_result
  without dispatching the tool. The substitution flows through `tool:gate`
  `result`, so `tool:after` and `tool:transform` still fire.

  Error results are NEVER recorded (and therefore never replayed): a
  failure is a state-dependent verdict, and replaying it would freeze it —
  e.g. an `edit` rejected by `requireReadBeforeEdit` would keep failing
  with "has not been read" even after the model reads the file, as long
  as the retry hashes to the same key. Failed calls always re-execute.

  Requires a session (`createSession()`); without one, the map is a silent
  no-op since per-session state has nowhere to live. Tools with side
  effects or non-deterministic outputs (network, time, randomness) MUST
  NOT be listed — there is no safety net beyond the consumer's hasher.

  For MCP tools, key by the namespaced wire name (`mcp_<server>_<tool>`).
  Concurrency-safe siblings (`ToolDef.isConcurrencySafe`) in the
  SAME assistant turn race against each other — none can dedup against
  a sibling that started in the same batch. Unsafe tools act as barriers
  and honor submission order within a turn, so an unsafe-but-listed tool
  follows the cache cleanly.

  **Cache policy**: only the most recent `(hash, result)` per tool is
  retained. Interleaved patterns (input A, input B, input A) miss on the
  second A because B overwrote it. Sufficient for the common spam-the-
  same-call loop; consumers needing a richer cache should hook
  `tool:gate` directly.

  Default: `undefined` (no per-tool dedup).
- `destroyTimeoutMs?` (`number`): Watchdog for `agent.destroy()` teardown awaits (in milliseconds).

  Bounds each awaited teardown step (pending MCP warmup, MCP connection
  `close()`, execution-context `destroy()`) so a custom connector / sandbox
  whose `close()` never settles can't strand `destroy()` — which hosts
  await before releasing their own run gate (TUI session switch). Steps
  that time out are abandoned and teardown continues. Set `0` to disable.
  Default: 30000 (30 seconds).
- `disableBackgroundTasks?` (`boolean | 'non-durable'`): Hide the built-in `shell` tool's `run_in_background` field + the
  background-mode paragraphs in its description, even when
  `AgentBehavior.tasksDir` is set. The model never sees the flag
  and won't try to use it.

  Combined with the implicit gate on `tasksDir` (background mode also
  auto-hides when `tasksDir` is unset, since the host hasn't wired the
  log dir), this gives two ways to opt out:

  - **Implicit**: don't set `tasksDir`. The host hasn't opted in.
  - **Explicit**: set `disableBackgroundTasks: true`. The host has
    `tasksDir` for some other reason (legacy, fixture) but doesn't
    want the model spawning background work.

  The `'non-durable'` form is capability-aware: backgrounding is
  disabled only when the execution context's
  `capabilities.detachedTasks` is NOT `'durable'`. It is the default on a
  replay-mode durable runtime (`AgentOptions.durable`), so a
  remote context whose tasks survive worker crashes keeps background
  mode, while host-local `ProcessContext` tasks — which would not
  survive process death — stay disabled.

  Only applies to the framework-provided `shell` tool (identity check
  against the exported `shell` constant). Hosts who wrap or replace
  the shell tool own their spec — the auto-disable doesn't touch
  tool defs the agent doesn't recognize.

  Default: `false`.
- `effortHint?` (`boolean`): Emit the level-derived `output_config.effort` hint on Anthropic requests
  (default: on). Set `false` for Anthropic models that reject the effort
  field: the level still decides whether thinking is on (`'off'` vs the
  rest) and an explicit `thinkingBudget` still routes to the `enabled`
  path — but no `output_config.effort` is sent, so without a
  `thinkingBudget` every budgeted level (`minimal`..`max`) produces the
  same plain-adaptive request and the effort distinctions between them are
  lost. Anthropic-only; other providers derive their reasoning fields from
  the level itself and ignore this.

  Run-wide: spawned subagents inherit the behavior, so disabling the hint
  also strips it from any Anthropic model a child runs on, including ones
  that accept the effort field.
- `elideStaleReads?` (`boolean | StaleReadPolicy`): How to handle older `read_file` results once a successful `edit` /
  `multi_edit` / `write_file` later in the same run modified the same path.

  - `'stub'` (or legacy `true`) — replace the stale read's `tool_result`
    with a short stub on the **wire**; persisted session turns keep the
    original content. Reclaims the context the model would otherwise carry
    forward "in case it needs it", but REWRITES THE MESSAGE PREFIX at the
    stale read's position — a prefix cache invalidates everything from that
    point, so each newly-stale read re-bills the conversation at the
    cache-write rate. Measured on a 129-turn production trace: 13
    invalidations, \~0.25M re-billed chars. On Anthropic the same rewrite
    also drops every replayed thinking block after the stale read
    (preserved-thinking prefix binding; surfaced as
    `provider:input-transformations`), so each stub costs reasoning too.
    Prefer `'notice'` there.

  - `'notice'` — append-only. The stale read keeps its bytes; instead, a
    `<system-reminder>` naming the stale path is appended to the mutating
    tool's own result the moment the mutation succeeds. Nothing earlier in
    the transcript ever changes, so the prompt cache never breaks. The
    trade: stale file bodies stay in context (billed at the \~0.1x
    cache-read rate) and the transcript reaches compaction thresholds
    sooner. On the same trace this halves the remaining cache spend —
    keeping stale bytes cached is cheaper than re-billing the conversation
    to delete them unless the run outlives \~12.5x(prefix/staleBytes) turns.

  - `false` / `undefined` — off: no stubs, no notices.

  Detection is identical and conservative in both modes — only a
  tool_result confirming success (`Edited …`, `Created …`, `Updated …`)
  marks prior reads stale. Failed edits and `No change needed` write_file
  calls do NOT. Write safety never depends on this knob:
  `requireReadBeforeEdit` compares content hashes at edit time.

  Default: `false`. The built-in chat profiles leave it off.
- `extensions?` (`Record<string, unknown>`): Namespaced bag for host-only configuration. The framework never
  reads these — they ride along through behavior resolution
  (run-level keys shallow-merge over agent-level ones) and surface on
  `ToolContext.behavior.extensions` so custom tools / hooks can read
  host policy without the host monkey-patching `AgentBehavior` with
  unknown top-level keys.

  Default: `undefined`.
- `fetchUrlAllowHosts?` (`readonly string[]`): Egress allowlist for the web tools (`fetch_url`, `web_search`). When set
  to a non-empty list, those tools refuse any request whose target host
  isn't covered — on `fetch_url` this is checked for the initial URL AND
  every redirect hop (alongside the SSRF blocklist); on `web_search` it
  gates the keyless DuckDuckGo / Tavily / Brave backends so a sandboxed or
  compliance-bound host can pin egress to approved domains.

  Matching is host-suffix based and case-insensitive: an entry `example.com`
  matches `example.com` and any subdomain (`docs.example.com`), but not
  `notexample.com`. Ports are ignored (match on hostname only). Leading
  dots are tolerated (`.example.com` ≡ `example.com`).

  Default: `undefined` (no allowlist — every public host is reachable,
  subject to the SSRF blocklist). An empty array is treated the same as
  `undefined` so a host can't accidentally lock itself out with `[]`.
- `fetchUrlCacheTtlMs?` (`number`): Enable an in-process response cache for the `fetch_url` tool, with this
  TTL in milliseconds. Keyed on the requested URL string. When a fan-out
  re-fetches the same page within the window (e.g. spidering a docs site
  across parallel tool calls), the cached body is returned without a second
  network round-trip.

  The cache is bounded by entry count and total bytes (fixed internal caps)
  and is **process-local** — it never persists across restarts and is not
  shared between agents. Only successful `2xx` text responses are cached;
  redirects, errors, and aborts are not.

  Default: `undefined` / `0` → caching disabled (every call is a fresh
  fetch — the stateless default). Set e.g. `900_000` (15 min) to enable.
- `hookTimeoutMs?` (`number`): Watchdog for awaited hook dispatch (in milliseconds).

  Bounds every awaited `hooks.callHook(...)` the agent loop and run
  lifecycle await — `context:transform` / `system:transform` /
  `turn:before` / `stream:start` / `turn:after` / `stream:end` /
  `tool-results:after`, plus the run-setup (`session:start`,
  `agent:start`) and run-teardown (`agent:done`, `session:end`,
  `session:turns`, `skills:deactivate`) hooks. A host handler that never
  resolves would otherwise hang the first turn (before the provider
  watchdog can run) or strand the `running` gate during cleanup.

  State-mutating pre-stream hooks surface a timeout as a typed
  `OperationTimeoutError` (fail-closed — a half-applied transform must
  not reach the wire); observability/lifecycle hooks log under
  `ZIDANE_DEBUG` and continue. Generous by design — the goal is catching
  infinite hangs, not policing slow handlers.

  Set `0` to disable. Default: 300000 (5 minutes).
- `maxConcurrentTools?` (`number`): Maximum number of tools that may be in flight concurrently within a
  single assistant turn. The scheduler dispatches concurrency-safe tools
  (`ToolDef.isConcurrencySafe`) in parallel up to this cap; unsafe tools
  act as barriers (wait for the fleet to drain, then run alone).

  Default: `10`. Set to `1` to force fully sequential dispatch regardless
  of per-tool flags — useful for deterministic debugging / eval-grade
  runs. Values `< 1` are clamped to `1`.
- `maxConsecutiveLengthResumes?` (`number`): Cap on consecutive `resumeOnMaxTokens` resumes. Every resume re-bills the
  full input context and can re-spend the entire output budget, so the cap
  is what turns a stuck model from an unbounded burn into a bounded one.
  When consecutive length resumes reach it, the loop exits cleanly with the
  last turn's `TurnUsage.finishReason` still `'length'` so consumers can
  surface the truncation from `stats`.

  Only bounds turns that produced visible text: a resumed turn with no text
  and no tool calls (a pure reasoning spin that restarts from scratch every
  resume) is bounded separately and much tighter — after 2 consecutive such
  turns the run fails with [`AgentLengthResumeExhaustedError`](https://zidane.si/docs/reference/index.md#AgentLengthResumeExhaustedError) instead
  of silently completing.

  NOTE: semantics match `AgentBehavior.maxConsecutiveRefusalResumes` —
  `0` DISABLES the recovery entirely (a truncated turn ends the run
  immediately, as if `resumeOnMaxTokens` were off); unlimited resumes are
  never offered.

  Default: `8`.
- `maxConsecutivePauseTurns?` (`number`): Hard cap on consecutive Anthropic `pause_turn` recoveries within a
  single run. Anthropic 4.6+ may return `stop_reason: 'pause_turn'`
  mid-turn for server-side reasons; the loop injects a "Please continue."
  user message and re-enters. With some 4.6 streaming edge cases the
  model returns empty pauses indefinitely, which without this cap spins
  forever burning tokens (the host's only escape was an external abort).

  When the consecutive-empty-pause counter reaches this value, the loop
  exits cleanly. The last turn's `TurnUsage.finishReason` stays
  `'pause'` so consumers can detect pause exhaustion from `stats`. The
  `agent:abort` hook does NOT fire — this is graceful termination, not
  user cancel.

  Only turns with no tool calls, no text, AND no thinking count toward
  the cap (the case Claude Code's v2 SDK collapses to `stop` outright).
  A pause turn with text resets the counter — productive turns are
  never penalised.

  Defaults to `0` (UNLIMITED) — empty pauses are vanishingly rare and
  silently ending a live trajectory is worse than the tail risk of a spin.
  Set a positive integer to re-arm the cap.
- `maxConsecutiveRefusalResumes?` (`number`): Cap on consecutive refusal recoveries within a single run. A turn the
  provider ends with `finishReason: 'content-filter'` (e.g. Anthropic
  `stop_reason: refusal`) and no tool calls would otherwise end the run
  silently mid-task — an empty assistant turn and a "completed" status
  that reads as a stalled session. The loop injects a synthetic continue
  (marked via `SessionTurn.injected` so replays render it as a notice, not
  a user prompt) and re-enters; refusals are usually transient.

  Every resume re-bills the full input context, so keep the cap small.
  When consecutive refusal resumes reach it, the loop exits cleanly with
  the last turn's `TurnUsage.finishReason` still `'content-filter'` so
  consumers can surface the refusal from `stats`.

  NOTE: semantics deliberately differ from
  `AgentBehavior.maxConsecutivePauseTurns` — `0` DISABLES the
  recovery entirely (a refusal ends the run immediately); unlimited
  refusal retries are never offered.

  Default: `3`.
- `maxCostUsd?` (`number`): Run-level cost ceiling, expressed in USD. After each turn, the sum of
  `TurnUsage.cost` across the run is compared against this value; if the
  total has crossed the cap, the loop throws `AgentBudgetExceededError`
  with `limit: 'cost'`. The agent finalizes the session run as
  `'aborted'` so partial spend is recorded.

  Complements `maxTurns`: a single expensive turn (large context
  window, deep reasoning) can blow past a turn-count cap but cost is
  the unit operators actually want to bound for unattended runs.

  Checked **post-turn**, so the run may exceed by up to one turn's
  spend before tripping — soft cap, not exact. Default: unbounded.
  Set `0` to disable explicitly (negative / `NaN` are ignored).

  Requires the provider to populate `TurnUsage.cost` (every built-in
  provider does). For providers that don't, the check silently no-ops
  and `maxTotalTokens` is the right knob instead.
- `maxImageEdge?` (`number`): Downscale inline PNG images whose longest edge exceeds this many pixels,
  once, when they enter the session (tool results and prompt images). The
  persisted turn holds the shrunk bytes, so the prompt cache sees the same
  prefix on every request. Other formats and PNGs the built-in decoder
  doesn't handle (interlaced, 16-bit) pass through untouched.

  `2000` is the largest edge Anthropic accepts once a request carries more
  than 20 images, so media can stay inline without that rejection. Claude
  4.7+ models see up to 2576 px and older ones 1568 px, so it keeps most of
  the detail either way while cutting upload size.

  Default: `undefined` (off). The built-in chat profiles, `runHeadless`
  and the Restate start runner use `2000`.
- `maxPairingRepairsPerTurn?` (`number`): Per-turn ceiling on pre-send pairing repairs before the run is failed
  with [`AgentToolPairingError`](https://zidane.si/docs/reference/errors.md#AgentToolPairingError).

  The pairing pass repairs the WIRE view only — the persisted session
  keeps its corrupted turns by design ("don't rewrite the past"). When a
  provider systematically reuses tool_use ids, every turn re-heals the
  entire history, the model never sees its own results (they were
  stripped as orphans), and the repair count grows quadratically while
  the run makes zero forward progress. This breaker converts that
  invisible loop into a typed failure carrying the full repair list.

  Sizing guidance: legitimate corruption (a crash window, a single buggy
  turn) produces a handful of repairs per turn; storms produce hundreds
  per turn within a few turns. `200` is a safe ceiling that can't misfire
  on healthy sessions.

  Default: `undefined` (disabled: repair silently every turn, never
  throw). Opt in to get the breaker.
- `maxTokens?` (`number`): Hard cap on output tokens per LLM response. When unset, the provider uses
  a model-driven default — Anthropic sends the model's max output (the field
  is mandatory there), OpenAI-style providers omit it so the model runs to a
  natural stop. Set a number to impose a fixed per-turn output ceiling.
- `maxTotalTokens?` (`number`): Run-level token ceiling — sum of `TurnUsage.input + TurnUsage.output`
  across the run. Same post-turn semantic + soft-cap behavior as
  `maxCostUsd`; throws `AgentBudgetExceededError` with
  `limit: 'tokens'` when exceeded.

  Cache reads / creations are **not** included in the sum — they're
  billed at a steep discount, so counting them at par would make the
  ceiling under-estimate the run's true affordability. Operators
  wanting a stricter accounting should override via the `usage` hook.

  Default: unbounded. Set `0` to disable explicitly.
- `maxTurns?` (`number`): Max agent loop iterations.

  Default: unlimited (Infinity). The loop runs until the model signals
  completion (no tool calls / `end_turn`), the abort signal fires, or this
  cap is hit. Set a finite value as a safety net for runaway loops.
- `maxTurnsWarning?` (`number`): Inject a synthetic user message when this many turns remain before
  `maxTurns`, telling the model to wrap up.

  Without it, a run that grinds into the cap dies silently mid-loop —
  the model never learns it was on a clock, produces no closing summary,
  and the user gets a truncated session (observed in production: a
  200-turn run terminated by the cap during a read→edit ping-pong, with
  no final answer). The warning gives the model a deterministic chance
  to land the work: it fires once, after the turn that leaves exactly
  this many turns remaining, riding the same steer pathway as
  `toolBudgets` nudges.

  Only meaningful when `maxTurns` is finite. Values `>= maxTurns` or
  non-positive are ignored. Default: `undefined` (no warning).
- `maxTurnsWarningMessage?` (`string | ((ctx: { maxTurns: number; remaining: number }) => string)`): Optional custom message for `maxTurnsWarning`. When unset, the loop
  uses its built-in wrap-up warning. String form is sent verbatim; function
  form receives the remaining/maximum turn counts and can tailor the nudge.
- `maxWallMs?` (`number`): Run-level wall-clock ceiling in milliseconds. The loop records the
  run's start via `AgentClock.now()` and compares elapsed time against
  this value after each turn; when crossed it throws
  `AgentBudgetExceededError` with `limit: 'wallClock'`. Same post-turn
  semantic + soft-cap behavior as `maxCostUsd` — a single long
  turn can overshoot the deadline before the check runs.

  Time is read through the run's clock, so a run on a replay-mode
  durable runtime (whose clock is journaled) gets a
  replay-stable deadline: the breach trips at the same turn boundary
  on every replay of the same invocation. This is the supported
  replacement for host-side timer + `agent.abort()` deadlines, which
  cannot be made journal-safe from outside the loop.

  Default: unbounded. Set `0` to disable explicitly (negative / `NaN`
  are ignored).
- `mcpConnectTimeoutMs?` (`number`): MCP connect watchdog in milliseconds.

  The built-in `connectMcpServers()` has per-server bootstrap timeouts. This
  aggregate guard covers the whole connect step, built-in or custom, so an
  embedder's `mcpConnector` that returns a promise that never settles cannot
  block the first model call.

  `maxWallMs`, when lower, bounds this value during `run()`. Set `0` to
  disable. Default: 120000 (2 minutes).
- `mcpToolNameSeparator?` (`'_' | '__'`): Separator used for provider-facing MCP tool names.

  Internal/canonical names stay `mcp_<server>_<tool>` for sessions, hooks,
  budgets, and dispatch. This option only controls the wire name shown to
  the model via generated aliases. Explicit `toolAliases` still win.

  - `'_'` (default): `mcp_server_tool`
  - `'__'`: `mcp__server__tool`
- `mediaBudget?` (`MediaBudget`): Keep media inline and age it out only near the provider's hard request
  limits. While the inline image / document count stays ≤ `maxItems` and
  their base64 bytes ≤ `maxBytes`, nothing is touched. Past either, the
  OLDEST media ages out (same recoverable `attachment://<hash>` marker as
  `AgentBehavior.mediaKeepTurns`) in steps of half that limit, so the
  prefix is rewritten once per half a budget of new media instead of once
  per image. Stateless: the aged set is a pure function of the message
  list, so it only ever grows within a run.

  Images still wider or taller than 2000 px (formats
  `AgentBehavior.maxImageEdge` can't re-encode) keep the
  `mediaKeepTurns`-style 8-message window: once a request carries more than
  20 images, Anthropic rejects the whole request if any one exceeds 2000 px.

  `maxItems` is lowered to the provider's
  `capabilities.maxMediaPerRequest` when that is smaller (Bedrock: 20).
  Ignored when `mediaKeepTurns` is set.

  Default: `undefined` (off). The built-in chat profiles, `runHeadless`
  and the Restate start runner use `{ maxItems: 80, maxBytes: 20 MiB }`.
- `mediaKeepTurns?` (`number`): Age base64 media (top-level `image` / `document` blocks AND image /
  document parts nested in `tool_result` outputs) off the **wire** once it
  is older than the last `mediaKeepTurns` messages. The aged-out block is
  replaced with a short text marker carrying a re-resolve handle
  (`attachment://<hash>`); the persisted session keeps the original bytes
  untouched, so the model can pull any image back on demand with
  `read_file path="attachment://<hash>"`.

  This is the long-run de-bloat lever: a screenshot or pasted image is only
  carried as base64 for a few recent turns instead of riding the prompt
  forever. Distinct from `compactStrategy: 'tail'`, which only stubs
  `tool_result` *text* and never counts (or trims) media bytes.

  The frontier is chunk-snapped for prompt-cache stability — media drops
  out in discrete jumps, not one block per turn. `0` ages out ALL media;
  `undefined` (default) disables the pass entirely (media rides every turn).

  Prefer `AgentBehavior.mediaBudget` on prompt-cached providers: each
  step here rewrites an earlier message, and on a long session that re-bills
  the whole conversation at the cache-write rate — far more than keeping a
  \~1.6k-token screenshot as a cache read for the rest of the run.

  Takes precedence over `mediaBudget` when both are set at the same layer
  (a run's `behavior` overrides the agent's as a pair), so an explicit
  window keeps working on top of profiles that default to the budget.

  Preserved thinking (Anthropic): each age-out step rewrites an earlier
  message, which drops every replayed thinking block after the frontier
  server-side (`provider:input-transformations`). Same trade as
  `compactStrategy: 'tail'` — once per chunk, not per turn.

  Default: `undefined` (off).
- `modelCatalog?` (`ModelCatalog`): Override model metadata (output ceiling, pricing, context window,
  reasoning support) for this agent — and per run, like every other
  behavior field.

  Resolution is first-hit-wins: run behavior → agent behavior → the
  process-wide `setModelCatalog()` → pi-ai's bundled registry → local
  fallback tables. Entries merge FIELD-LEVEL over the bundled entry, and
  everything stays keyed by `(provider, modelId)`, so overriding one
  provider leaves the others fully bundled.

  Accepts a table keyed `"<provider>:<modelId>"` or a
  `{ get, list }` source — see `ModelCatalog`.

  ```ts
  createAgent({ provider, behavior: {
    modelCatalog: { 'anthropic:my-gateway-alias': { maxTokens: 128000 } },
  } })
  ```

  Note: the chat/TUI model picker resolves metadata before any agent
  exists, so it reads only the process-wide catalog.

  Default: unset — the bundled registry is authoritative.
- `modelOptions?` (`Record<string, boolean>`): Default model-specific options applied to every run, keyed by option id
  (e.g. `{ fast: true }`). A per-run `AgentRunOptions.modelOptions`
  takes precedence when set. See `StreamOptions.modelOptions`.
- `persistDir?` (`string`): Directory under which persisted tool-result blobs land. Each call's
  payload is written to `<persistDir>/<callId>.txt` (one file per
  `tool_use` id, atomic via write-then-rename).

  The chat layer resolves this to `<userDir>/tool-results/<sessionId>/`
  at session activation; SDK consumers pass an absolute path. Required
  when `AgentBehavior.persistThreshold` is non-zero — when unset
  the framework treats persistence as disabled.

  Default: `undefined`.
- `persistExcludeTools?` (`readonly string[]`): Canonical tool names to exclude from disk persistence regardless of
  output size. The framework bypasses persistence for any tool whose
  canonical name appears in this list — useful for tools whose results
  are intentionally part of the prompt (`skills_use`), short envelopes
  (`tool_search`, `present_plan`, `ask_user`), or where persistence
  would be circular (`read_file`, whose pagination already serves the
  same use case).

  Default: `undefined` (no exclusions). The chat-layer built-in profiles
  set their own list — see `src/chat/agents.ts`.
- `persistMaxBytes?` (`number`): Soft byte-cap on the cumulative size of persisted blobs under
  `persistDir` for THIS session. After every successful blob
  write the framework sums the directory's `*.txt` payloads and, if
  the total exceeds this value, removes oldest-first by mtime until
  the remainder is at or below the cap.

  The just-written blob is never evicted in the same sweep (its mtime
  is the newest), so a single oversize result still lands and gets
  pointed at by its `<persisted-output>` stub — the LRU is a steady-
  state housekeeping mechanism, not a per-call admission gate.

  Long unattended runs can otherwise grow `<userDir>/tool-results/<sessionId>/`
  without bound; session-delete cleanup runs on demand, not on
  schedule.

  Default: `undefined` (no cap). Set `0` to disable explicitly; the
  eviction step is a no-op for non-positive / non-finite values.
- `persistThreshold?` (`number`): Persist large `tool_result` outputs to disk and replace the in-message
  content with a `<persisted-output>` stub (preview + filesystem path).
  When the post-`tool:transform` byte size of a tool's result exceeds
  this threshold, the framework writes the full payload to
  `<persistDir>/<callId>.txt` and substitutes a fixed-format stub so the
  model sees a 2 KiB preview plus the path it can `read_file`.

  The substitution happens at emit time (just after `tool:transform` runs)
  and the stub flows into `session.turns` directly — so every subsequent
  turn re-emits the same bytes, keeping the prompt-cache prefix stable.

  Set `0` / `undefined` to disable. Built-in chat profiles default to
  `8192`. Tools listed in `AgentBehavior.persistExcludeTools` bypass
  regardless of size — typically because their output is intentionally
  short or persisting would be circular (e.g. `read_file`).

  Requires `AgentBehavior.persistDir` to be set; without a target
  directory the framework silently skips persistence (no throw, no
  substitution) since there's nowhere to write the blob.

  Default: `undefined` (off).
- `persistTurns?` (`boolean`): Persist completed turns to the session store **incrementally**, after
  every `turn:after` / `tool-results:after`. This is session-turn
  persistence — unrelated to the `persist*` knobs above, which spill large
  tool-result blobs to disk.

  - `true` (default) — append each turn to the store as it completes. A
    tool_use block is always durable alongside its matching tool_result, so
    a mid-run process death resumes cleanly from the last completed turn.
    Maximum crash-recovery granularity.
  - `false` — skip the per-turn append hooks; turns are persisted **once at
    run end** via the flush that already runs on every exit path (complete /
    abort / error). The seeded user prompt is still persisted up-front, so a
    crash keeps the prompt but loses the in-flight run's generated turns.

  Why turn it off: on a session store whose `appendTurns` re-serializes the
  whole session per call (file-map, remote), per-turn persistence is
  O(turns) writes of an O(turns)-sized payload — quadratic I/O over a long
  run. Hosts that save at the request boundary anyway (and accept the
  coarser crash-recovery granularity) get O(1) writes per run by disabling
  it. No effect on sessionless runs (nothing to persist) and the store stays
  attached either way, so `loadSession` / resume / explicit `session.save()`
  are unaffected.

  Default: `true`.
- `progressNudges?` (`false | { failedMutations?: number | false; maxNudges?: number; messages?: Partial<Record<ProgressNudgeKind, ProgressNudgeMessage>>; mode?: 'steer'; noopMutations?: number | false; readOnlyTools?: readonly string[]; readOnlyTurns?: number | false; singleCallTurns?: number | false; toolErrors?: number | false; validationRejects?: number | false }`): Advisory progress nudges for broad stalls that exact repeat guards do not
  cover: several read-only turns, failed/no-op mutations, validation rejects,
  or tool errors. All nudges are non-interrupting and off by default.
- `providerStreamIdleTimeoutMs?` (`number`): Provider stream inactivity watchdog in milliseconds.

  Bounds the gap between observable stream events (text / thinking /
  server-tool / tool-call-argument deltas) AFTER the first one arrived —
  the window `providerStreamStartTimeoutMs` does not cover. A stream that
  goes silent mid-generation (upstream disconnect the SDK never surfaces,
  a wedged proxy) otherwise hangs until `providerStreamTimeoutMs` (30 min
  default). If it expires, the loop aborts the provider request and
  surfaces a typed timeout; the retry path treats it like the other
  watchdogs (retryable, and eligible for fallback, only when no text or
  tool-call content streamed and no server-side tool executed; reasoning
  alone is dropped and regenerated).

  Off by default: legitimate silent gaps exist (server-side tool
  execution, very long prefill on providers that emit nothing until
  done), so hosts opt in with a value tuned to their provider mix —
  interactive products serving well-behaved streaming APIs can run this
  as low as 60-90s. `maxWallMs`, when lower, bounds this value too.
  Set `0` (or leave unset) to disable.
- `providerStreamStartTimeoutMs?` (`number`): Provider first-response watchdog in milliseconds.

  Starts at `stream:start` and is cleared by the first text/thinking/server
  tool callback or by the provider returning a tool-only/final result. If it
  expires, the loop aborts the provider request signal and surfaces a typed
  provider error instead of leaving `agent.run()` pending forever.

  `maxWallMs`, when lower, bounds this value too. Set `0` to disable.
  Default: 300000 (5 minutes).
- `providerStreamTimeoutMs?` (`number`): Provider total stream watchdog in milliseconds.

  Bounds the whole `provider.stream()` call, including streams that already
  produced partial output but never settle. If it expires, the loop aborts
  the provider request signal and surfaces the timeout as a provider error.

  `maxWallMs`, when lower, bounds this value too. Set `0` to disable.
  Default: 1800000 (30 minutes).
- `readFileDefaults?` (`{ limit?: number; maxBytes?: number }`): Default `read_file` slice limits for calls that omit `limit` / `maxBytes`.
  Explicit tool-call arguments still win. SDK default remains the tool's
  built-in defaults; hosts can tighten these for context-sensitive profiles.
  `maxBytes` caps text reads only; image and document reads keep the
  tool's attachment cap.
- `readLineNumbers?` (`boolean`): Prefix every line of `read_file` output with its 1-indexed line number
  followed by a tab (`<N>\t<content>`) — the compact `cat -n`-style
  format Claude Code emits. The `edit` tool strips the prefix from
  `old_string` / `new_string` so the model can paste back a numbered
  chunk verbatim without breaking the match.

  Set `false` to opt out — useful for callers piping `read_file` into
  downstream parsers that don't recognize the prefix. Reads inside codemode
  scripts skip the prefix whatever this says, since the script parses the
  text. Per-call `read_file({ lineNumbers })` overrides both defaults.

  Default: `true`.
- `repeatGuard?` (`RepeatGuardConfig`): Consecutive-identical-call repeat guard. Unlike `dedupTools` (keyed
  on argument hash, replays/blocks per the consumer's hasher) and
  `toolBudgets` (run-cumulative count regardless of payload), the
  repeat guard watches a **run-local chain of back-to-back identical
  tool payloads** and escalates:

  - At `blockThreshold` (default 4; `read_file` default 6)
    consecutive identical calls: either refuse the call via `tool:gate`
    `block` or let it execute and append a steering message, depending on
    `RepeatGuardConfig.thresholdAction`. Fires
    `repeat-guard:exceeded` with `action: 'block' | 'steer'`.
  - At `abortThreshold` (default 8; `read_file` default 12)
    consecutive identical calls: abort the whole run via the agent's
    `AbortController` (the loop then fires `agent:abort`, the same path a
    user cancel takes). A `repeat-guard:exceeded` event with
    `action: 'abort'` fires first so consumers can distinguish a guard abort
    from a user cancel. This is the only layer that stops a model which
    ignores the `Blocked:` reason and keeps retrying.

  "Consecutive" means the next tool call in the run is the same tracked
  tool with the same normalized payload. Any intervening tool call, tracked
  or not, breaks the chain.

  `mode: 'window'` intentionally relaxes that definition. `mode: 'both'`
  keeps the chain guard while also catching interleaved repeats inside
  `windowSize`.

  `tools` selects which tools the guard watches. Each entry is a predicate
  over the canonical tool name; a string entry matches that exact name. The
  default (when `tools` is unset) tracks the built-in `shell` and
  `read_file` tools — auto-approved retry / re-read loop magnets. Provide
  your own list to widen / narrow the set; an empty list disables the guard.

  `normalize` lets you collapse semantically-identical payloads to one
  streak key. The built-in normalizer for `shell`-shaped tools strips
  `cd <path> &&` prefixes, trailing `& sleep N ; <verify>` tails, and short
  flags. Return `undefined` to opt a specific call out of streak tracking
  entirely.

  Streak state is run-local and in-memory (no session required), torn down
  when the run ends.

  Default: `undefined` (no repeat guard). The built-in chat profiles enable
  it with the default thresholds.
- `requireReadBeforeEdit?` (`boolean`): Require `read_file` before `edit` / `multi_edit` on the same path.
  Eliminates the silent-corruption failure mode where a model "remembers"
  stale content and applies a substring edit against bytes that have
  moved.

  Never-read files are refused outright. Content DRIFT (read, but the
  bytes changed since — a formatter, codegen, an editor auto-save) is
  anchor-tolerant: the hazard drift creates is a mis-anchored
  `old_string`, so if the edit still resolves VERBATIM against the
  current bytes it proceeds with a disclosure note; when resolution
  fails — or only a fuzzy recovery matches, the one combination where
  mis-anchoring is genuinely plausible — the drift is refused
  ("re-read the file"), escalating on repeat refusals to name the
  likely external writer. Flat drift refusals are
  what taught models to abandon `edit` for shell one-liners that bypass
  every guard.

  Requires a session. Off by default; turn it on for stricter eval-grade
  runs where silent edit corruption would invalidate the result.

  Default: `false`.
- `resumeOnMaxTokens?` (`boolean`): Auto-resume on output-cap truncation. When `true`, a turn that hits
  `max_tokens` (`finishReason: 'length'`) without completing a tool call
  injects a synthetic "Please continue." and re-enters the loop so the model
  finishes its output, instead of the run silently ending mid-trajectory.
  The loop bounds consecutive resumes internally so a stuck model can't loop
  forever. Off by default; pairs with the model-driven `maxTokens` envelope
  that already prevents most truncation. Context-window exhaustion (also
  surfaced as `'length'`) is absorbed by auto-compaction on the resumed turn.
- `retry?` (`RetryConfig`): Bounded retry-with-backoff policy applied around `provider.stream()`.

  Triggers only on errors the provider classifies as `retryable: true`
  (typically 429 / 5xx HTTP responses and mid-stream SSE errors like
  Anthropic's `overloaded_error`). Terminal failures (auth, invalid
  request, context exceeded, aborts) skip retry and propagate.

  Skipped when text or tool-call content has already streamed this turn,
  or a server-side tool ran — replaying over partial output would produce
  a confusing transcript. Reasoning alone does not block a retry: it is
  dropped (see `discardedThinking` on the `stream:retry` hook) and the
  next attempt generates its own.
  Mid-stream continuation (replaying with the partial assistant response
  appended for the model to continue from) is a separate, larger change.

  The Anthropic SDK already retries pre-stream errors up to twice
  internally; this loop-level retry catches mid-stream failures and
  other providers that don't ship an SDK-side retry.

  Default: 3 attempts, 1s → 2s → 4s exponential with full jitter,
  capped at 30s per delay, with `retry-after` / `retry-after-ms`
  headers honored when present. Set `maxAttempts: 1` to disable.
- `schema?` (`Record<string, unknown>`): JSON Schema for structured output enforcement
- `shellLongTimeoutMs?` (`number`): Steering threshold for blocking `shell` calls, in milliseconds.

  When a foreground `shell` call requests a `timeout` ABOVE this value
  and background mode is actually available (schema exposes
  `run_in_background`, `tasksDir` is set, the execution context
  supports it), the call is refused with a steering message telling
  the model to re-issue with `run_in_background: true` (and
  `wait_task` when the next step depends on the result). This kills
  the "blocking shell pinned to a transport timeout" footgun — a model
  that picks a 600–900s blocking wait (e.g. `while pgrep …; do sleep`)
  holds its whole turn hostage to one exec call regardless of how good
  the background-mode prompt is.

  When background mode is NOT available the blocking call proceeds
  unmodified — refusing it would leave the model with no alternative.

  Default: `undefined` (off). Set `0` / negative to disable explicitly.
- `shellToolNudges?` (`boolean | { maxPerKind?: number }`): Append a one-line pointer to the dedicated tool when a `shell` call does
  work that tool covers: a pure file read (`read_file`), a pure content
  search (`grep`), or a scripted file edit — python/perl/`sed -i`/heredoc
  writes (`edit` / `multi_edit`). Shown only when that tool is advertised,
  at most `maxPerKind` times per kind per session (default 2), so it fades
  once the model has seen it.

  `true` uses the defaults; an object tunes `maxPerKind`.

  Default: off.
- `spawnTimeoutMs?` (`number`): Watchdog for the first `executionContext.spawn()` (in milliseconds).

  The process context spawns synchronously, but docker / sandbox / e2b /
  daytona contexts do daemon + network round-trips (image pull, container
  create/start) with no inherent timeout. This bounds that first spawn so
  a hung daemon surfaces a typed `OperationTimeoutError` instead of a run
  that never starts responding.

  `maxWallMs`, when lower, bounds this value too. Set `0` to disable.
  Default: 300000 (5 minutes). Raise for hosts doing large first-time
  image pulls on slow links.
- `strictToolPairing?` (`boolean`): Fail-fast instead of repair when the pre-send pairing pass detects
  corruption (orphan `tool_use` / `tool_result`, duplicate ids,
  compaction-stranded blocks). Throws [`AgentToolPairingError`](https://zidane.si/docs/reference/errors.md#AgentToolPairingError) from
  the next `agent.run()` turn carrying the structured repair list the
  loop would have performed.

  Use case: training-data collectors that must reject any transcript
  containing the synthetic `SYNTHETIC_TOOL_RESULT_PLACEHOLDER` rather
  than ship poisoned data to the fine-tuning pipeline. User-facing chat
  sessions should leave this off (the repair-on-the-fly behavior is the
  point of the pass).

  Telemetry note: `pairing:repair` still fires for every repair before
  the throw, so observability handlers see exactly what would have
  happened.

  Default: `false`.
- `surfaceMcpInstructions?` (`boolean`): Render the per-server `instructions` payload from the MCP `initialize`
  handshake as a `# MCP Server Instructions` section appended to the
  system prompt. Each server may return free-form guidance (e.g. "the
  database is already provisioned, use `apply_migration` directly")
  that materially changes how the model uses the server's tools.

  Matches the Claude Code SDK's default. Without this, models often
  try to bootstrap state the server already advertises as ready —
  e.g. running `npx some-cli init` in a loop when the MCP server's
  instructions would have said "skip the init, use `apply_migration`".

  The section is built once per run and embedded into the system
  prompt next to the skills + searchable-tools catalogs, so it rides
  the same system-prompt cache breakpoint. Server insertion order
  matches `mcpServers` config order for byte-stable output.

  Only eager-connect servers contribute. `lazyConnect: true` servers
  are excluded by design — pulling their instructions would force the
  connection the host asked to defer. Empty / whitespace-only payloads
  are normalized to "no instructions" so a server that returns `""`
  doesn't produce a stub heading.

  Default: `true`. Set `false` for paranoid embedders that want full
  control of the system prompt or need to avoid leaking server
  advisories into untrusted contexts.
- `taskBudget?` (`number`): Total thinking budget across the whole task, in tokens. Anthropic
  adaptive-thinking only — emitted as
  `output_config.task_budget = { type: 'tokens', total }` alongside the
  level-derived `effort` hint. Ignored where a task budget has no meaning:
  non-Anthropic providers, `thinking: 'off'`, and the explicit-budget
  `enabled` path (`thinkingBudget` set on a non-adaptive level). A
  whole-task cap, so it is never routed through `thinkingDecay`.
- `tasksDir?` (`string`): Absolute directory where the `shell` tool's background mode (the
  `run_in_background: true` flag) appends output log files. One file
  per task: `<tasksDir>/<task-id>.<context-timestamp>.log`. The model
  gets the absolute path back in the tool result and reads incremental
  output via the regular `read_file` tool.

  The chat layer resolves this to `<userDir>/sessions/<sessionId>/tasks/` at
  session activation; SDK consumers pass an absolute path. When unset,
  `shell({ run_in_background: true })` surfaces a clean error to the
  model so the framework doesn't silently fall back to a path the user
  didn't pick.

  Default: `undefined`.
- `textToolCallRescue?` (`false | { mode?: 'steer' }`): Steer when a model emits a textual tool call instead of a provider-native
  tool call. Execution of parsed text calls is intentionally not part of this
  public contract.
- `thinkingBudget?` (`number`): Thinking token budget — overrides the level-based default when set
- `thinkingDecay?` (`ThinkingDecay`): Taper the thinking budget over the course of a run. Late turns are
  usually checkpoint / cleanup work where reasoning rarely pays for
  itself; early turns benefit most. Three forms — see [`ThinkingDecay`](https://zidane.si/docs/reference/index.md#ThinkingDecay):

  - **Step** — `{ afterTurn, floor }`: full budget through `afterTurn`, then
    `floor` for the rest of the run. ONE budget change per run. This is the
    cache-aware schedule — prefer it whenever prompt caching is on.
  - **Geometric** — `{ afterTurn, factor, floor }`: multiply by `factor`
    each turn past `afterTurn`, clamped to `floor`. Example
    `{ afterTurn: 5, factor: 0.5, floor: 1024 }` with a base budget of 8192:
    turns 1-5 = 8192, turn 6 = 4096, turn 7 = 2048, turn 8+ = 1024.
  - **Function** — `(runTurn, baseBudget) => number`. Arbitrary curves;
    `runTurn` is 1-indexed, run-relative (resumed sessions reset).

  ## Prompt-cache cost — measured, not theoretical

  On Anthropic, `thinking.budget_tokens` is part of the prompt-cache key:
  a request with a different budget reads NOTHING back — `cacheRead: 0`,
  scaffold included — and re-bills the entire prefix at the cache-write
  rate (measured live; pinned in
  `test-inference/cache-stability.inference.test.ts`). Every distinct value
  a schedule emits is therefore one full re-bill: the geometric example
  above costs three (4096, 2048, 1024), a step schedule costs exactly one,
  and a function that returns a fresh value per turn costs one PER TURN —
  a per-turn curve silently disables caching for the rest of the run.

  No-op when `thinkingBudget` is unset. Applies to the per-turn thinking
  budget: the Anthropic explicit-budget `enabled` path and openai-compat
  `max_tokens` padding / `reasoning.max_tokens`. It does NOT touch
  `taskBudget` (a whole-task cap — per-turn decay is meaningless there).
  It is also skipped for `thinking: 'adaptive'` on providers that treat
  `thinkingBudget` as a whole-response envelope cap there (Anthropic —
  `ProviderCapabilities.adaptiveBudgetIsEnvelope`): decaying an envelope cap
  would strangle text/tool output, not just thinking. On providers where
  adaptive + `thinkingBudget` is a genuine per-turn thinking budget (e.g.
  openai-compat `reasoning.max_tokens`), adaptive still decays.

  Default: `undefined` (no decay).
- `thinkingOverrunPolicy?` (`'off' | 'steer-next-turn' | { message?: string; mode?: 'off' | 'steer-next-turn'; thresholdTokens?: number }`): When a provider streams more thinking text than expected, optionally nudge
  the next continuing turn toward concrete progress. This is advisory and
  non-interrupting: same-turn tool calls are still allowed to finish.
- `toolBatchExecutor?` (`ToolBatchExecutor`): Optional per-turn tool batch scheduler. When unset, Zidane uses its
  native sliding-window scheduler (on a replay-mode durable runtime, its
  journaled one, which joins concurrent calls through the runtime).
- `toolBatchTimeoutMs?` (`number`): Custom `toolBatchExecutor` watchdog in milliseconds.

  Native tool execution keeps its existing per-tool/cancellation semantics.
  This only bounds a host-provided batch executor; on timeout the executor's
  signal is aborted and the pending tool calls receive error tool results so
  a tool-only assistant turn cannot hang the run forever.

  Set `0` / `undefined` to disable. Default: disabled.
- `toolBudgets?` (`Record<string, { hardMax?: number; max: number; onExceed?: 'steer' | 'block' | ((ctx: { count: number; max: number; tool: string }) => { message: string; mode: 'steer' | 'block' }) }>`): Per-tool soft call budget for this run. Keyed by **canonical** tool name.
  On the first call after the run-cumulative dispatched count for that tool
  reaches `max`, the framework fires `onExceed`:

  - `'steer'` (default) — let the call execute, but emit a synthetic user
    message after the turn that nudges the model away from re-calling the
    tool. Reuses the existing post-turn steer pathway used by
    `toolOutputBudget`. Fires `tool-budget:exceeded` with `mode: 'steer'`.
  - `'block'` — refuse the call via `tool:gate` `block`. The model sees a
    `Blocked: <reason>` tool result. Fires `tool-budget:exceeded` with
    `mode: 'block'`.
  - **Function** — `(ctx) => { mode, message }`. The consumer supplies the
    steering / refusal text and chooses the mode dynamically.

  Counts include both real dispatches and dedup replays.
  Excludes calls already blocked by an earlier gate (skill allow-list,
  consumer hook). Tool dispatched by spawned subagents has its own per-run
  counter — child counts never charge the parent.

  For MCP tools, key by the namespaced wire name (`mcp_<server>_<tool>`).

  Atomic in parallel mode: the middleware tracks its own per-tool
  approval counter, incremented synchronously at gate-time. A
  4-call parallel batch against `max: 2` will let the first 2 through
  and refuse the rest, even though the loop's `runToolCounts` only
  propagates between calls (not within a single batch's gate fan-out).

  Default: `undefined` (no budget enforcement).
- `toolDisclosure?` (`'eager' | 'lazy'`): Tool disclosure strategy. Controls whether the model sees every tool's
  full `inputSchema` in its tool list every turn ("eager") or whether MCP
  tools are advertised as a name+description catalog in the system prompt
  and only get full schemas after being surfaced via the `tool_search`
  native tool ("lazy" / progressive disclosure).

  Native tools (those passed to `createAgent({ tools })`) and skill tools
  are always eager — they are core to the agent and cheap. Only MCP tools
  are eligible for lazy disclosure.

  When `'lazy'`, the agent:

  - Appends a `<searchable_tools>` section to the system prompt listing
    every MCP tool by `name` + `description` only (no `inputSchema`).
  - Auto-injects a `tool_search` native tool (opt out via
    `AgentBehavior.toolSearch`) the model uses to load schemas on
    demand. Surfaced tools persist for the rest of the run.
  - Rebuilds the wire-level tool list each turn in two phases: every
    initially-eager tool in registry order (the cache-stable prefix),
    then every dynamically-unlocked tool in **unlock order** at the
    tail (`toolSearch.mode: 'unlock'`).

  Trade-off of unlock mode: providers hash the request prefix as
  `tools → system → messages`, so ANY change to the tools array — even a
  tail append — re-bills the system prompt and every message. Each
  discovery wave therefore costs one full prompt-cache rewrite; on a long
  conversation that dwarfs the schema bytes lazy disclosure saved.
  `toolSearch.mode: 'dispatch'` (the default) removes the rewrite
  entirely: surfaced tools are invoked through a fixed `call_tool`
  envelope and the tools array never changes. See
  `AgentBehavior.toolSearch`.

  Default: `'eager'`.
- `toolOutputBudget?` (`number`): Soft per-turn cap on total tool-output bytes. When the sum of `outputBytes`
  across a turn's tool results exceeds this value, the loop injects a
  synthetic user message instructing the model to summarize before calling
  more tools, and fires the `budget:exceeded` hook.

  Measured **post-`tool:transform`** so consumer truncation counts toward the
  budget. Off by default (undefined / `0` disables the check). A reasonable
  starting value for OSS-model integrations is `32768`.
- `toolOutputBudgetExcludeTools?` (`readonly string[]`): Canonical tool names whose output is exempt from
  `AgentBehavior.toolOutputBudget` accounting. Their text-equivalent
  bytes don't count toward the per-turn cap and the "summarize before
  calling more tools" nudge is not triggered by them alone.

  Intended for tools whose entire purpose is to LOAD context into the
  conversation — penalising them via the budget creates the exact
  failure mode the budget is meant to prevent (the model gets steered
  away from the very tool it just called to make progress):

  - `tool_search` — returns full `inputSchema` payloads for MCP tools the
    model needs to call next. A Notion MCP server with 20+ tools easily
    exceeds 64 KiB of schema JSON in a single discovery call.
  - `skills_use` / `skills_read` — inject skill content; that text IS
    the value of the call.

  For MCP tools, key by the namespaced wire name (`mcp_<server>_<tool>`).
  The matching happens on the canonical (registry-key) name, so aliases
  are stable.

  Default: `undefined` — every tool counts. `DEFAULT_BUDGET_EXCLUDE_TOOLS`
  (`zidane/chat`) is a ready-made list of the context-loading tools.
- `toolSearch?` (`{ catalog?: 'full' | 'names'; limit?: number; mode?: 'unlock' | 'dispatch'; tool?: false }`): Fine-grained config for the `tool_search` tool auto-injected when
  `AgentBehavior.toolDisclosure` is `'lazy'`. No-op in eager mode.

  - `tool: false` — opt out of the auto-injection entirely. Use when the
    host wants to ship a custom discovery tool. Note that the catalog
    text drops the call-to-action prose in this case so the model isn't
    pointed at a non-existent tool.
  - `limit` — default cap on results returned per `tool_search` call when
    the model omits the parameter. Default: `20`.
  - `mode` — how a surfaced tool becomes callable:
    - `'unlock'`: its schema is appended to the wire-level tools array on
      the next turn. Simple, but every discovery wave changes the tools
      array and rewrites the whole prompt cache. On Anthropic it also
      invalidates every replayed thinking block (preserved-thinking
      prefix binding), so the model loses its earlier reasoning at each
      wave.
    - `'dispatch'`: the tools array stays byte-stable for the run. A
      fixed `call_tool { name, arguments }` native tool is added to the
      eager set; `tool_search` still returns the schema into the
      transcript and the model calls the tool through the envelope. The
      loop unwraps it before dispatch, so gating, argument validation
      (against the inner tool's schema), hooks, telemetry and transcript
      events all see the inner tool exactly as with a direct call.

  Note on host-defined `tool_search`: a tool the host registers under the
  name `tool_search` (or under any alias whose canonical is `tool_search`)
  will shadow the auto-injected one — the catalog text will point at the
  host's wire name, but driving the unlock flow requires either using
  `createToolSearchTool({ catalog, unlocked })` from `tools/tool-search`
  (which internally mutates the unlock set) or fully opting out via
  `toolSearch.tool: false` and treating discovery as a host-side concern.
  A bare host tool that doesn't touch the unlock set will not advance the
  lazy disclosure state and the hard gate will keep refusing lazy calls.

  Default: `undefined` (auto-inject with the default limit). `mode`
  unset resolves to `'dispatch'` on every provider; set `'unlock'`
  explicitly to append surfaced tools instead.
- `trackShellReads?` (`boolean`): Count plain shell file reads toward the read-state that
  `AgentBehavior.requireReadBeforeEdit` checks. After a successful
  `shell` call, files read in full view by `cat`, `head`, `tail`, `nl` or
  `sed -n '<range>p'` (segments joined by `&&` / `;`, an optional leading
  `cd dir`) are recorded with their current content hash. Piped segments,
  redirects, command substitution and anything else the parser can't
  classify record nothing.

  Entries are marked stale for `read_file` dedup, so a later `read_file`
  always returns the full body instead of claiming the shell output is
  already visible. Only matters with `requireReadBeforeEdit` on: it stops
  the gate refusing files the model demonstrably read through `shell`.
  Not rebuilt by durable-replay read-state hydration. Shell calls made
  from codemode scripts count too.

  Default: `true` (set `false` to make only `read_file` count).
- `unknownToolRecovery?` (`false | { maxNudges?: number; maxSuggestions?: number; message?: string | ((ctx: { called: string; suggestions: readonly string[] }) => string); mode?: 'steer' }`): Steer when the model calls a tool name that is not registered. The unknown
  tool result remains an error; this only queues a bounded follow-up nudge
  with closest advertised tool-name suggestions.
- `writeFileExistingPolicy?` (`'allow' | 'block-with-edit-recipe' | { allowPaths?: readonly string[]; denyPaths?: readonly string[]; mode?: 'allow' | 'block-with-edit-recipe' }`): Policy for full-file writes to paths that already exist. The guard is a
  `tool:gate` refusal that points the model toward `read_file` + `edit` /
  `multi_edit`.

Source: [src/types.ts:828](https://github.com/Tahul/zidane/blob/main/src/types.ts#L828)

### `AgentClock`

Interface.

Time + UUID source. Defaults to `Date.now()` and `crypto.randomUUID()`.

Scoped to **journaled metadata** — every callsite that lands in
`SessionTurn.id`, `SessionTurn.createdAt`, `runId`, `turnId`, or hook
payloads consumers may persist. Live-only measurements (TTFT deltas,
`elapsed` counters) keep `Date.now()` directly so they reflect real
wall-clock progress.

`now()` may return a `Promise<number>`: the loop awaits it at every
callsite. `randomUUID()` stays synchronous so a clock can provide
replay-stable identifiers without async metadata plumbing.

Precedence: `AgentRunOptions.clock` > `AgentOptions.clock` >
[`DEFAULT_AGENT_CLOCK`](https://zidane.si/docs/reference/types.md#DEFAULT_AGENT_CLOCK). A run on a replay-mode durable runtime
(`AgentOptions.durable`) takes its time and ids from the runtime instead,
so a replay regenerates byte-identical session metadata.

```ts
interface AgentClock
```

**Properties**

- `now` (`() => number | Promise<number>`): Current wall-clock time in ms (epoch). May be async.
- `randomUUID` (`() => string`): RFC-4122 v4 UUID string. Always sync.

Source: [src/types.ts:44](https://github.com/Tahul/zidane/blob/main/src/types.ts#L44)

### `AgentDecide`

Interface.

`agent.decide` / `ctx.decide`. Never throws by default: failures come back as
`{ ok: false, reason }`. Pass `{ throw: true }` to get a `DecisionError` instead.

```ts
interface AgentDecide
```

**Properties**

- `available` (`boolean`, readonly): Whether a decision provider is configured, has usable credentials, and its
  default model is not known to be unavailable. Cheap and synchronous; does
  not guarantee the endpoint is up.

Source: [src/decisions/types.ts:214](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L214)

### `AgentDecisionsOptions`

Interface.

Agent-level configuration for decisions (`AgentOptions.decisions`).

```ts
interface AgentDecisionsOptions
```

**Properties**

- `model?` (`string`): Default decision model. Default: the provider's `decisions.defaultModel`.
- `provider?` (`Provider`): Provider to send decisions to. Default: the first provider in
  `AgentOptions.provider` that implements `decisions` and can answer now
  (usable credentials), else the first that implements `decisions`.
- `timeoutMs?` (`number`): Default wall-clock cap in ms. Default: 5000.

Source: [src/decisions/types.ts:230](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L230)

### `AgentRunOptions`

Interface.

Options for one `Agent.run` call.

```ts
interface AgentRunOptions
```

**Properties**

- `behavior?` (`AgentBehavior`): Behavior overrides for this run (overrides agent defaults)
- `clock?` (`AgentClock`): Override the time + UUID source for this run only. Per-run wins over
  agent-level (`AgentOptions.clock`); both fall back to
  [`DEFAULT_AGENT_CLOCK`](https://zidane.si/docs/reference/types.md#DEFAULT_AGENT_CLOCK). See [`AgentClock`](https://zidane.si/docs/reference/types.md#AgentClock).
- `contextSections?` (`{ rulesBlock?: string; rulesFiles?: { path: string; source: string }[] }`): Optional metadata describing which named sections the host baked into
  `system` (e.g. the rendered AGENTS.md/CLAUDE.md "Rules" block). Purely
  informational — used by `agent.getContextBreakdown()` to attribute system-
  prompt tokens to a "Rules" bucket the agent can't otherwise isolate from
  the opaque base string. Does not affect what's sent to the model.
- `depth?` (`number`): Zero-based subagent depth. 0 = top-level `agent.run()`, 1 = first-level
  child spawned by a parent agent, and so on. Used by the spawn tool to
  enforce `maxDepth` and to stamp `child:*` forwarded hook payloads.
- `hooks?` (`RunHookMap`): Per-run hook registrations. Each hook is attached before the run starts and
  detached in a finally block so handlers never leak across runs.

  Accepts either a single handler or an array (all handlers register).
- `leaseRenewMs?` (`number`): How often a top-level checkpoint run (`zidane/durable`) renews its
  session's lease, in ms. Default 60 s. Shorter: a run another process
  took over notices sooner, and stops showing output its new owner now
  shows (a channel run, whose reply lives in a shared conversation).
- `metadata?` (`Readonly<Record<string, JsonValue>>`): Host data about this run's prompt (an extension `prompt` interceptor's
  `metadata`). The agent never reads it: `run` interceptors see it on the
  options they receive. Not persisted.
- `model?` (`string | (string | ModelTargetSpec)[]`): Model selection for this run.

  - `string` — a model id. With a single provider the id runs on that
    provider; with multiple providers the id is tried on EVERY provider in order
    (fallback chain).
  - `Array<string | ModelTargetSpec>` — an ordered fallback list. Object
    entries pin a model to one provider by name; bare strings (and object
    entries without `provider`) fan out across all providers.
  - omitted — each provider's `meta.defaultModel`, in provider order.

  The run starts on the first resolved target and advances down the list
  when a stream fails terminally (see the `model:fallback` hook). Once
  advanced, the run stays on that target (run-sticky); the next `run()`
  starts from the primary again.
- `modelOptions?` (`Record<string, boolean>`): Enabled model-specific options for this run, keyed by option id (e.g.
  `{ fast: true }` for Anthropic's fast mode). Options the active model
  doesn't declare are silently ignored by the provider. Overrides
  `AgentBehavior.modelOptions` when both are set.
- `parentCallId?` (`string`): Tool-call id of the `spawn` call that created this run. The built-in
  `spawn` tool populates it from its own `ToolContext.callId` for every
  child it starts — no `persist` / shared session needed, unlike
  `parentRunId`. Hosts that roll their own subagent (calling `agent.run`
  with `depth` set from inside a tool) must pass it themselves; nothing
  derives it, so `depth >= 1` alone does not imply it is set.

  Lands on every `ToolContext` / `ToolHookContext` / `McpToolHookContext`
  of the run, so a host can attribute a subagent's tool call to the spawn
  call that owns it — see `ToolHookContext.parentCallId`. Recorded
  on the run (`SessionRun.parentCallId`), so a replay pairs the child
  with its `spawn` call exactly.
- `parentRunId?` (`string`): Parent run id. Populated automatically by the `spawn` tool when the child
  shares the parent's session; recorded on the resulting `SessionRun` so the
  parent↔child run tree can be reconstructed from a persisted session.
- `pauseSignal?` (`AbortSignal`): Pause signal: when triggered, the run stops at the next turn boundary
  (after the model call in flight and its tool results are saved), never
  mid-turn, and returns with `stats.paused`. A later run on the same
  session continues from those turns. A host uses it to restart (an
  update) without losing work.
- `prompt?` (`string | PromptPart[]`): User prompt. Optional when resuming a session with existing turns.

  Accepts either a plain string (single text part) or an array of `PromptPart`s for
  multimodal inputs (text, images, documents). See [`PromptPart`](https://zidane.si/docs/reference/types.md#PromptPart).
- `resumeRunId?` (`string`): Resume a prior run's conversation: seed this run's turns from the
  session turns tagged with this `runId` (filtered through
  `filterUnresolvedToolUses` so a run killed mid-tool doesn't resume
  with an orphan `tool_use`). Used by the spawn tool to continue a
  stopped/failed subagent — the new run gets its own `runId`, with
  `resumedFromRunId` recorded on the resulting `SessionRun` so the
  chain stays reconstructible. Requires a session and a `prompt`
  (the continuation instruction); throws when the referenced run has
  no persisted turns.
- `runAgent?` (`SessionRunAgent`): The host's agent profile this run runs as, persisted onto the resulting
  `SessionRun.agent` so a transcript can name it per run.
- `runLabel?` (`string`): Host-assigned display label persisted onto the resulting `SessionRun`
  (`SessionRun.label`). The spawn tool passes its reserved `child-N`
  label here so label → run resolution (spawn `resume`) and transcript
  numbering read a stored fact instead of deriving it positionally.
- `signal?` (`AbortSignal`): Abort signal — when triggered, the run aborts the same way `agent.abort()` does
- `system?` (`string`): System prompt for this run. Falls back to `AgentOptions.system`, then to `'You are a helpful assistant.'`.
- `systemFreeze?` (`SystemFreezeOptions`): Keep the conversation's system prompt byte-stable across runs and agent
  rebuilds, so provider prompt caches and replayed thinking stay valid. The
  run composes its fresh prompt as usual (base, skills catalog,
  searchable-tools catalog, MCP instructions), then:

  - with `system` set, sends that string instead and appends the notes
    `notes(fresh, sections)` returns to this run's prompt turn, ahead of
    `AgentRunOptions.systemNotes`;
  - otherwise sends the fresh prompt and hands it to `onCapture`, for the
    host to persist as the conversation's frozen prompt.

  Both callbacks also get the sections the SDK appended
  ([`FrozenPromptSections`](https://zidane.si/docs/reference/index.md#FrozenPromptSections)); the `zidane` root exports helpers
  (`systemSnapshotTurn`, `latestSystemSnapshot`, `catalogChangeNotes`…) to
  store snapshots and announce those sections' changes.
  `system:transform` hooks still run per turn on top. Omitted: every run
  sends its fresh prompt.
- `systemNotes?` (`readonly (SystemNote & { text: string })[]`): Notes for the model, appended to this run's prompt turn after the prompt
  as text blocks marked with `SystemNote`. Use them to announce a
  change (mode, environment, instructions, effort) without editing the
  system prompt. Ignored when the run seeds no prompt turn.
- `thinking?` (`'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive'`, default `'off'`): Thinking effort for this run. See [`ThinkingLevel`](https://zidane.si/docs/reference/types.md#ThinkingLevel).
- `tools?` (`Record<string, ToolDef>`): Tool overrides for this run. Pass {} for no tools. Omit to use agent tools.
- `tracingContext?` (`Readonly<Record<string, string>>`): Opaque trace-context carrier propagated from the parent agent's
  tracer (typically a W3C `{ traceparent, tracestate }` map). The
  child's `agent.run()` re-emits it on the `agent:start` hook so the
  child's tracer can stitch its root span as a continuation of the
  parent's spawn span. Empty / absent on top-level runs.

  Set automatically by the `spawn` tool when a parent's tracer wrote
  into `SpawnHookContext.tracingContext` on `spawn:before`. SDK
  consumers that drive subagents manually can populate this directly.

Source: [src/types.ts:3308](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3308)

### `AgentStats`

Interface.

Token, cost and timing totals returned by `Agent.run`.

```ts
interface AgentStats
```

**Properties**

- `children?` (`ChildRunStats[]`): Stats from child agents spawned during this run, in completion order. Recursive.
- `cost?` (`number`): Cumulative cost in USD — parent loop plus every recursively-spawned
  sub-agent. Sums per-turn `TurnUsage.cost` reported by the provider.
  Absent when neither parent nor any descendant reported a non-zero cost.
- `elapsed` (`number`): Wall-clock duration of the top-level `agent.run()` call, in milliseconds.
  Children run during parent tool calls so this naturally subsumes child
  wall time — sequential children inflate it, parallel children compress
  into the parent's window.
- `output?` (`Record<string, unknown>`): Structured output from schema enforcement (only present when behavior.schema is set)
- `paused?` (`boolean`): The run stopped at a turn boundary on `pauseSignal`, with work left: continue it with another run.
- `suspended?` (`boolean`): `Agent.suspend` stopped the run: on a checkpoint runtime it stays resumable (`Agent.resume`).
- `timeTillFirstTokenMs?` (`number`): Milliseconds from the start of `agent.run()` to the first observable signal from the
  provider (first `stream:text`, `stream:thinking`, or `tool:before` event).

  Absent when the run produced no observable signals (e.g. aborted before any stream event).
- `totalCacheCreation` (`number`): Cumulative cache-creation tokens across the parent agent loop and every
  recursively-spawned sub-agent. Same rationale as
  `AgentStats.totalCacheRead` — separate Anthropic billing rate.
  Always `0` for providers that don't report cache usage.
- `totalCacheRead` (`number`): Cumulative cache-read tokens across the parent agent loop and every
  recursively-spawned sub-agent. Surfaced at the top level (rather than
  only per-`TurnUsage`) because Anthropic prices cache reads at a separate
  line-item rate from regular input — billing-correct cost computation
  needs this number directly. Always `0` for providers that don't report
  cache usage.
- `totalIn` (`number`): Cumulative input tokens across the parent agent loop **and** every
  recursively-spawned sub-agent. Use this for billing / token-ledger
  consumption.
- `totalOut` (`number`): Cumulative output tokens. Same semantics as `AgentStats.totalIn`.
- `turns` (`number`): Number of parent agent-loop turns. Children's turn counts live under
  `children[].stats.turns` and are NOT folded in here — a single "turns"
  number for the whole tree would conflate two different measures
  (parent-loop iterations vs. tree-wide tool-call rounds).

  Tree-wide turn count: `flattenTurns(stats).length`.
- `turnUsage?` (`TurnUsage[]`): Per-turn usage breakdown for the **parent loop only**. Children's per-turn
  usages live under `children[].stats.turnUsage`. Use [`flattenTurns`](https://zidane.si/docs/reference/types.md#flattenTurns)
  to walk the full tree.

Source: [src/types.ts:3585](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3585)

### `ChildRunStats`

Interface.

Stats of one spawned subagent run, passed to `spawn:complete` and listed in `AgentStats.children`.

```ts
interface ChildRunStats
```

**Properties**

- `callId?` (`string`): Tool-call id of the `spawn` call that produced this child — see
  `SpawnHookContext.callId`. Lets a `spawn:complete` listener close
  out the same grouping key it opened on `spawn:before`.
- `depth?` (`number`): Subagent depth when this child ran. 1 = direct child of the top-level
  agent, 2 = grandchild, etc. Useful for telemetry that wants to group
  runs by depth.
- `id` (`string`): Spawn id of the child.
- `output?` (`Record<string, unknown>`): Final structured output when the child was run with `behavior.schema`.
  Mirrors `AgentStats.output` but is surfaced here so the parent can read
  it without peeking at the nested `stats` bag.
- `stats` (`AgentStats`): The child agent's full [`AgentStats`](https://zidane.si/docs/reference/types.md#AgentStats). Cumulative for that child's
  own subtree (child loop + its grandchildren). Do **not** sum
  `ctx.stats.totalIn` across `spawn:complete` events to derive top-level
  totals — `agent.run()`'s return value is the canonical cumulative root.
- `status?` (`'error' | 'completed' | 'aborted' | 'timeout'`): Terminal state of the child run. `'completed'` is the default. Exposed so
  a parent reading `stats.children` can distinguish aborted/timed-out
  children without re-parsing the returned string.
- `task` (`string`): Task the child was given.

Source: [src/types.ts:3656](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3656)

### `DecideHookContext`

Interface.

Present on every hook context except `decision:*` (see [`AgentHookHandlers`](https://zidane.si/docs/reference/types.md#AgentHookHandlers)).

```ts
interface DecideHookContext
```

**Properties**

- `decide` (`AgentDecide`, readonly): Make a decision request from inside a hook. Bound to the active run: its
  abort signal cancels the decision and `decision:*` hooks carry its run id.
  Never throws; see [`AgentDecide`](https://zidane.si/docs/reference/types.md#AgentDecide).

Source: [src/agent-hooks.ts:1781](https://github.com/Tahul/zidane/blob/main/src/agent-hooks.ts#L1781)

### `DecisionEndHookContext`

Interface.

Context passed to the `decision:end` hook.

```ts
interface DecisionEndHookContext extends DecisionStartHookContext
```

**Properties**

- `answers?` (`Readonly<Record<string, DecisionAnswer>>`): Answers keyed by question name. Set only when `ok` is `true`.
- `decisionId` (`string`, from `DecisionStartHookContext`): Unique id of this decision.
- `durationMs` (`number`): Elapsed time from start to finish, in milliseconds.
- `inputKind` (`DecisionInputKind`, from `DecisionStartHookContext`): Kind of input the request carried.
- `message?` (`string`): Failure message. Set only when `ok` is `false`.
- `model?` (`string`, from `DecisionStartHookContext`): Selected model, when known.
- `ok` (`boolean`): Whether the decision produced answers.
- `provider?` (`string`, from `DecisionStartHookContext`): Name of the selected provider, when one was selected.
- `questionNames` (`string[]`, from `DecisionStartHookContext`): Names of the questions in the request.
- `reason?` (`DecisionFailureReason`): Failure reason. Set only when `ok` is `false`.
- `runId?` (`string`, from `DecisionStartHookContext`): Run the decision was issued from (via `ctx.decide`), when known.
- `startedAt` (`number`, from `DecisionStartHookContext`): Start time as epoch milliseconds.
- `usage?` (`DecisionUsage`): Token usage and cost. Set only when `ok` is `true`.

Source: [src/decisions/types.ts:269](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L269)

### `DecisionFailure`

Interface.

A decision that produced no answers. See [`DecisionFailureReason`](https://zidane.si/docs/reference/types.md#DecisionFailureReason).

```ts
interface DecisionFailure
```

**Properties**

- `decisionId` (`string`): Unique id of this decision, shared with the `decision:*` hooks.
- `error?` (`Error`): Underlying error, when the failure came from a thrown error.
- `message` (`string`): Human-readable explanation.
- `model?` (`string`): Model the decision was sent to, when one was selected.
- `ok` (`false`): Always `false` for a failed decision.
- `provider?` (`string`): Name of the provider the decision was sent to, when one was selected.
- `reason` (`DecisionFailureReason`): Category of the failure.

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

### `DecisionRequest`

Interface.

Input to `agent.decide` and `ctx.decide`: the evidence and the questions to answer about it.

```ts
interface DecisionRequest<Q extends DecisionQuestions = DecisionQuestions>
```

**Type parameters**

- `Q` (`extends DecisionQuestions`, default `DecisionQuestions`)

**Properties**

- `input` (`DecisionInput`): Evidence the questions are evaluated against.
- `model?` (`string`): Model override. Falls back to `AgentOptions.decisions.model`, then the provider default.
- `questions` (`Q`): Questions keyed by name. Each answer comes back under the same name.
- `signal?` (`AbortSignal`): Extra cancellation, combined with the active run's signal.
- `timeoutMs?` (`number`): Wall-clock cap in ms. Falls back to `AgentOptions.decisions.timeoutMs`, then 5000.

Source: [src/decisions/types.ts:127](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L127)

### `DecisionStartHookContext`

Interface.

Context passed to the `decision:start` hook.

```ts
interface DecisionStartHookContext
```

**Properties**

- `decisionId` (`string`): Unique id of this decision.
- `inputKind` (`DecisionInputKind`): Kind of input the request carried.
- `model?` (`string`): Selected model, when known.
- `provider?` (`string`): Name of the selected provider, when one was selected.
- `questionNames` (`string[]`): Names of the questions in the request.
- `runId?` (`string`): Run the decision was issued from (via `ctx.decide`), when known.
- `startedAt` (`number`): Start time as epoch milliseconds.

Source: [src/decisions/types.ts:251](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L251)

### `DecisionSuccess`

Interface.

A decision that produced answers.

```ts
interface DecisionSuccess<Q extends DecisionQuestions = DecisionQuestions>
```

**Type parameters**

- `Q` (`extends DecisionQuestions`, default `DecisionQuestions`)

**Properties**

- `answers` (`DecisionAnswers<Q>`): Answers keyed by question name.
- `decisionId` (`string`): Unique id of this decision, shared with the `decision:*` hooks.
- `id?` (`string`): Provider-side request id, when reported.
- `model` (`string`): Model that answered.
- `ok` (`true`): Always `true` for a successful decision.
- `provider` (`string`): Name of the provider that answered.
- `usage` (`DecisionUsage`): Token usage and cost of the call.

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

### `DecisionUsage`

Interface.

Token usage of one decision call.

```ts
interface DecisionUsage
```

**Properties**

- `cost?` (`number`): USD, when the provider reports it or the price is known.
- `input` (`number`): Input tokens.
- `output` (`number`): Output tokens.

Source: [src/decisions/types.ts:141](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L141)

### `McpServerConfig`

Interface.

Configuration of one MCP server the agent connects to and exposes as tools.

```ts
interface McpServerConfig
```

**Properties**

- `args?` (`string[]`): For stdio: command arguments
- `auth?` (`'oauth'`): OAuth 2.1 authentication (sse / streamable-http only).

  - `'oauth'` — enables the SDK's OAuth flow with RFC 9728 protected-resource
    metadata discovery, RFC 8414 / OIDC authorization-server metadata, RFC 7591
    dynamic client registration, PKCE, and refresh-token rotation. Tokens persist
    between runs via the host's credential store.
  - `undefined` (default) — no OAuth. The host may still auto-promote a server
    to OAuth on `UnauthorizedError` IF no static `Authorization` header is set
    (the headers check stops us from second-guessing user-managed bearer tokens).

  Recognized aliases at parse time: Cursor's `authMethod: 'mcpOAuth'` maps to
  `auth: 'oauth'` so `~/.cursor/mcp.json` pastes work unchanged.
- `bootstrapTimeout?` (`number`): Timeout in milliseconds for MCP server bootstrap (connect + tool discovery).

  Zidane connects MCP servers lazily on the first `run()`. Without a
  bootstrap timeout, a slow or hung server can delay the first provider call
  for an arbitrarily long time even when that MCP server is never used.

  Default: `10000`.
- `cachedTools?` (`McpToolSchema[]`): Pre-cached tool schemas to advertise without issuing `tools/list` at
  bootstrap. The connection is still established (the SDK's `connect()`
  is needed for `tools/call`) — only the discovery round-trip is
  skipped. Schemas are trusted as-is; the host owns invalidation
  (typical cache key: `(server identity, server version)`). If the
  server later returns `MethodNotFound` for a cached tool, the host
  should drop the entry from its cache so the next bootstrap re-lists.

  Compatible with every transport, every auth mode, and with
  `McpServerConfig.lazyConnect`. Composes with the existing
  `enabledTools` / `disabledTools` / `toolFilter` filters — those run
  over the cached schemas exactly as they would over `listTools()`
  output.
- `closeTimeout?` (`number`): Timeout in milliseconds for this server's shutdown inside
  `McpConnection.close()` (default: 5000).

  Each server's close is raced against this bound so a hung transport
  close, an MCP server ignoring stdin EOF, or a wedged `mcp:close` hook
  listener cannot block `agent.destroy()` indefinitely. When the bound
  fires on a stdio server, the child process is SIGKILLed so the
  abandoned close cannot leak a subprocess into a long-lived host.
- `command?` (`string`): For stdio: command to run
- `cwd?` (`string`): For stdio: working directory of the spawned child process. Forwarded to
  `StdioClientTransport`'s `cwd` (which is `spawn`'s `cwd`).

  When omitted, the child inherits the parent's cwd (`process.cwd()` at
  spawn time). Set this when the server resolves paths relative to its
  own cwd (e.g. an MCP server reading `db/migrations/...`)
  and the project directory differs from where the agent was launched.

  Pass an absolute path. Relative values are forwarded as-is to the SDK,
  which resolves them against the parent process's cwd.
- `disabledTools?` (`string[]`): Deny-list of tool names. Tools matching are dropped before registration.
  Same matching semantics as `McpServerConfig.enabledTools`.
- `disclosure?` (`'eager' | 'lazy'`): Per-server override for `AgentBehavior.toolDisclosure`. When set,
  this server's tools follow this disclosure mode regardless of the
  agent-wide default. Useful when one big MCP server (200+ tools) should
  stay lazy while smaller servers stay eager.

  Default: inherits from `behavior.toolDisclosure`.
- `enabledTools?` (`string[]`): Allow-list of tool names to expose. Names match the upstream tool name
  (NOT the namespaced `mcp_{server}_{tool}` form). Tools not in the list are
  dropped before registration — the model never sees them in its catalog and
  the wire cost of advertising them is avoided.

  Mutually exclusive with `McpServerConfig.disabledTools` — passing both
  throws at bootstrap time.

  Composes with `McpServerConfig.toolFilter`: allow-list applies first,
  then the predicate. Composes with the `mcp:tools:filter` hook: config-side
  filters apply first, then the hook can further narrow the list.
- `env?` (`Record<string, string>`): For stdio: environment variables to pass to the server process.

  Merged on top of the MCP SDK's default inherited environment — a safety
  whitelist (`PATH`, `HOME`, `LANG`, `SHELL`, `USER` on POSIX; `APPDATA`,
  `PATH`, ... on Win32). Setting this to `{}` does not strip `PATH` from
  the child process. Set `McpServerConfig.strictEnv` to `true` to
  pass `env` verbatim with no inherited defaults.
- `headers?` (`Record<string, string>`): Optional headers for HTTP transports
- `lazyConnect?` (`boolean`): Defer the `client.connect(transport)` call until the first
  `tools/call` reaches this server. Bootstrap registers the server's
  tools using `McpServerConfig.cachedTools` without touching
  the network, taking MCP setup off the critical path of
  `agent.run()`. The first invocation pays the connect cost
  (\~200-500ms typically); every subsequent call reuses the live
  client.

  Requires `McpServerConfig.cachedTools` — without schemas in
  hand there is nothing to advertise to the model, so deferring the
  connection has no purpose. Bootstrap rejects the config otherwise.

  **Incompatible with `auth: 'oauth'`**: the OAuth handshake (token
  refresh / RFC 9728 metadata discovery) can fail in ways that
  fire `mcp:auth:required` at bootstrap so the host can surface a
  login affordance *before* the model commits to calling a tool.
  Deferring that to first call means an auth failure surfaces mid-run
  as a tool-result error, which the model can't recover from without
  a fresh prompt. Bootstrap rejects the combination so the error is
  loud and proximate to the misconfiguration. Use OAuth servers
  without `lazyConnect` (with `cachedTools` alone, if you want to
  skip the `tools/list` round-trip).

  On connect failure (network error, transport refused), the cached
  promise is dropped so the next `tools/call` retries. The model
  sees the failure as a normal tool error. Subsequent calls remain
  eligible to succeed once the upstream is reachable again.
- `name` (`string`): Display name (used for tool namespacing)
- `oauth?` (`McpOAuthClientConfig`): Pre-registered OAuth client, for auth servers without RFC 7591 dynamic
  client registration (Slack's `mcp.slack.com`, most enterprise IdPs).
  Setting it implies `auth: 'oauth'`; the SDK then skips registration and
  uses this client for the authorization-code + refresh flows.

  - `clientId` — the app's client ID. `${VAR}` placeholders expand.
  - `clientSecret` — optional; prefer leaving it out of the file: hosts
    keep a secret entered in their UI in the MCP credential store instead.
    `${VAR}` placeholders expand, so `"${SLACK_CLIENT_SECRET}"` works too.
  - `callbackPort` — pins the loopback redirect to
    `http://127.0.0.1:<port>/callback`, the URL to register with the
    provider (they reject redirects that don't match). Default: random.
  - `scope` — space-separated scopes to request.
- `pinHeaderOrigin?` (`boolean`): Pin `McpServerConfig.headers` to the configured URL's origin
  (sse / streamable-http only).

  When set, redirects are followed manually and the configured headers
  are STRIPPED the moment a hop leaves the original origin, so a
  cooperating-then-redirecting server cannot harvest a plugin's bearer
  token. Set by the Agent Plugins loader, which must satisfy the spec's
  "never forward configured headers to a different origin" rule; opt in
  manually for any other server whose headers carry a secret.
- `preExpanded?` (`boolean`): Suppress `${VAR}` expansion at the transport boundary.

  Set by the Agent Plugins loader (`plugins/mcp.ts`), whose placeholder
  rules are DELIBERATELY narrower than zidane's: the spec expands only
  `${PLUGIN_ROOT}` / `${PLUGIN_DATA}`, and only inside `args`, `env`
  values and `cwd` — never in `command`, `url` or `headers`. Those two
  placeholders are already substituted by the time the config gets here,
  so a second pass would (a) expand the wrong fields and (b) let an
  ambient `PLUGIN_ROOT` env var reach fields the spec says must keep the
  literal text.

  Hand-written `mcps.json` entries leave this unset and keep the full
  `${VAR}` / `${VAR:-default}` / `${env:VAR}` behavior.
- `scope?` (`'context' | 'host'`): For stdio: WHERE the server process runs.

  - `'host'` (default) — spawned as a child of the agent process, on the
    host filesystem, with the host's env and network.
  - `'context'` — spawned inside the agent's `ExecutionContext`, via
    `ExecutionContext.execStream`.

  Why this is opt-in rather than automatic: with a sandboxed context
  (Daytona / E2B / Docker) a host-spawned server and the agent's own
  tools see DIFFERENT filesystems, so a path the model discovered via
  `list_files` doesn't exist for the server, and an MCP write lands
  where `read_file` can't see it. `'context'` closes that gap — but it
  is wrong for plenty of servers: a GitHub / Linear / Sentry MCP wants
  the host's credentials and only talks to the network, and moving it
  into the sandbox would break it for no benefit. Only servers that
  touch the workspace (filesystem, git, build tooling) want `'context'`.

  Three consequences worth knowing before flipping this:

  - The context's image must contain the server's runtime (`node`,
    `uvx`, …). A missing binary surfaces as the server exiting
    immediately at bootstrap.
  - Environment: the server inherits the CONTEXT's environment with
    `env` merged on top — NOT the host path's `getDefaultEnvironment()`
    whitelist. On the default process context the context environment
    IS the host's full `process.env`, so a context-scoped server sees
    every host variable unless you set `strictEnv: true`, which limits
    it to exactly `env` (via `env -i`, same semantics as on the host).
  - This is filesystem COHERENCE, not isolation. Remote-transport MCP
    servers can never be moved into the context, so the agent's reach
    is still not bounded by the sandbox.

  Ignored for `sse` / `streamable-http` (nothing local to relocate).
  Requires a context implementing `execStream`; anything else is a
  bootstrap error rather than a silent fallback to `'host'`.
- `shared?` (`boolean`): For a host-spawned stdio server run through an MCP server pool
  (`ConnectMcpServersOptions.pool`): whether every session whose config
  matches shares one process (`true`, the default), or each session gets
  its own (`false`: the session and its subagents still share one). Set it
  `false` for a server that keeps per-user state in its process (a browser
  it drives, an open document). Ignored without a pool, for remote servers
  and for `scope: 'context'`, which are never shared.
- `strictEnv?` (`boolean`): When true, `McpServerConfig.env` is passed verbatim to the spawned
  process — the MCP SDK's default inherited environment (`PATH`, `HOME`, ...)
  is NOT merged in. Most consumers should leave this off; the default merge
  prevents `spawn ENOENT` when a stdio server declares an `env` without
  restating `PATH`.
- `toolFilter?` (`(tool: { description?: string | null; inputSchema?: unknown; name: string }) => boolean`): Custom predicate run on each upstream tool. Return `true` to keep, `false`
  to drop. Receives the raw `listTools()` payload — useful for filtering by
  description, schema shape, or other metadata that an allow/deny list can't
  express.

  Runs after the allow/deny filter but before the `mcp:tools:filter` hook.
- `toolTimeout?` (`number`): Timeout in milliseconds for MCP tool calls (default: 30000)
- `toolTimeouts?` (`Record<string, number>`): Per-tool timeout overrides in milliseconds, keyed by the server-side
  tool name (NOT the namespaced `mcp_<server>_<tool>` agent name). Takes
  precedence over `toolTimeout` for matching tools.

  Use case: one server exposing both fast metadata calls and slow
  operations (`list_tables` vs `apply_migration`) — a single per-server
  timeout either kills legitimate long calls or lets fast ones hang.
- `transport` (`'sse' | 'stdio' | 'streamable-http'`): Transport type
- `url?` (`string`): For sse/streamable-http: server URL

Source: [src/types.ts:93](https://github.com/Tahul/zidane/blob/main/src/types.ts#L93)

### `McpToolHookContext`

Interface.

Base context for MCP tool hooks.

`tool` is the native tool name on the MCP server. `server` is the configured server
name. The canonical zidane-namespaced identity is `mcp_{server}_{tool}`.

`displayName` equals the canonical namespaced name unless the agent has aliased
this MCP tool via `AgentOptions.toolAliases`; in which case `displayName` is the
alias that the LLM sees.

```ts
interface McpToolHookContext
```

**Properties**

- `callId` (`string`): Id of the tool call.
- `depth?` (`number`): Subagent depth — see `ToolHookContext.depth`.
- `displayName` (`string`): Aliased wire name for this MCP tool, or the canonical `mcp_{server}_{tool}` name.
- `input` (`Record<string, unknown>`): Input the tool was called with.
- `parentCallId?` (`string`): Spawning tool call's id when this tool call's agent is a subagent — see `ToolHookContext.parentCallId`.
- `parentRunId?` (`string`): Parent run id when this tool call's agent is a subagent — see `ToolHookContext.parentRunId`.
- `runId?` (`string`): Owning run id — same semantics as `ToolHookContext.runId`.
- `server` (`string`): Configured name of the MCP server.
- `tool` (`string`): Tool name on the MCP server, without the `mcp_<server>_` prefix.
- `turnId` (`string`): Id of the turn that dispatched the call.

Source: [src/types.ts:3824](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3824)

### `ModelUsage`

Interface.

Per-model usage rollup produced by [`statsByModel`](https://zidane.si/docs/reference/types.md#statsByModel).

`turns` counts the number of `TurnUsage` entries attributed to the model
across the whole tree (parent loop + every recursively-spawned child).
Cache and cost numbers are summed from the same set of turns.

```ts
interface ModelUsage
```

**Properties**

- `cacheCreation` (`number`): Tokens written to the prompt cache.
- `cacheRead` (`number`): Tokens read from the prompt cache.
- `cost` (`number`): Total cost in USD. Turns without a known price contribute 0.
- `input` (`number`): Input tokens, excluding cache reads and cache writes.
- `output` (`number`): Output tokens.
- `turns` (`number`): Number of turns attributed to the model.

Source: [src/stats.ts:33](https://github.com/Tahul/zidane/blob/main/src/stats.ts#L33)

### `MutationValidationBatch`

Interface.

Successful mutations from one tool-results round, passed to `validate`.

```ts
interface MutationValidationBatch
```

**Properties**

- `mutations` (`readonly MutationValidationMutation[]`): Successful mutations since the last validation, in call order.
- `turnId` (`string`): Id of the turn that produced the mutations.

Source: [src/mutation-validation.ts:28](https://github.com/Tahul/zidane/blob/main/src/mutation-validation.ts#L28)

### `MutationValidationFailure`

Interface.

Details of a failed validation, passed to `onFailure`.

```ts
interface MutationValidationFailure
```

**Properties**

- `batch` (`MutationValidationBatch`): Batch that failed validation.
- `failureCount` (`number`): Number of failures so far in the current run, including this one.
- `message` (`string`): Message built for the failure, from `buildFailureMessage` or the default.
- `result` (`{ errors?: readonly string[]; message?: string; ok: false }`): Failing result returned by `validate`.

Source: [src/mutation-validation.ts:41](https://github.com/Tahul/zidane/blob/main/src/mutation-validation.ts#L41)

### `MutationValidationHooksOptions`

Interface.

Options for [`createMutationValidationHooks`](https://zidane.si/docs/reference/index.md#createMutationValidationHooks).

```ts
interface MutationValidationHooksOptions
```

**Properties**

- `buildFailureMessage?` (`(failure: Omit<MutationValidationFailure, 'message'>) => string`): Builds the message reported to the model for a failure. Defaults to a summary of the changed files and the validation details.
- `maxFailuresPerRun?` (`number`, default `2`): Maximum number of failures reported per run. Later failures are ignored.
- `onError?` (`(error: unknown, batch: MutationValidationBatch) => MaybePromise<void>`): Called when `validate` throws. The error is not reported to the model.
- `onFailure?` (`(failure: MutationValidationFailure) => MaybePromise<void>`): Called when validation fails and the run has not crossed
  `maxFailuresPerRun`. Hosts typically call `agent.followUp(message)` here.
- `validate` (`(batch: MutationValidationBatch) => MaybePromise<void | MutationValidationResult>`): Checks a batch of successful mutations. Return `{ ok: false }` to report a failure, or nothing to pass.

Source: [src/mutation-validation.ts:53](https://github.com/Tahul/zidane/blob/main/src/mutation-validation.ts#L53)

### `MutationValidationMutation`

Interface.

One successful file mutation (`created`, `updated` or `edited`) made by a tool call.

```ts
interface MutationValidationMutation
```

**Properties**

- `callId` (`string`): Id of the tool call.
- `displayName` (`string`): Display name of the tool.
- `input` (`Record<string, unknown>`): Input the tool was called with.
- `outcome` (`SuccessfulMutationOutcome`): Outcome the tool reported for the call.
- `result` (`string | ToolResultContent[]`): Result the tool returned for the call.
- `tool` (`string`): Name of the tool that made the change.
- `turnId` (`string`): Id of the turn that made the call.

Source: [src/mutation-validation.ts:10](https://github.com/Tahul/zidane/blob/main/src/mutation-validation.ts#L10)

### `OAuthRefreshHookContext`

Interface.

Context for OAuth refresh hooks

```ts
interface OAuthRefreshHookContext
```

**Properties**

- `credentials` (`Record<string, unknown> & { access: string; expires: number; refresh: string }`): Credentials after the refresh.
- `previousCredentials` (`Record<string, unknown> & { access: string; expires: number; refresh: string }`): Credentials before the refresh.
- `provider` (`string`): Name of the provider whose credentials were refreshed.
- `providerId` (`string`): OAuth provider id used to look up the refresh flow.
- `source` (`'params' | 'file'`): Where the credentials came from: provider params or the shared credentials file.

Source: [src/types.ts:4146](https://github.com/Tahul/zidane/blob/main/src/types.ts#L4146)

### `PromptAudioPart`

Interface.

Audio attachment in a multimodal prompt.

```ts
interface PromptAudioPart
```

**Properties**

- `data` (`string`): Base64-encoded payload (no data-URI prefix)
- `mediaType` (`string`): IANA media type (e.g. `audio/wav`, `audio/mpeg`)
- `name?` (`string`): Optional display name
- `type` (`'audio'`): Block discriminator, always `'audio'`.

Source: [src/types.ts:2414](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2414)

### `PromptDocumentPart`

Interface.

Document attachment in a multimodal prompt.

```ts
interface PromptDocumentPart
```

**Properties**

- `data` (`string`): Either a base64-encoded payload (`encoding: 'base64'`) or raw text (`encoding: 'text'`)
- `encoding` (`'text' | 'base64'`): How `data` is encoded: base64 bytes or raw text.
- `mediaType` (`string`): IANA media type (e.g. `application/pdf`, `text/plain`)
- `name?` (`string`): Optional display name used in attachment headers
- `type` (`'document'`): Block discriminator, always `'document'`.

Source: [src/types.ts:2438](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2438)

### `PromptImagePart`

Interface.

Image attachment in a multimodal prompt.

```ts
interface PromptImagePart
```

**Properties**

- `data` (`string`): Base64-encoded payload
- `mediaType` (`string`): IANA media type (e.g. `image/png`, `image/jpeg`)
- `name?` (`string`): Optional display name
- `type` (`'image'`): Block discriminator, always `'image'`.

Source: [src/types.ts:2402](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2402)

### `PromptTextPart`

Interface.

Text part of a multimodal prompt.

```ts
interface PromptTextPart
```

**Properties**

- `text` (`string`): Text of the prompt part.
- `type` (`'text'`): Block discriminator, always `'text'`.

Source: [src/types.ts:2394](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2394)

### `PromptVideoPart`

Interface.

Video attachment in a multimodal prompt.

```ts
interface PromptVideoPart
```

**Properties**

- `data` (`string`): Base64-encoded payload (no data-URI prefix)
- `mediaType` (`string`): IANA media type (e.g. `video/mp4`, `video/webm`)
- `name?` (`string`): Optional display name
- `type` (`'video'`): Block discriminator, always `'video'`.

Source: [src/types.ts:2426](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2426)

### `QueuedUserMessage`

Interface.

A user message that waits in a run's steering or follow-up queue
(`Agent.steer`, `Agent.followUp`). The loop adds it as a user turn.

```ts
interface QueuedUserMessage
```

**Properties**

- `id?` (`string`): The id of the caller. `steer:inject` gives it back. The agent's own messages have none.
- `prompt` (`string | PromptPart[]`): The message: text, or parts for text with images and documents.
- `systemNotes?` (`readonly (SystemNote & { text: string })[]`): Notes for the model after the message, as `run({ systemNotes })` adds them.

Source: [src/types.ts:2366](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2366)

### `RepeatGuardConfig`

Interface.

Configuration for the consecutive-identical-call repeat guard. See
`AgentBehavior.repeatGuard` for the full contract.

```ts
interface RepeatGuardConfig
```

**Properties**

- `abortThreshold?` (`number`): Consecutive identical calls at which the guard aborts the whole run via
  the agent's `AbortController` (the loop then fires `agent:abort`). Must
  be `>` the block threshold to be reachable; clamped up if not. Set to a
  non-positive value or `Infinity` to disable the abort escalation
  (block-only). Default: `8` (`read_file` uses `12` unless this is
  explicitly configured).
- `blockReason?` (`string | ((name: string, count: number) => string)`): Legacy alias for `steeringMessage`. Preserved for callers already
  customizing the old block-only message.
- `blockThreshold?` (`number`): Consecutive identical calls at which the guard refuses the call via
  `tool:gate` `block`. Clamped to `>= 2`. Default: `4` (`read_file` uses
  `6` unless this is explicitly configured).
- `countBlockedCalls?` (`boolean`): Count calls a PRIOR `tool:gate` handler already blocked (tool-budget
  cap, skill allowed-tools refusal) toward the streak.

  Why you'd want it on: a model hammering a budget-blocked tool with the
  identical payload is the canonical stuck loop, but those attempts never
  reach this guard's counter when this is off — the abort escalation is
  unreachable and the run burns turns until an external cap
  (`maxTurns`). With it on, blocked attempts keep marching the streak
  toward `abortThreshold`; the guard never overwrites the prior gate's
  refusal reason and only acts at the abort escalation.

  Calls answered by a prior gate's `result` substitute (dedup replay)
  are never counted regardless — the model received a real payload.

  Default: `false` (blocked calls don't count).
- `globalCeiling?` (`number`): Universal repeat ceiling — the last-resort chain breaker. Independent of
  `tools`, it counts exact back-to-back `(tool, args)` repeats across
  EVERY tool and aborts the run when one recurs this many times in a row.
  Catches untracked tool chains and tools stuck behind a sticky block (it
  counts blocked attempts, unlike `countBlockedCalls`; never a
  `result` substitute).

  Set high enough that honest retries don't trip it. The main false-positive
  risk is a tool that's legitimately polled with identical args many times
  (a status/wait tool) — exclude those via `globalCeilingExclude`.

  A non-positive or non-finite value disables it. Default: `12`.
- `globalCeilingExclude?` (`readonly RepeatGuardToolMatcher[]`): Tools exempt from the `globalCeiling` (e.g. status/poll/wait tools
  legitimately called with identical args many times). Each entry is a
  [`RepeatGuardToolMatcher`](https://zidane.si/docs/reference/types.md#RepeatGuardToolMatcher). `undefined` (default) exempts nothing; an
  empty array also exempts nothing.
- `globalCeilingWindow?` (`number`): Deprecated no-op kept for config compatibility. The `globalCeiling`
  uses strict chain counting, not a sliding window.
- `mode?` (`'both' | 'window' | 'chain'`): Repeat detector strategy.

  - `'chain'` — strict back-to-back chain counting. Any intervening tool
    call breaks the chain.
  - `'window'` — sliding-window counting. Interleaved calls can accumulate.
  - `'both'` — run both counters and trip when either reaches the threshold.

  Default: `'chain'`. For compatibility, setting `windowSize` without
  `mode` keeps the old `'window'` behavior.
- `normalize?` (`(name: string, input: Record<string, unknown>) => string | undefined`): Collapse a call's input to a streak key. Identical keys for the same
  tool extend the streak. Return `undefined` to opt the call out of
  streak tracking. When unset, the built-in normalizer is used (shell
  command canonicalization for `shell`-shaped tools, JSON for everything
  else).
- `steeringMessage?` (`string | ((name: string, count: number) => string)`): Model-visible steering text at the block threshold. Used as the
  `Blocked: <message>` reason for `thresholdAction: 'block'`, or appended
  to the successful tool result for `thresholdAction: 'steer'`. String used
  verbatim; callback receives the tool name and observed repeat count.
  Default: a generic pivot suggestion.
- `thresholdAction?` (`'steer' | 'block'`): What to do at `blockThreshold`.

  - `'block'` (default) refuses the call via `tool:gate`, returning
    `Blocked: <steering message>`.
  - `'steer'` lets the tool execute, then appends the steering message to
    the tool result. `abortThreshold` still blocks and aborts the run.
- `tools?` (`readonly RepeatGuardToolMatcher[]`): Which tools to track. Each entry is a [`RepeatGuardToolMatcher`](https://zidane.si/docs/reference/types.md#RepeatGuardToolMatcher).
  When unset, the default tracks the built-in `shell` and `read_file`
  tools. An empty array disables the guard.
- `windowSize?` (`number`): Sliding-window size for `mode: 'window'` / `mode: 'both'`.

  When set (`>= 2`), a call's count is the number of occurrences of its
  normalized key among that tool's last `windowSize` tracked calls
  (including this one). A *different* payload of the same tool no longer
  resets the count. This is opt-in non-chain behavior for consumers that
  explicitly want to catch interleaved repeats.

  The effective window is clamped up to the abort threshold (or the
  block threshold when abort is disabled) so the thresholds stay
  reachable.

  Default: `undefined` for `mode: 'chain'`; a conservative built-in default
  is used when `mode` requests window counting without an explicit size.

Source: [src/types.ts:634](https://github.com/Tahul/zidane/blob/main/src/types.ts#L634)

### `ResolveContentRefBlock`

Interface.

Block passed to a [`ResolveContentRef`](https://zidane.si/docs/reference/types.md#ResolveContentRef) callback.

```ts
interface ResolveContentRefBlock
```

**Properties**

- `encoding?` (`'text' | 'base64'`): How `data` is encoded, for document blocks.
- `mediaType` (`string`): IANA media type of the referenced content.
- `name?` (`string`): Optional display name.
- `ref` (`string`): Opaque host-owned content reference to resolve.
- `type` (`'image' | 'document'`): Kind of content the reference points to.

Source: [src/types.ts:2722](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2722)

### `RetryConfig`

Interface.

Configuration for the loop's retry-with-backoff policy around
`provider.stream()`. See `AgentBehavior.retry`.

```ts
interface RetryConfig
```

**Properties**

- `initialDelayMs?` (`number`): Initial backoff delay in milliseconds. Subsequent delays double
  (exponential) before jitter is applied and the cap enforced.

  Default: `1000`.
- `maxAttempts?` (`number`): Maximum number of attempts including the initial call. `1` disables retry.
  Values below `1`, non-finite, or non-integer fall back to the default.

  Default: `3`.
- `maxDelayMs?` (`number`): Upper bound on any single backoff delay in milliseconds. Caps both the
  exponential schedule and any `retry-after` header the server returns.

  Default: `30_000`.

Source: [src/types.ts:369](https://github.com/Tahul/zidane/blob/main/src/types.ts#L369)

### `SessionHookContext`

Interface.

Base context for session hooks

```ts
interface SessionHookContext
```

**Properties**

- `sessionId` (`string`): Id of the session.

Source: [src/types.ts:3848](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3848)

### `SessionMessage`

Interface.

One message of a session conversation, as sent to the provider.

```ts
interface SessionMessage
```

**Properties**

- `content` (`SessionContentBlock[]`): Content blocks of the message, in order.
- `role` (`'user' | 'assistant'`): Author of the message.

Source: [src/types.ts:3157](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3157)

### `SessionTurn`

Interface.

One persisted turn of a session: a user prompt, an assistant response, or a system or command entry.

```ts
interface SessionTurn
```

**Properties**

- `aborted?` (`true`): On a `role: 'assistant'` turn: model output a process death cut off,
  rebuilt when its durable run resumed. Shown in the transcript, left out
  of the model context; the request is sent again.
- `content` (`SessionContentBlock[]`): Content blocks of the turn, in order.
- `createdAt` (`number`): Unix timestamp (Date.now()) when the turn was created
- `display?` (`string`): Display-only replacement for transcript rendering — the full `content`
  still reaches the model unchanged. Set by command context notes
  (`ctx.appendContext`) so a replayed session renders the same one-line
  marker the live path showed (with the full note as a collapsed detail)
  instead of duplicating bulky note bodies as a wall of user text.
  Mirrors `SessionRun.origin.display`, at turn granularity, for turns
  that belong to no run.
- `id` (`string`): Unique turn id. Provider-loop turns are minted from AgentClock.randomUUID().
- `injected?` (`InjectedTurnReason`): Set on a `role: 'user'` turn the agent loop injected rather than the user
  typed (a refusal retry, an output-limit or pause "Please continue.", a
  tool-output-budget warning). The content still reaches the model; hosts
  use the marker to keep the turn out of everything that treats user turns
  as prompts (transcript bubbles, prompt matching, usage counts).
- `role` (`'system' | 'user' | 'assistant' | 'command'`): Conversational role, plus `'command'` — a display-only turn emitted by a
  `/` command's local output. `'command'` turns are persisted + rendered
  but excluded from every model-message builder (they never reach the LLM),
  mirroring how `'system'` turns are filtered from the wire.
- `runId?` (`string`): Run that produced this turn (e.g. 'run_1')
- `systemSnapshot?` (`SystemPromptSnapshot`): On a `role: 'system'` turn: the conversation's frozen system prompt, which
  `content` holds as one text block. The latest snapshot for a prompt key
  wins. Recorded by hosts that pass `AgentRunOptions.systemFreeze`.
- `usage?` (`TurnUsage`): Token usage — only present on assistant turns

Source: [src/types.ts:3165](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3165)

### `SpawnHookContext`

Interface.

Base context for spawn hooks

```ts
interface SpawnHookContext
```

**Properties**

- `callId?` (`string`): Tool-call id of the `spawn` call this event belongs to — the same value
  the child's own tool events carry as `ToolHookContext.parentCallId`.
  Gives hosts the `childId` -> spawn-call mapping at spawn time, which is
  the only place to learn it for a child that never dispatches a tool (and
  the only place at all under parallel spawns, where `tool:before` for two
  spawn calls and their `spawn:before` events interleave).

  Absent for hand-fired payloads and for spawn-style tools invoked with a
  `ToolContext` that carries no `callId`. The child's run records it as
  `SessionRun.parentCallId`.
- `depth?` (`number`): Subagent depth for the spawn. 1 = direct child of the top-level agent.
  Present on spawn:before/complete/error, and on their nested
  `child:spawn:*` re-emissions (where it is ≥ 2).
- `id` (`string`): Spawn id of the child.
- `model?` (`string`): Model id the child runs on — the resolved spawn choice (per-call
  `model` input > subagent-type default > tool option > inherited
  parent model > child provider default). Set on `spawn:before` /
  `spawn:error`; hosts surface it next to the spawn-start marker so
  cost-tiered delegation is visible. Optional for backward
  compatibility with hand-fired hook payloads.
- `provider?` (`string`): `Provider.name` serving the child — differs from the parent's when the spawn routed to another provider.
- `resumesChildId?` (`string`): Child label of the prior run this spawn continues (e.g. `"child-2"`),
  set when the spawn was invoked with `resume`. Hosts render it next to
  the spawn-start marker (“child-4 (resumes child-2)”). Absent on
  fresh spawns.
- `task` (`string`): Task the child was given.
- `tracingContext?` (`Record<string, string>`): Mutable trace-context carrier for parent → child span linkage. Empty
  object by default; a parent tracer mutates it on `spawn:before` (e.g.
  `ctx.tracingContext.traceparent = '00-…-…-01'`) and the `spawn` tool
  forwards the populated object to the child via
  `AgentRunOptions.tracingContext`. The child's tracer re-emits it on
  `agent:start` so it can be used as parent context when opening the
  child's root span.

  Opaque to the harness — keys / values are tracer-defined. Standard
  choice is W3C Trace Context (`traceparent` + optional `tracestate`),
  but Datadog / Sentry / B3 carriers work too.

Source: [src/types.ts:3866](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3866)

### `StagedToolState`

Interface.

Staging handle behind a [`ToolStateView`](https://zidane.si/docs/reference/types.md#ToolStateView): the seam a durable run
uses to move a call's mutations through its step record. Not part of
the tool-facing surface; resolve it with [`stagedToolState`](https://zidane.si/docs/reference/index.md#stagedToolState).

```ts
interface StagedToolState
```

**Properties**

- `clear` (`() => void`): Drop staged ops — for paths whose mutations were never journaled.
- `pending` (`() => ToolStateOp[]`): Ops staged so far, in mutation order. Non-destructive.
- `restore` (`(ops: readonly ToolStateOp[]) => void`): Replace the staged ops (replay path: re-seed from the journaled value).

Source: [src/tool-state.ts:137](https://github.com/Tahul/zidane/blob/main/src/tool-state.ts#L137)

### `StreamHookContext`

Interface.

Context for stream hooks

```ts
interface StreamHookContext
```

**Properties**

- `turnId` (`string`): Id of the turn that is streaming.

Source: [src/types.ts:3959](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3959)

### `ToolHookContext`

Interface.

Base context for tool execution hooks: the tool call a `tool:*` hook fires
for, with ids, names, input and the run it belongs to.

`name` is the canonical tool identity — the spec name registered on the agent (or the
`mcp_{server}_{tool}` name for MCP tools). Hooks should policy-match against `name`.

`displayName` is the outward-facing name — the alias surfaced to the LLM when
`AgentOptions.toolAliases` maps the canonical name; otherwise equal to `name`.
UI/telemetry adapters should emit `displayName`.

Canonical vs. alias matters on session resume: `session.turns` persists canonical
names only, so renaming an alias cannot desync history.

```ts
interface ToolHookContext
```

**Properties**

- `callId` (`string`): Id of the tool call.
- `depth?` (`number`): Subagent depth for this tool call. 0 = top-level, 1 = first-level
  child, etc. Mirrors `ToolContext.depth` so hook consumers don't
  have to cross-reference the tool context. Omitted on top-level
  runs (treated as 0).
- `displayName` (`string`): Aliased (wire) name — equal to `name` when no alias is defined.
- `input` (`Record<string, unknown>`): Input the tool was called with.
- `name` (`string`): Canonical tool name (spec name). Stable across alias-map changes.
- `parentCallId?` (`string`): Tool-call id of the `spawn` call that created the run this tool call
  belongs to. Absent on top-level runs.

  Set for every child the built-in `spawn` tool starts — it comes from the
  spawn tool's own call rather than from session-backed run bookkeeping,
  so unlike `parentRunId` it survives `persist: false` spawns. It is NOT
  an invariant of `depth >= 1`: a host that spawns by calling `agent.run`
  with `depth` itself sees a subagent with no `parentCallId` unless it
  passes `AgentRunOptions.parentCallId` too. Treat it as present-or-absent,
  not as derivable from depth.

  Hosts mirroring subagent activity into a client protocol use it as the
  "parent tool use" grouping key: absent at depth 0, and at depth >= 1 a
  real tool-call id rather than a synthetic label. Where it *resolves*
  depends on depth — at depth 1 it names a call the parent's own `tool:*`
  events already carried, while at depth >= 2 it names a call in the
  child's turn, which a top-level listener only ever saw as
  `child:tool:*`. So it always groups correctly, but a lookup against the
  top-level tool stream alone only hits for depth 1. This mirrors the
  ancestry semantics of `childId` / `depth` on `child:*` events.
- `parentRunId?` (`string`): Parent run id when this tool call's agent is a subagent — i.e. the
  `SessionRun.parentRunId` of the run that owns the call. Absent on
  top-level runs. Useful for observability stitching: a UI grouping
  subagent-scoped state (e.g. todowrite, edit batches) by parent
  run can read this directly off `tool:before` / `tool:after`
  without resolving the run row.
- `parentToolCallId?` (`string`): Set when another tool issued this call through `ToolContext.callTool`
  (the `codemode` tool): the id of that calling tool call. Nested calls
  never appear in `session.turns` and the model never sees their result,
  so consumers that render model-visible tool calls or coach the model on
  them should skip these.
- `runId?` (`string`): The run this tool call belongs to (the `SessionRun.id`). Lets a single
  `tool:*` listener disambiguate calls across parallel runs / subagent
  trees without subscribing to the `child:tool:*` bubble events.
- `runsNestedAgent?` (`true`): Set when the called tool runs a subagent (`DurableToolDef.runsNestedAgent`, e.g. `spawn`).
- `scriptLine?` (`number`): On a nested call: the line of the calling script that made it
  (`NestedToolCallOptions.line`). Absent when the caller tracks no lines.
- `turnId` (`string`): Id of the turn that dispatched the call.

Source: [src/types.ts:3741](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3741)

### `ToolResultAudioContent`

Interface.

Audio tool-result block with inline base64 `data`.

```ts
interface ToolResultAudioContent
```

**Properties**

- `data` (`string`): Base64-encoded payload
- `mediaType` (`string`): IANA media type (e.g. `audio/wav`, `audio/mpeg`)
- `type` (`'audio'`): Block discriminator, always `'audio'`.

Source: [src/types.ts:2670](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2670)

### `ToolResultDocumentContent`

Interface.

Document tool-result block with inline `data`.

```ts
interface ToolResultDocumentContent
```

**Properties**

- `data` (`string`): Base64-encoded payload or raw text, according to `encoding`.
- `encoding` (`'text' | 'base64'`): How `data` is encoded: base64 bytes or raw text.
- `mediaType` (`string`): IANA media type (e.g. `application/pdf`, `text/plain`)
- `name?` (`string`): Optional display name/title for providers that surface document metadata.
- `ref?` (`undefined`): Absent on inline blocks.
- `type` (`'document'`): Block discriminator, always `'document'`.

Source: [src/types.ts:2690](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2690)

### `ToolResultDocumentRefContent`

Interface.

Document tool-result block that carries a content reference instead of inline data.

```ts
interface ToolResultDocumentRefContent
```

**Properties**

- `data?` (`undefined`): Absent on ref blocks. The payload is resolved from `ref`.
- `encoding` (`'text' | 'base64'`): How the resolved payload is encoded.
- `mediaType` (`string`): IANA media type (e.g. `application/pdf`, `text/plain`)
- `name?` (`string`): Optional display name/title for providers that surface document metadata.
- `ref` (`string`): Opaque host-owned content reference, resolved only at provider wire time.
- `type` (`'document'`): Block discriminator, always `'document'`.

Source: [src/types.ts:2706](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2706)

### `ToolResultImageContent`

Interface.

Image tool-result block with inline base64 `data`.

```ts
interface ToolResultImageContent
```

**Properties**

- `data` (`string`): Base64-encoded payload
- `mediaType` (`string`): IANA media type (e.g. `image/png`, `image/jpeg`)
- `name?` (`string`): Optional display name used in attachment headers
- `ref?` (`undefined`): Absent on inline blocks.
- `type` (`'image'`): Block discriminator, always `'image'`.

Source: [src/types.ts:2642](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2642)

### `ToolResultImageRefContent`

Interface.

Image tool-result block that carries a content reference instead of inline data.

```ts
interface ToolResultImageRefContent
```

**Properties**

- `data?` (`undefined`): Absent on ref blocks. The payload is resolved from `ref`.
- `mediaType` (`string`): IANA media type (e.g. `image/png`, `image/jpeg`)
- `name?` (`string`): Optional display name used in attachment headers
- `ref` (`string`): Opaque host-owned content reference, resolved only at provider wire time.
- `type` (`'image'`): Block discriminator, always `'image'`.

Source: [src/types.ts:2656](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2656)

### `ToolResultTextContent`

Interface.

Text tool-result block.

```ts
interface ToolResultTextContent
```

**Properties**

- `text` (`string`): Text of the block.
- `type` (`'text'`): Block discriminator, always `'text'`.

Source: [src/types.ts:2634](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2634)

### `ToolResultVideoContent`

Interface.

Video tool-result block with inline base64 `data`.

```ts
interface ToolResultVideoContent
```

**Properties**

- `data` (`string`): Base64-encoded payload
- `mediaType` (`string`): IANA media type (e.g. `video/mp4`, `video/webm`)
- `type` (`'video'`): Block discriminator, always `'video'`.

Source: [src/types.ts:2680](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2680)

### `ToolStateController`

Interface.

Per-agent owner of the snapshot + per-call staging.

One controller per agent instance: the snapshot is shared across runs
(so state written in run 1 is visible in run 2 without a reload), while
each tool call gets its own view so its mutations can be journaled and
committed independently.

```ts
interface ToolStateController
```

**Properties**

- `begin` (`(callId: string) => ToolStateView`): Fresh staging view for `callId`. Repeated calls return the same view.
- `commit` (`(callId: string) => Promise<void>`): Apply `callId`'s staged ops to the store and merge them into the
  snapshot, then drop the view. No-op when nothing was staged — which
  is the common case and, crucially, means no durable write happens for
  calls that never touched state.
- `committed` (`Pick<ToolStateView, 'get' | 'has' | 'keys'>`): Committed-only reads, straight off the backing map — no per-call copy
  (unlike `snapshot`) and no prototype-key ambiguity. Staged writes of
  in-flight calls are invisible here until their `commit`. This is what
  `agent.toolState` is built on.
- `discard` (`(callId: string) => void`): Discard `callId`'s view without committing.
- `hydrate` (`() => Promise<void>`): Load the snapshot from the store. Idempotent and memoized — the first
  caller performs the load, later callers await the same promise. Hosts
  don't call this directly; the agent invokes it at run start, which is
  the deterministic outside-the-journal position a durable store needs.
- `pending` (`(callId: string) => readonly ToolStateOp[]`): Ops `callId` has staged but not yet committed, in staging order. For
  reporting a failed commit (the controller drops the view on failure, so
  a caller that wants the lost delta must read it beforehand).
- `snapshot` (`() => Readonly<Record<string, unknown>>`): Current snapshot, for hosts that want to inspect or persist it.

Source: [src/tool-state.ts:154](https://github.com/Tahul/zidane/blob/main/src/tool-state.ts#L154)

### `ToolStateStore`

Interface.

Durable backing for tool state. NOT runtime-specific — implement it
against whatever the host already trusts with durable writes.

`load` runs once per agent instance, before any tool dispatch. `commit`
receives the ops staged by one tool call, in the order the tool made them.

Two requirements on `commit`:

- **Idempotent.** A durable adapter may hand the same ops twice if the
  invocation is retried before the commit is recorded.
- **Non-throwing, ideally.** A rejection does not fail the tool call — the
  body already ran and its side effects already happened, so the dispatch
  path reports the failure (the `tool-state:commit-failed` hook, else a
  stderr line) and drops the write rather than killing the run. Retry inside your implementation if the write
  matters more than the latency.

```ts
interface ToolStateStore
```

**Properties**

- `commit` (`(ops: readonly ToolStateOp[]) => void | Promise<void>`): Persists the ops staged by one tool call, in staging order.
- `load` (`() => Record<string, unknown> | Promise<Record<string, unknown>>`): Returns the persisted state as a key to value record. Called once per agent instance.
- `maxValueBytes?` (`number`): Largest single value this store accepts, in bytes of JSON. Declared by
  the store because the limit belongs to the backend, not the caller:
  a journal's is an entry cap, Redis's is a value cap, a file store's
  is whatever you say it is. `ToolStateView.set` enforces it at stage time.
  Defaults to [`DEFAULT_MAX_VALUE_BYTES`](https://zidane.si/docs/reference/index.md#DEFAULT_MAX_VALUE_BYTES).

Source: [src/tool-state.ts:105](https://github.com/Tahul/zidane/blob/main/src/tool-state.ts#L105)

### `ToolStateView`

Interface.

Tool-facing view. Reads hit the hydrated snapshot merged with this
call's own staged writes; writes stage an op (see the module docstring
for why they aren't written through).

```ts
interface ToolStateView
```

**Properties**

- `delete` (`(key: string) => void`): Stage a deletion.
- `get` (`<T = unknown>(key: string) => T | undefined`): Value for `key`, or `undefined` when absent (or staged for deletion).
  Pass the expected type (`get<boolean>('delivered')`) — what crossed the
  store is JSON, so nothing here can validate it for you.
- `has` (`(key: string) => boolean`): Whether `key` currently resolves to a value.
- `keys` (`() => string[]`): Keys visible to this call, snapshot + staged writes, ascending.
- `set` (`(key: string, value: unknown) => void`): Stage a write. Values must be JSON-serializable — they cross a journal
  and a store. `set(key, undefined)` is normalized to a deletion, since
  that is what a read would report and an undefined value would not
  survive serialization intact.

  Throws if the value is not serializable, or if it exceeds the store's
  size budget (`ToolStateStore.maxValueBytes`, default
  [`DEFAULT_MAX_VALUE_BYTES`](https://zidane.si/docs/reference/index.md#DEFAULT_MAX_VALUE_BYTES)). Failing HERE is the point: the
  alternative is a rejection at commit time, long after the tool that
  caused it returned — and on a replay runtime an oversized state value
  can fail the whole invocation and retry it, so an unguarded fat write is
  a poison pill, not a dropped write.

Source: [src/tool-state.ts:49](https://github.com/Tahul/zidane/blob/main/src/tool-state.ts#L49)

### `TurnUsage`

Interface.

Token usage and timing for one turn.

```ts
interface TurnUsage
```

**Properties**

- `cacheCreation?` (`number`): Tokens written to cache (Anthropic)
- `cacheRead?` (`number`): Tokens read from cache (Anthropic)
- `cost?` (`number`): Cost in USD for this turn. Provider-reported when available
  (OpenRouter, OpenAI via pi-ai); otherwise estimated from `modelId` ×
  pi-ai's bundled price registry by `fillEstimatedCost` in
  `src/providers/cost.ts`. Absent only when neither path could resolve
  a price (unknown / unbundled model).
- `energyJoules?` (`number`): Measured energy the provider reports for this request, in joules (Neuralwatt's `energy.energy_joules`); absent when the provider reports none.
- `finishReason?` (`TurnFinishReason`): Why the model stopped this turn. Providers normalize native stop reasons to this union.
  Absent when the provider did not surface a reason (e.g. mock turns).
- `input` (`number`): Input tokens for the turn. Cache tokens are reported in `cacheRead` and `cacheCreation`.
- `modelId?` (`string`): The model ID the provider ultimately used. May differ from the requested model when the
  provider remaps aliases. Absent for providers that do not echo a model ID.
- `output` (`number`): Output tokens for the turn.
- `provider?` (`string`): Provider key that served this turn, stamped by `fillEstimatedCost`
  alongside the cost fill. Authoritative where present — downstream
  spend attribution (e.g. the GUI usage dashboard) does not have to
  GUESS the provider from the model id's shape, which mis-attributes
  proxied/rebranded models (a Claude model via Bedrock ≠ Anthropic).
  Absent on turns persisted before this field existed.
- `streamDurationMs?` (`number`): Milliseconds from `provider.stream()` dispatch to the stream returning —
  the whole provider round trip for this turn, retries included (same
  span `timeToFirstTokenMs` is measured within). With `output` this gives
  an honest generation speed: `output / (streamDurationMs −
  timeToFirstTokenMs)` is tokens per second of actual generation, which a
  run's wall-clock cannot provide (it includes tool execution). Absent
  when the stream never returned.
- `thinking?` (`number`): Thinking/reasoning tokens used
- `thinkingDropped?` (`number`): Replayed thinking blocks the provider dropped before sampling this turn (Anthropic `input_transformations` entries of type `'thinking_dropped'`); absent when none.
- `timeToFirstTokenMs?` (`number`): Milliseconds from the moment the loop dispatched `provider.stream()`
  (the `stream:start` hook firing) to the first observable byte of this
  turn — earliest of `stream:text`, `stream:thinking`, or a tool_use
  block. Captures the per-turn TTFT independently of run-relative TTFT
  (`AgentStats.timeTillFirstTokenMs`, which only marks the first
  turn).

  Useful for metrics histograms (`gen_ai.client.time_to_first_token`)
  across long multi-turn runs where the run-level metric collapses to
  the cold turn only. Absent for empty-stream turns and turns that
  errored before any byte landed.

Source: [src/types.ts:3516](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3516)

## Types

### `AgentHookBus`

Type.

`agent.hooks`: the agent's hookable, with registration typed through
[`AgentHookHandlers`](https://zidane.si/docs/reference/types.md#AgentHookHandlers) (so handlers see `ctx.decide`) while `callHook`
keeps the raw [`AgentHooks`](https://zidane.si/docs/reference/agent.md#AgentHooks) context shapes.

```ts
type AgentHookBus = AgentHookRegistration & Hookable<AgentHooks>
```

Source: [src/agent-hooks.ts:1816](https://github.com/Tahul/zidane/blob/main/src/agent-hooks.ts#L1816)

### `AgentHookHandlers`

Type.

Handler signatures as consumers see them: identical to [`AgentHooks`](https://zidane.si/docs/reference/agent.md#AgentHooks)
except object contexts also carry `DecideHookContext.decide`. The
agent attaches it (non-enumerable) right before handlers run, so code that
fires hooks builds contexts without it.

```ts
type AgentHookHandlers = { [K in keyof AgentHooks]: K extends `decision:${string}` ? AgentHooks[K] : WithDecide<AgentHooks[K]> }
```

Source: [src/agent-hooks.ts:1800](https://github.com/Tahul/zidane/blob/main/src/agent-hooks.ts#L1800)

### `AgentToolState`

Type.

Read-only window over an agent's durable tool state — the read half of the
[`ToolStateView`](https://zidane.si/docs/reference/types.md#ToolStateView) a tool sees through `ToolContext.state`, minus
the write path. Backed by `ToolStateController.committed`, so a host
reads exactly what the tools have committed so far; a call's staged writes
stay invisible until it settles.

Reads are lazy: they resolve against the live state at call time, so a read
inside a `turn:after` / `agent:done` hook sees every commit from the turn
that just settled. The state hydrates from the configured
`AgentOptions.toolState` store at run start (memoized on success,
retried on a failed load) — before the first run, reads report only what
this process has committed, which for a fresh agent is nothing. Read after
a run, not before.

There is no `set`: a host-level write has no tool result to be journaled
with, so it would not survive a durable replay — exposing it would
reintroduce exactly the class of bug the staging design exists to prevent.
Mutate state from inside a tool.

Scope is this agent instance only. Tool state is NOT inherited by subagents
(`spawn` starts a fresh controller), so this view never reflects a child's
writes.

```ts
type AgentToolState = Pick<ToolStateView, 'get' | 'has' | 'keys'>
```

Source: [src/agent-types.ts:409](https://github.com/Tahul/zidane/blob/main/src/agent-types.ts#L409)

### `DecisionAnswer`

Type.

Answer to one question: a predicate, choice or score answer, or a refusal.

```ts
type DecisionAnswer =
  | DecisionPredicateAnswer
  | DecisionChoiceAnswer
  | DecisionScoreAnswer
  | DecisionRefusalAnswer
```

Source: [src/decisions/types.ts:108](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L108)

### `DecisionAnswers`

Type.

Answers keyed by question name, each typed from its question.

```ts
type DecisionAnswers<Q extends DecisionQuestions> = { readonly [K in keyof Q]: DecisionAnswerFor<Q[K]> }
```

**Type parameters**

- `Q` (`extends DecisionQuestions`)

Source: [src/decisions/types.ts:120](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L120)

### `DecisionFailureReason`

Type.

Why a decision produced no answers:

- `unavailable`: decisions disabled, no capable provider, missing credentials, or unknown model
- `auth`: the provider rejected the credential (401/403)
- `unsupported`: the input can't be expressed on this provider's wire (e.g. images)
- `timeout` / `aborted`: wall-clock cap hit / caller or run cancelled
- `rate_limited` / `quota`: 429 or 529 / 402
- `invalid`: the request failed validation, or the response was malformed
- `error`: anything else (5xx, network)

```ts
type DecisionFailureReason =
  | 'unavailable'
  | 'auth'
  | 'unsupported'
  | 'timeout'
  | 'aborted'
  | 'rate_limited'
  | 'quota'
  | 'invalid'
  | 'error'
```

Source: [src/decisions/types.ts:160](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L160)

### `DecisionInput`

Type.

The evidence the questions are evaluated against:

- a string,
- a JSON object of related context,
- multimodal prompt parts (text + images; support varies per provider).

```ts
type DecisionInput = string | Readonly<Record<string, unknown>> | readonly PromptPart[]
```

Source: [src/decisions/types.ts:61](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L61)

### `DecisionOutcome`

Type.

Result of a decision: a [`DecisionSuccess`](https://zidane.si/docs/reference/types.md#DecisionSuccess) or a [`DecisionFailure`](https://zidane.si/docs/reference/types.md#DecisionFailure). Narrow on `ok`.

```ts
type DecisionOutcome<Q extends DecisionQuestions = DecisionQuestions> = 
  | DecisionSuccess<Q>
  | DecisionFailure
```

**Type parameters**

- `Q` (`extends DecisionQuestions`, default `DecisionQuestions`)

Source: [src/decisions/types.ts:208](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L208)

### `DecisionQuestion`

Type.

One question: a predicate, a choice or a score.

```ts
type DecisionQuestion =
  | DecisionPredicateQuestion
  | DecisionChoiceQuestion
  | DecisionScoreQuestion
```

Source: [src/decisions/types.ts:50](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L50)

### `DecisionQuestions`

Type.

Questions keyed by name. The name keys the matching answer.

```ts
type DecisionQuestions = Record<string, DecisionQuestion>
```

Source: [src/decisions/types.ts:53](https://github.com/Tahul/zidane/blob/main/src/decisions/types.ts#L53)

### `MutationValidationResult`

Type.

Result of a validation: `{ ok: true }`, or `{ ok: false }` with an optional message and error list.

```ts
type MutationValidationResult =
  | { ok: true }
  | { errors?: readonly string[]; message?: string; ok: false }
```

Source: [src/mutation-validation.ts:36](https://github.com/Tahul/zidane/blob/main/src/mutation-validation.ts#L36)

### `ProgressNudgeKind`

Type.

Stall pattern that can trigger a progress nudge. See `AgentBehavior.progressNudges`.

```ts
type ProgressNudgeKind =
  | 'readOnlyTurns'
  | 'failedMutations'
  | 'noopMutations'
  | 'validationRejects'
  | 'toolErrors'
  | 'singleCallTurns'
```

Source: [src/types.ts:404](https://github.com/Tahul/zidane/blob/main/src/types.ts#L404)

### `ProgressNudgeMessage`

Type.

Message a progress nudge steers the model with: a fixed string, or a function of the nudge `kind` and its trigger `count`.

```ts
type ProgressNudgeMessage =
  | string
  | ((ctx: { count: number; kind: ProgressNudgeKind }) => string)
```

Source: [src/types.ts:407](https://github.com/Tahul/zidane/blob/main/src/types.ts#L407)

### `PromptPart`

Type.

One block of a multimodal user prompt.

`agent.run({ prompt })` accepts either a plain string (treated as a single
text part) or an array of these parts for multimodal inputs.

`document` parts are routed per provider: PDF-style mime types are sent as
native document blocks when the provider supports them; text documents are
inlined as text with an attachment header. Providers that cannot handle an
image or document throw early.

```ts
type PromptPart =
  | PromptTextPart
  | PromptImagePart
  | PromptAudioPart
  | PromptVideoPart
  | PromptDocumentPart
```

Source: [src/types.ts:2386](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2386)

### `QueuedMessageOptions`

Type.

What `Agent.steer` and `Agent.followUp` take besides the prompt.

```ts
type QueuedMessageOptions = Pick<QueuedUserMessage, 'id' | 'systemNotes'>
```

Source: [src/types.ts:2360](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2360)

### `RepeatGuardToolMatcher`

Type.

Per-tool predicate selecting which tools the [`RepeatGuardConfig`](https://zidane.si/docs/reference/types.md#RepeatGuardConfig)
watches. A bare string matches that exact canonical name; a function
receives the canonical name and returns `true` to track it.

```ts
type RepeatGuardToolMatcher = string | ((name: string) => boolean)
```

Source: [src/types.ts:628](https://github.com/Tahul/zidane/blob/main/src/types.ts#L628)

### `ResolveContentRef`

Type.

Host callback that resolves a content reference to its payload. Called at provider wire time with the block that holds the `ref`.

```ts
type ResolveContentRef = (block: ResolveContentRefBlock) => Promise<{ data: string }>
```

Source: [src/types.ts:2736](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2736)

### `RunHookMap`

Type.

Per-run hook registrations. Each entry can be a single handler or an array of handlers.
Keys are `AgentHooks` event names (loose-typed here to avoid a circular import; `agent-run.ts`
validates the event names at run time, and [`AgentHookMap`](https://zidane.si/docs/reference/agent.md#AgentHookMap) is the strongly-typed form).

```ts
type RunHookMap = Record<string, ((ctx: any) => unknown) | ((ctx: any) => unknown)[]>
```

Source: [src/types.ts:3267](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3267)

### `SessionContentBlock`

Type.

Content block of a session message: text, media, a tool call, a tool result, or thinking.

`text`, `tool_call` and `thinking` blocks can carry a `signature`: opaque
state the producing API wants back with that block (a Gemini thought
signature, an Anthropic thinking HMAC, OpenAI encrypted reasoning), tagged
with its `SignatureProducer`. Only that provider's wire builder reads
it; the others map blocks field by field and never send it.

```ts
type SessionContentBlock =
  | { origin?: TextOrigin; signature?: string; signatureProducer?: GeminiSignatureScope; systemNote?: SystemNote; text: string; type: 'text' }
  | { data: string; mediaType: string; name?: string; ref?: never; type: 'image' }
  | { data?: never; mediaType: string; name?: string; ref: string; type: 'image' }
  | { data: string; mediaType: string; name?: string; type: 'audio' }
  | { data: string; mediaType: string; name?: string; type: 'video' }
  | { data: string; encoding: 'base64' | 'text'; mediaType: string; name?: string; ref?: never; type: 'document' }
  | { data?: never; encoding: 'base64' | 'text'; mediaType: string; name?: string; ref: string; type: 'document' }
  | { id: string; input: Record<string, unknown>; name: string; signature?: string; signatureProducer?: GeminiSignatureScope; type: 'tool_call' }
  | { callId: string; calls?: NestedToolCallRecord[]; durationMs?: number; isError?: boolean; metadata?: ToolResultMetadata; output: string | ToolResultContent[]; type: 'tool_result' }
  | { signature?: string; signatureProducer?: SignatureProducer; text: string; type: 'thinking' }
  | { data: string; type: 'redacted_thinking' }
  | { details: unknown[]; model?: string; producer: 'openrouter'; type: 'provider_reasoning' }
  | { model?: string; producer: 'vllm'; reasoningText: string; type: 'provider_reasoning' }
  | { id: string; input: Record<string, unknown>; name: string; producer: 'anthropic'; type: 'server_tool_use' }
  | { content: unknown; producer: 'anthropic'; toolName: string; toolUseId: string; type: 'server_tool_result' }
  | { compactedAt: number; model: string; replacesTurnIds: readonly string[]; summary: string; type: 'compact-summary'; usage: TurnUsage }
```

Source: [src/types.ts:3003](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3003)

### `SessionEndStatus`

Type.

How a run ended: `'completed'`, `'aborted'` or `'error'`.

```ts
type SessionEndStatus = 'completed' | 'aborted' | 'error'
```

Source: [src/types.ts:4160](https://github.com/Tahul/zidane/blob/main/src/types.ts#L4160)

### `SuccessfulMutationOutcome`

Type.

Tool outcomes that count as a successful mutation.

```ts
type SuccessfulMutationOutcome = Extract<ToolOutcome, 'created' | 'updated' | 'edited'>
```

Source: [src/mutation-validation.ts:7](https://github.com/Tahul/zidane/blob/main/src/mutation-validation.ts#L7)

### `ThinkingLevel`

Type.

One of the `THINKING_LEVELS` strings: how much the model reasons before it answers.

```ts
type ThinkingLevel = typeof THINKING_LEVELS[number]
```

Source: [src/thinking-levels.ts:16](https://github.com/Tahul/zidane/blob/main/src/thinking-levels.ts#L16)

### `ToolOutcome`

Type.

Structured side-effect outcome for mutating tools (write_file, edit,
multi_edit, and custom tools that opt in via `ToolContext.reportOutcome`).

Surfaced on `tool:after.outcome` so hosts can build no-op-resistant loop
guards ("did this edit actually change bytes?") WITHOUT string-matching the
result prose (`Created `/`Updated `/`No change needed` …), which is fragile
across versions. Absent on `tool:after` for read-only tools and any tool
that never calls `reportOutcome`.

- `created` — a new file/resource was written.
- `updated` — an existing file/resource changed (full overwrite differed).
- `edited`  — a surgical in-place change applied at least one replacement.
- `noop`    — the call succeeded but changed zero bytes (idempotent write,
  replacement that produced identical content).
- `failed`  — the mutation was attempted but did not take effect (error,
  stale-read rejection, no-match). Pairs with `isError` on the
  wire result; reported explicitly so guards don't have to infer.

```ts
type ToolOutcome = 'created' | 'updated' | 'edited' | 'noop' | 'failed'
```

Source: [src/types.ts:2495](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2495)

### `ToolResultContent`

Type.

A single block of structured tool-result content.

MCP servers can return a mix of text, image, audio, video, and resource blocks.
Tools return `string` for the common text-only case or `ToolResultContent[]` when
they need to preserve non-text content (e.g. screenshots from a browser MCP).

Providers that support native multi-part tool results (Anthropic, OpenAI Codex via
pi-ai) route image blocks into their wire format verbatim; OpenAI-compat providers
route them via a companion-user-message fallback when the underlying model/endpoint
does not accept images inside tool-role messages.

```ts
type ToolResultContent =
  | ToolResultTextContent
  | ToolResultImageContent
  | ToolResultImageRefContent
  | ToolResultAudioContent
  | ToolResultVideoContent
  | ToolResultDocumentContent
  | ToolResultDocumentRefContent
```

Source: [src/types.ts:2467](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2467)

### `ToolResultMetadata`

Type.

Provider/model-invisible metadata associated with a tool result. Hooks use
this to persist telemetry about transforms (compression, redaction, etc.)
without adding model-visible text to the result body. Hooks can read it; the
model never sees it.

```ts
type ToolResultMetadata = Record<string, JsonValue>
```

Source: [src/types.ts:2526](https://github.com/Tahul/zidane/blob/main/src/types.ts#L2526)

### `ToolStateOp`

Type.

One staged mutation. JSON-shaped on purpose: durable adapters journal
these verbatim, so a tagged union with an explicit delete flag beats
`value: undefined` (which JSON drops).

```ts
type ToolStateOp =
  | { delete?: never; key: string; value: unknown }
  | { delete: true; key: string; value?: never }
```

Source: [src/tool-state.ts:84](https://github.com/Tahul/zidane/blob/main/src/tool-state.ts#L84)

### `TurnFinishReason`

Type.

Reason the provider gave for stopping the turn.

- `'stop'` — natural turn end (`end_turn` / `stop_sequence`).
- `'tool-calls'` — model emitted tool_use blocks.
- `'length'` — `max_tokens` reached, or (Anthropic 4.6+) the response bumped
  against the model's context window mid-stream
  (`model_context_window_exceeded`). The partial response is preserved; the
  loop emits this reason so consumers can prune/retry.
- `'content-filter'` — model refused.
- `'pause'` — Anthropic `pause_turn`: a server-side mid-turn pause for very
  long thinking. The loop continues with a synthetic "Please continue."
  user message rather than terminating; consumers see the pause via this
  finish reason on the prior assistant turn.
- `'error'` — provider classified the turn as failed.
- `'other'` — unknown / unmapped.

```ts
type TurnFinishReason =
  | 'stop'
  | 'tool-calls'
  | 'length'
  | 'content-filter'
  | 'pause'
  | 'error'
  | 'other'
```

Source: [src/types.ts:3513](https://github.com/Tahul/zidane/blob/main/src/types.ts#L3513)

## Re-exports

These names are documented on the entry point that owns them.

- [zidane/agent](https://zidane.si/docs/reference/agent.md): [`Agent`](https://zidane.si/docs/reference/agent.md#Agent), [`AgentHookMap`](https://zidane.si/docs/reference/agent.md#AgentHookMap), [`AgentHooks`](https://zidane.si/docs/reference/agent.md#AgentHooks), [`AgentOptions`](https://zidane.si/docs/reference/agent.md#AgentOptions)
- [zidane/errors](https://zidane.si/docs/reference/errors.md): [`AgentAbortedError`](https://zidane.si/docs/reference/errors.md#AgentAbortedError), [`AgentBudgetExceededError`](https://zidane.si/docs/reference/errors.md#AgentBudgetExceededError), [`AgentContextExceededError`](https://zidane.si/docs/reference/errors.md#AgentContextExceededError), [`AgentProviderError`](https://zidane.si/docs/reference/errors.md#AgentProviderError), [`AgentToolNotAllowedError`](https://zidane.si/docs/reference/errors.md#AgentToolNotAllowedError), [`ClassifiedError`](https://zidane.si/docs/reference/errors.md#ClassifiedError), [`ClassifiedErrorKind`](https://zidane.si/docs/reference/errors.md#ClassifiedErrorKind), [`CONTEXT_EXCEEDED_MESSAGE_PATTERNS`](https://zidane.si/docs/reference/errors.md#CONTEXT_EXCEEDED_MESSAGE_PATTERNS), [`matchesContextExceeded`](https://zidane.si/docs/reference/errors.md#matchesContextExceeded)
- [zidane/providers/anthropic](https://zidane.si/docs/reference/providers-anthropic.md): [`AnthropicParams`](https://zidane.si/docs/reference/providers-anthropic.md#AnthropicParams)
- [zidane/contexts](https://zidane.si/docs/reference/contexts.md): [`BackgroundTaskStatus`](https://zidane.si/docs/reference/contexts.md#BackgroundTaskStatus), [`ContextCapabilities`](https://zidane.si/docs/reference/contexts.md#ContextCapabilities), [`ContextHardening`](https://zidane.si/docs/reference/contexts.md#ContextHardening), [`ContextType`](https://zidane.si/docs/reference/contexts.md#ContextType), [`DetachedTasksCapability`](https://zidane.si/docs/reference/contexts.md#DetachedTasksCapability), [`ExecResult`](https://zidane.si/docs/reference/contexts.md#ExecResult), [`ExecutionContext`](https://zidane.si/docs/reference/contexts.md#ExecutionContext), [`ExecutionHandle`](https://zidane.si/docs/reference/contexts.md#ExecutionHandle), [`SpawnConfig`](https://zidane.si/docs/reference/contexts.md#SpawnConfig), [`TaskEntry`](https://zidane.si/docs/reference/contexts.md#TaskEntry), [`TaskExitInfo`](https://zidane.si/docs/reference/contexts.md#TaskExitInfo), [`TaskHandle`](https://zidane.si/docs/reference/contexts.md#TaskHandle), [`TaskStallInfo`](https://zidane.si/docs/reference/contexts.md#TaskStallInfo)
- [zidane/providers/cerebras](https://zidane.si/docs/reference/providers-cerebras.md): [`CerebrasParams`](https://zidane.si/docs/reference/providers-cerebras.md#CerebrasParams)
- [zidane/tools](https://zidane.si/docs/reference/tools.md): [`ChildAgent`](https://zidane.si/docs/reference/tools.md#ChildAgent), [`InteractionToolOptions`](https://zidane.si/docs/reference/tools.md#InteractionToolOptions), [`ReadStateEntry`](https://zidane.si/docs/reference/tools.md#ReadStateEntry), [`ReadStateMap`](https://zidane.si/docs/reference/tools.md#ReadStateMap), [`SpawnToolOptions`](https://zidane.si/docs/reference/tools.md#SpawnToolOptions), [`SpawnToolState`](https://zidane.si/docs/reference/tools.md#SpawnToolState), [`ToolContext`](https://zidane.si/docs/reference/tools.md#ToolContext), [`ToolDef`](https://zidane.si/docs/reference/tools.md#ToolDef), [`ToolDescriptionOverride`](https://zidane.si/docs/reference/tools.md#ToolDescriptionOverride), [`ToolDescriptions`](https://zidane.si/docs/reference/tools.md#ToolDescriptions), [`ToolDescriptionTarget`](https://zidane.si/docs/reference/tools.md#ToolDescriptionTarget), [`ToolMap`](https://zidane.si/docs/reference/tools.md#ToolMap), [`ValidationResult`](https://zidane.si/docs/reference/tools.md#ValidationResult)
- [zidane/session](https://zidane.si/docs/reference/session.md): [`CreateSessionOptions`](https://zidane.si/docs/reference/session.md#CreateSessionOptions), [`RemoteStoreOptions`](https://zidane.si/docs/reference/session.md#RemoteStoreOptions), [`Session`](https://zidane.si/docs/reference/session.md#Session), [`SessionData`](https://zidane.si/docs/reference/session.md#SessionData), [`SessionRun`](https://zidane.si/docs/reference/session.md#SessionRun), [`SessionStore`](https://zidane.si/docs/reference/session.md#SessionStore)
- [zidane/mcp](https://zidane.si/docs/reference/mcp.md): [`McpConnection`](https://zidane.si/docs/reference/mcp.md#McpConnection)
- [zidane/providers/openai](https://zidane.si/docs/reference/providers-openai.md): [`OpenAIParams`](https://zidane.si/docs/reference/providers-openai.md#OpenAIParams)
- [zidane/providers/openrouter](https://zidane.si/docs/reference/providers-openrouter.md): [`OpenRouterParams`](https://zidane.si/docs/reference/providers-openrouter.md#OpenRouterParams)
- [zidane/presets](https://zidane.si/docs/reference/presets.md): [`Preset`](https://zidane.si/docs/reference/presets.md#Preset)
- [zidane/providers](https://zidane.si/docs/reference/providers.md): [`Provider`](https://zidane.si/docs/reference/providers.md#Provider), [`ProviderCapabilities`](https://zidane.si/docs/reference/providers.md#ProviderCapabilities), [`StreamCallbacks`](https://zidane.si/docs/reference/providers.md#StreamCallbacks), [`StreamOptions`](https://zidane.si/docs/reference/providers.md#StreamOptions), [`ToolCall`](https://zidane.si/docs/reference/providers.md#ToolCall), [`ToolResult`](https://zidane.si/docs/reference/providers.md#ToolResult), [`ToolSpec`](https://zidane.si/docs/reference/providers.md#ToolSpec), [`TurnResult`](https://zidane.si/docs/reference/providers.md#TurnResult)
- [zidane/contexts/sandbox](https://zidane.si/docs/reference/contexts-sandbox.md): [`SandboxProvider`](https://zidane.si/docs/reference/contexts-sandbox.md#SandboxProvider)
- [zidane/skills](https://zidane.si/docs/reference/skills.md): [`SkillConfig`](https://zidane.si/docs/reference/skills.md#SkillConfig), [`SkillResource`](https://zidane.si/docs/reference/skills.md#SkillResource), [`SkillsConfig`](https://zidane.si/docs/reference/skills.md#SkillsConfig)
- [zidane/tools/prompts](https://zidane.si/docs/reference/tools-prompts.md): [`ToolPrompts`](https://zidane.si/docs/reference/tools-prompts.md#ToolPrompts)
