# zidane/agent

> The agent core, `createAgent()`, with manual compaction, run summaries, cache telemetry and read-state helpers.

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

Use `zidane/agent` for the agent loop without the eval, headless and UI surfaces of the root `zidane` entry. It holds `createAgent()`, the `Agent` and `AgentOptions` types, hook maps, the run-summary collector, prompt-cache drift logging and the per-session read state. An agent can also compact its own conversation with `agent.compact()`, report when its prompt cache is estimated to expire with `agent.cacheExpiresAt()`, and estimate what a model, thinking, system or tools change would re-read with `agent.cacheImpact()`. Every symbol is also exported from `zidane`. See [Agents](https://zidane.si/docs/agents.md) for the guide.

```ts
import { createAgent, createRunSummaryCollector } from 'zidane/agent'
import { basic } from 'zidane/presets'
import { anthropic } from 'zidane/providers/anthropic'

const agent = createAgent({ ...basic, provider: anthropic() })
const collector = createRunSummaryCollector()
const uninstall = collector.install(agent.hooks)

await agent.run({ prompt: 'Name the files in this directory.' })
uninstall()
console.log(collector.latest()?.totals.output)
console.log(agent.cacheImpact({ model: 'claude-opus-5' }).tokens, agent.cacheExpiresAt())

const result = await agent.compact({ keepTurns: 2 })
console.log(result.compacted)
```

## Functions

### `createAgent`

Function.

Creates an agent: a provider, tools, hooks and optional session behind one `run()` call.

Setup is synchronous. MCP servers and skills connect on the first `run()` or `warmup()`,
or in the background when `eager` is set. Throws when `provider` is an empty array or
a multi-provider list has blank or duplicate provider names.

```ts
function createAgent(rawOptions: AgentOptions): Agent
```

**Parameters**

- `rawOptions` (`AgentOptions`): Agent options. Only `provider` is required.

**Returns** `Agent`: The [`Agent`](https://zidane.si/docs/reference/agent.md#Agent). Call `agent.destroy()` when you are done to release the execution context.

**Example**

```ts
~~~ts
import { createAgent } from 'zidane'
import { anthropic } from 'zidane/providers/anthropic'

const agent = createAgent({
  provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }),
  system: 'You are a helpful assistant.',
})

const stats = await agent.run({ prompt: 'Say hello.' })
console.log(`Done in ${stats.turns} turns`)
await agent.destroy()
~~~
```

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

### `createRunSummaryCollector`

Function.

Build a run-summary collector. State is created fresh inside each
`install()` call, so a single collector instance can be installed
across multiple agents without attribution cross-talk. `latest()`
returns the most-recent summary across **any** install — install
per-agent collectors if you need separate post-run snapshots.

```ts
function createRunSummaryCollector(
  options?: RunSummaryCollectorOptions,
): RunSummaryCollector
```

**Parameters**

- `options?` (`RunSummaryCollectorOptions`, default `{}`): Collector options. `onSummary` is called with each finished summary.

**Returns** `RunSummaryCollector`

**Example**

```ts
const collector = createRunSummaryCollector({
  onSummary: s => console.log(JSON.stringify(s)),
})
const uninstall = collector.install(agent.hooks)
try { await agent.run({ prompt }) }
finally { uninstall() }
```

Source: [src/run-summary.ts:261](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L261)

### `diffCacheDimensions`

Function.

Compare two snapshots and return the list of dimensions whose bytes
differ. The `details` map carries a short human-readable description per
changed dimension (e.g. `system: 1024 → 1031 bytes`).

Pure / no I/O. Use this in tests to assert the diff logic; the logger
itself just formats and emits the result.

```ts
function diffCacheDimensions(
  prev: CacheDimensionSnapshot,
  curr: CacheDimensionSnapshot,
): CacheDimensionDiff
```

**Parameters**

- `prev` (`CacheDimensionSnapshot`): Snapshot of the previous turn.
- `curr` (`CacheDimensionSnapshot`): Snapshot of the current turn.

**Returns** `CacheDimensionDiff`

Source: [src/cache-telemetry.ts:660](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L660)

### `fnv1a32`

Function.

FNV-1a-style 32-bit hash over UTF-16 code units. Pure and allocation-free.
Cryptographic strength is irrelevant here — we only need change detection,
and a 32-bit collision is unlikely for the few dimensions tracked.

Feeds the FULL char code into the mix (same as read-state's
`hashContent`) — masking to the low byte would collapse code points that
share a low byte, so a non-ASCII system-prompt change could hash
identically and silently hide a cache-dimension drift.

Exported for tests; callers shouldn't typically import directly.

```ts
function fnv1a32(s: string): number
```

**Parameters**

- `s` (`string`): String to hash.

**Returns** `number`

Source: [src/cache-telemetry.ts:395](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L395)

### `hashMessages`

Function.

Hash each message independently, preserving order.

Per-message rather than cumulative so the diff can tell an APPEND (healthy)
from a REWRITE (a cache break) by comparing positionally — see
`CacheDimensionSnapshot.messageHashes`.

Folds field-by-field into a rolling FNV rather than `JSON.stringify`-ing the
whole list at once: on a long run the transcript is hundreds of KB, and
building one giant string every turn just to throw it away is the kind of
overhead that gets telemetry ripped back out.

Exported so hosts can stamp the same hashes onto their own spans.

```ts
function hashMessages(messages: readonly { content: unknown; role: string }[]): number[]
```

**Parameters**

- `messages` (`readonly { content: unknown; role: string }[]`): Messages in transcript order.

**Returns** `number[]`

Source: [src/cache-telemetry.ts:605](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L605)

### `installCacheBreakLogger`

Function.

Install the cache-break logger on an agent's hook bus.

Subscribes to `turn:before` (snapshot) + `turn:after` (compare with prior
snapshot, log breaks), plus `session:start` + `agent:start` to pick up the
session's baseline (unless `sessionBaseline: false`). Returns an
`uninstall` fn that detaches every listener. Idempotent w\.r.t. agent
lifecycle — installs/uninstalls cleanly across runs.

Recommended setup:

```ts
const uninstall = installCacheBreakLogger(agent.hooks, {
  log: line => logger.debug(line),
  verbose: !!process.env.ZIDANE_DEBUG?.includes('cache'),
})
// ...
uninstall()
```

Performance: hashing runs on each `turn:before`. The tools array and each
message block are serialized once per object (memoized), so a turn costs
the system prompt plus the blocks that are new since the last one.
Disabled callers pay nothing (don't install).

```ts
function installCacheBreakLogger(
  hooks: Hookable<AgentHooks>,
  options?: CacheBreakLoggerOptions,
): () => void
```

**Parameters**

- `hooks` (`Hookable<AgentHooks>`): Agent hook bus to install the logger on.
- `options?` (`CacheBreakLoggerOptions`, default `{}`): Logger options. All fields are optional.

**Returns** `() => void`

Source: [src/cache-telemetry.ts:768](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L768)

### `snapshotCacheDimensions`

Function.

Build a [`CacheDimensionSnapshot`](https://zidane.si/docs/reference/agent.md#CacheDimensionSnapshot) from a `turn:before` payload.

Pure — exported so hosts can compute snapshots outside the logger (e.g.
stamping them onto custom telemetry spans). The hash is deterministic on
byte-identical inputs.

```ts
function snapshotCacheDimensions(
  options: StreamOptions,
  sections?: CachePromptSections,
): CacheDimensionSnapshot
```

**Parameters**

- `options` (`StreamOptions`): Stream options of the outgoing request.
- `sections?` (`CachePromptSections`): Texts of the sections inside `options.system` to hash separately. The halves around `SYSTEM_PROMPT_BOUNDARY` are always hashed.

**Returns** `CacheDimensionSnapshot`

Source: [src/cache-telemetry.ts:417](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L417)

## Interfaces

### `Agent`

Interface.

Agent instance returned by [`createAgent`](https://zidane.si/docs/reference/agent.md#createAgent). Runs prompts, queues steering messages and exposes live state.

```ts
interface Agent
```

**Properties**

- `[asyncDispose]` (`() => Promise<void>`): TC39 explicit-resource-management `await using` sink — alias for
  `Agent.destroy`. Lets hosts write:

  ```ts
  await using agent = createAgent({ ... })
  await agent.run({ prompt: 'go' })
  // agent.destroy() runs automatically at scope exit
  ```

  Requires Node 20+ (or any runtime that ships the TC39
  explicit-resource-management proposal). Older runtimes simply ignore
  the symbol; nothing breaks. Idempotent because `destroy()` is.
- `abort` (`() => void`): Aborts the current run: the run signal fires, so an in-flight provider stream and tool calls unwind, and `run()` resolves with the stats so far. No-op when idle.
  Before the run started (`run()` / `resume()` still loading the `sessionId` session), the run does not start; while `run()` continues an interrupted run first (`onInterrupted: 'resume'`), the prompt does not run either.
- `activateSkill` (`(name: string) => Promise<void>`): Explicitly activate a skill by name. Fires `skills:activate` with
  `via: 'explicit'`. Throws if skills are off for this agent, if the skill
  isn't in the resolved catalog, or if the `maxActive` cap is reached. Idempotent — activating an already-active
  skill is a no-op.
- `activeSkills` (`readonly ActiveSkill[]`, readonly): Snapshot of currently active skills.
- `cacheExpiresAt` (`() => number | null`): When the provider's prompt cache for this conversation is estimated to
  expire, in epoch ms: the time this agent sent its last request plus the
  cache lifetime that request used (`behavior.cacheTtl`, else 5 minutes).
  `null` before this agent sent any request. An estimate: the provider can
  evict earlier, and a provider that defaults to a longer lifetime without
  `cacheTtl` set is still counted as 5 minutes.
- `cacheImpact` (`(change: AgentCacheChange) => AgentCacheImpact`): Estimate what `change` would make the next request read again instead of
  from the prompt cache. Literal: a model id or thinking level that differs
  from the last request counts, and `system` / `tools` count when `true`.
  It knows nothing about how the host applies a change, so a host that
  sends a change without touching the request (e.g. effort as a note in
  the conversation) should not ask. With no request sent yet, `model` and
  `thinking` have nothing to compare against and never count.
- `cancelTool` (`(callId: string, reason?: string) => boolean`): Cancel a single in-flight tool call by id without aborting the rest of
  the run. The matching call's `ctx.signal` flips and a `tool:cancelled`
  hook fires; the call's wire result becomes the canonical
  [`TOOL_USE_CANCELLED_MESSAGE`](https://zidane.si/docs/reference/index.md#TOOL_USE_CANCELLED_MESSAGE) with `isError: true`, while every
  other concurrent call keeps running and the assistant turn closes
  normally on the next batch boundary.

  Returns `true` when a live call with that id was found and the cancel
  flag was flipped (idempotent — repeated calls return `false`).
  `false` when no such call is currently dispatching. The cancel is then
  parked and applied if a call with that id registers before the next run
  starts or the agent is destroyed, so a cancel issued after the call has
  already produced a tool_result has no effect.

  The optional `reason` is forwarded to the `tool:cancelled` hook ctx
  and to the abort signal's `reason` field — useful for telemetry that
  wants to distinguish "user clicked cancel" from a host-side policy.
- `compact` (`(options?: AgentCompactOptions) => Promise<AgentCompactResult>`): Summarize the session's older turns now, the way auto-compaction does
  inside a run: same summary, restoration of recently read files, hooks
  (`compact:start` / `compact:progress` / `compact:end` with
  `reason: 'manual'`) and persistence. When the history still starts with
  the last request a run sent, that request is resent with the summary
  instruction so the provider reads it from its prompt cache.

  Needs an idle agent: rejects while a run (or another compaction) is in
  progress, and `isRunning` is `true` while it works, so `run()` rejects
  meanwhile. Rejects when the summary call fails.
- `deactivateSkill` (`(name: string) => Promise<void>`): Deactivate a skill by name. Fires `skills:deactivate` with `reason: 'explicit'`.
  No-op when the skill wasn't active.
- `decide` (`AgentDecide`): Make a typed decision request (predicate probability, choice, score)
  against the agent's decision provider — see `AgentOptions.decisions`.
  Never throws unless called with `{ throw: true }`; failures come back as
  `{ ok: false, reason }`. Inside hooks, prefer `ctx.decide`, which is bound
  to the active run.
- `destroy` (`() => Promise<void>`): Destroy the execution context and clean up resources.
  Idempotent — safe to call from both a `finally` block and a signal handler.
- `ensureExecution` (`() => Promise<ExecutionHandle>`): Ensure the execution handle exists (spawning it if this is called
  before the first run), firing the one-time `execution:ready` hook
  exactly as `run()` would. The seam for hosts and extension bridges
  (`ctx.workspace`) that need the live `(execution, handle)` pair
  before any run has happened. Shares `run()`'s in-flight spawn
  promise — concurrent callers observe one spawn — and mirrors
  `run()`'s destroy semantics: calling after `destroy()` re-spawns
  (the supported resurrection contract), while a destroy landing
  DURING the spawn reaps the fresh handle and rejects.
- `execution` (`ExecutionContext`, readonly): Execution context that decides where tools run.
- `followUp` (`(prompt: string | PromptPart[], options?: QueuedMessageOptions) => boolean`): Queue a message delivered after the run would otherwise end (the model
  returned a final answer). Same abort contract as `steer`: no-ops
  and returns `false` once the run is aborted, `true` when queued. Takes
  the same `prompt` and `options` as `steer`.
- `getContextBreakdown` (`(opts?: ContextBreakdownOptions) => Promise<ContextBreakdown | null>`): Categorized context-window usage — system prompt, rules, skills, MCP
  tools/instructions, searchable-tools catalog, tool definitions, conversation, deferred
  buckets, autocompact buffer, and free space. Computed on demand (the host
  opens a panel / popover). Available even before the first run: when no run
  snapshot exists yet it assembles a skills-only pre-run view (no MCP connect),
  so it never returns `null` for a live agent — only after `destroy()`.

  Per-category counts are heuristic estimates reconciled so the live
  categories sum to the real last-turn total; when the provider exposes
  `Provider.countTokens` (Anthropic, OpenAI), the system/tools buckets
  use exact counts and drop their `estimated` flag. One network round-trip
  on the exact path; the host decides when to call it.

  The host supplies `effectiveWindow` (the model's `rawWindow -
  outputReserve`, resolved via the chat model registry which the agent core
  deliberately doesn't depend on). When omitted, free-space / fraction are
  computed against `used` only (bar still renders, just without spare-space).
- `getContextSegments` (`(opts?: { rulesBlock?: string; rulesFiles?: readonly ContextSegmentFile[]; systemPath?: string }) => Promise<ContextSegment[] | null>`): The TEXT of every chunk in the session's default context, each tagged with
  where it came from — the sibling of `Agent.getContextBreakdown`,
  which reports the same assembly as token counts. Hosts render it as a
  "what is actually loaded" list the user can read, with the file-backed
  segments openable in an editor.

  Reads the live run snapshot, falling back to the same pre-run assembly
  `getContextBreakdown` uses, so it's populated before the first message and
  returns `null` only after `destroy()`. Cheap: no provider round-trip and no
  tool serialization — just the already-assembled strings.

  Instruction files (AGENTS.md / CLAUDE.md) are HOST-supplied via
  `opts.rulesFiles`: the agent receives the rendered rules block as an opaque
  substring of the host's system prompt and never reads those files itself,
  so it knows their paths at best and never their text. A host that already
  called `discoverAgentsMd` passes its `files` straight through — the shapes
  line up — and gets per-file segments with real `path`s. Omit it and the
  list covers only the agent-owned segments.

  `opts.rulesBlock` is the rendered instruction block as it appears inside the
  system string. Pass it so the base-prompt segment can subtract it and the
  instruction text isn't reported twice (once in the system prompt, once as
  its own file rows). Falls back to the assembly's copy, which is only present
  when the host threaded `AgentRunOptions.contextSections` into `run()`.

  `opts.systemPath` likewise marks the base prompt as backed by a `SYSTEM.md`
  override, making that segment openable. Same rationale: the agent receives
  an already-composed system string with no provenance attached.
- `handle` (`ExecutionHandle | null`, readonly): Live execution handle, or `null` until the first run (or `ensureExecution`) creates it.
- `hasPendingTaskNotifications` (`boolean`, readonly): `true` while at least one background-task exit is queued for delivery
  on the next `run()` (the push-fed `background:exit` queue; pending
  stall signals don't count). Exits already acknowledged by the model
  (`shell_kill`, a successful `wait_task`, reading the output file)
  never surface here.

  This is the host-side "auto-wake" signal: when it flips true while
  the agent is idle, a drain-only `run({ prompt: '' })` delivers the
  `<task-notification>` block without waiting for the user's next
  prompt — prompt-less runs are explicitly legal in that state (see
  `validateAndPrepareResume`).
- `hooks` (`AgentHookBus`): Hook bus for registering handlers on agent lifecycle events.
- `isRunning` (`boolean`, readonly): `true` while a run is in progress, from the `run()` / `resume()` call on (also while it loads its session or continues an interrupted run first).
- `killBackgroundTask` (`(taskId: string) => Promise<boolean>`): Host-side kill for a background task spawned via
  `shell({ run_in_background: true })`. Routes through the execution
  context's `killBackground` primitive, which SIGTERMs the whole
  process group and awaits the output stream's flush.

  Returns `true` when a task was found and killed (or was already
  exited — the call is idempotent on terminated tasks).
  Returns `false` when:

  - the task id doesn't exist in the context's registry, OR
  - the execution context doesn't support `killBackground`
    (some remote sandboxes), OR
  - the agent isn't bound to an execution handle yet (no run
    has ever fired).

  Distinct from the model-facing `shell_kill` tool — that one routes
  through the model's turn so the kill is visible in the transcript.
  `killBackgroundTask` is for the host's UI ("cancel this task" in
  the TUI's `ctrl+k` picker); the kill is observational from the
  model's perspective, surfacing as the usual `<task-notification>`
  on the next turn.
- `mcpConnection` (`McpConnection | null`, readonly): The live MCP connection — bound lazily by the first `run()` / `warmup()`
  (`null` before bootstrap and after `destroy()`). NON-OWNING for callers:
  wrap with `nonOwningMcpView` before handing it to child agents or tools
  — only this agent's own destroy path may tear the transports down. The
  seam that lets host-driven spawns (extension `ctx.spawnAgents`) share
  the parent's connection instead of re-bootstrapping every server per
  child (which would also strip OAuth servers of the host's provider).
- `mcpServers` (`readonly McpServerConfig[]`, readonly): The TRANSFORMED MCP config list (`mcpConfigTransforms` applied)
  the live connection is built from. Pass THIS — not the raw option —
  alongside `mcpConnection` when handing MCP state to child agents: a
  child's `mcpServers.length > 0` gate never invokes the shared
  connector if transform-added servers ride an empty raw list (mirrors
  `mcpServers` on the loop context).
- `meta` (`Readonly<Record<string, unknown>>`, readonly): Frozen view of the underlying `provider.meta`. Read-only to prevent
  accidental cross-agent contamination — writes are rejected at runtime
  (via `Object.freeze`) and at compile time (via `Readonly`). To override
  model / capability defaults, construct a new provider.
- `reset` (`() => Promise<void>`): Clear the agent's in-memory state (turns, queues, skill activations).
  Fires `skills:deactivate` with `reason: 'reset'` for each previously active
  skill. Awaiting lets host apps observe listener rejections.
  Throws while a run is in progress: call `abort()` and `await waitForIdle()` first.
- `resume` (`(options?: DurableResumeOptions) => Promise<AgentStats | null>`): Continue the session's run that a process death left unfinished, from
  its saved turns and the durable runtime's step records: finished steps
  answer from their records, a pending `safe` step runs again, a pending
  `unsafe` tool call is reported interrupted. Checkpoint runtimes only: in
  replay mode the platform re-runs the invocation (`run` with the same
  prompt) against its journal.

  Resolves `null` when there is no such run, including one that crashed
  without step checkpoints (never marked resumable): the next `run()`
  heals that one, reporting its dangling tool calls as interrupted.
  Also `null` when the run's step records no longer match its saved turns
  (a history rewrite): the run is healed at once and `resume:mismatch`
  fires.

  The resumed run keeps its id and record, and runs with the options its
  record kept (`SessionRun.options`: caps, an explicit model, thinking and
  model options), under what `options.run` passes. Per-run options the
  record does not keep (a spawned child's system prompt, depth and parent)
  come from `options.run`; unset, the agent's own apply.

  Rejects without a checkpoint-mode `durable` runtime (also when `durable`
  is unset), while a run is in progress, or when the agent has no session
  (`session` / `sessionId`).
- `run` (`(options: AgentRunOptions) => Promise<AgentStats>`): Runs the agent loop until the model stops, a limit is hit or the run is aborted. Rejects when a run is already in progress.
  With `durable: { onInterrupted: 'resume' }`, a run a dead process left in the session is continued first (`resume`);
  the result is the new run's stats, or the resumed run's when it was aborted or suspended (the prompt then does not run).
  On a top-level checkpoint run whose runtime has a lease (`DurableRuntime.lease`), rejects with `RunLeaseHeldError` when another live process holds the session's lease,
  and with `StaleEpochError` when another process takes the run over. Both are exported from `zidane/durable`.
- `session` (`Session | null`, readonly): Session bound to the agent, or `null` when none was supplied.
- `steer` (`(prompt: string | PromptPart[], options?: QueuedMessageOptions) => boolean`): Queue a mid-run nudge injected as a user turn at the next tool/turn
  boundary. Returns `false` (and queues nothing) when no run is in
  flight or when the run's `AbortController` is already tripped — a
  steer can never revive or confuse a dead run, and a message queued
  between runs would otherwise leak into the NEXT run and skip its
  first turn's tool calls. Returns `true` when queued.

  Hosts emulating Stop-hook semantics (queueing a follow-up on
  `turn:after`) should rely on this: a guard/user abort can fire mid-turn
  while `turn:after` still reports `finishReason: 'stop'`, so an
  unconditional enqueue would otherwise leak a message into a dead run.

  `prompt` takes the same values as `run({ prompt })`: text, or parts for
  text with images and documents. `options.id` comes back in
  `steer:inject`, so a host can tell which message the loop added.
  `options.systemNotes` follow the message, as `run({ systemNotes })`.
  Returns `false` for an empty prompt. Throws on a malformed part.
- `suspend` (`(options?: { graceMs?: number }) => Promise<void>`): Stops the current run so `resume` can continue it later (a host
  that drains before it exits). The model stream stops at once and its
  request is sent again on resume. Running tool calls may finish within
  `options.graceMs` (unset: they all finish); a call still running then is
  cut off and stays pending, so a resume reports it interrupted (or runs a
  `safe` one again). The run records no failed step and keeps its resumable
  mark; `run()` resolves with `suspended: true`. Without step checkpoints
  (`durable` unset, or no session) the run also stops
  with `suspended: true`, but nothing can resume it. A replay-mode run is
  aborted instead: it continues by running its invocation again.
  Before the run started (its session still loading), the run does not
  start and `run()` / `resume()` resolve `suspended: true` with zero stats.
  Resolves once the run stopped; no-op when idle.
- `toolState` (`AgentToolState`, readonly): Read-only view of this agent's committed durable tool state. See
  [`AgentToolState`](https://zidane.si/docs/reference/types.md#AgentToolState): hosts inspect what tools have persisted (flags,
  ids, counters, cursors) without a write path. Reflects only committed
  writes — a tool's staged-but-uncommitted mutations are not visible until
  the call settles.
- `turns` (`SessionTurn[]`, readonly): Conversation turns held in memory by the agent.
- `waitForIdle` (`() => Promise<void>`): Resolves when the current run has finished. Resolves immediately when the agent is idle.
- `warmup` (`() => Promise<void>`): Pre-connect MCP servers without running a turn. Idempotent and concurrency-safe:

  - No MCP servers configured → resolves immediately.
  - Connection already established → resolves immediately.
  - Another `warmup()` / `run()` is bootstrapping → awaits the in-flight promise.

  Use from host code that wants to hide MCP bootstrap latency behind other
  startup work (UI init, auth, etc.). Safe to call multiple times and from
  multiple callers concurrently.

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

### `AgentHooks`

Interface.

Event map for the agent hook bus. Each key is a hook name and each value is the handler signature. Register handlers with `agent.hooks.hook(name, fn)` or the `hooks` option of [`createAgent`](https://zidane.si/docs/reference/agent.md#createAgent).

```ts
interface AgentHooks
```

**Properties**

- `agent:abort` (`(ctx: { reason?: string; runId?: string }) => void`): The run was aborted — fires exactly once per aborted run, on every abort
  exit: a between-turn cancel (the loop's `signal.aborted` check), a
  `repeat-guard` abort, AND a cancel that lands mid-stream (which throws out
  of the turn and is caught at the run boundary). For a guard abort a
  `repeat-guard:exceeded` (`action: 'abort'`) event fired first, so consumers
  can attribute the cause.

  Does NOT fire on graceful non-cancel terminations that still finalize the
  session run as `'aborted'`: `maxConsecutivePauseTurns` exhaustion and the
  `maxCostUsd`/`maxTotalTokens` budget breaker (the latter throws
  `AgentBudgetExceededError`). Branch on those via their own signals
  (`finishReason: 'pause'`, the typed error) rather than `agent:abort`.

  `runId` — the aborted run, for correlating with `turn:after` / telemetry
  spans. `reason` — the string passed to `AbortController.abort(reason)` when
  one was supplied (mirrors `signal.reason`); absent for a bare `abort()`.
- `agent:done` (`(ctx: AgentStats) => void`): Run finished — fires on all exit paths (completion, maxTurns, abort, error).

  The `AgentStats` carried here is **cumulative** across the
  parent agent loop and every recursively-spawned sub-agent
  (`totalIn` / `totalOut` / `cost` / `totalCacheRead` / `totalCacheCreation`).
  For parent-loop-only counts use `ctx.turnUsage` (parent-only array);
  for tree-wide turn counts use `flattenTurns(ctx).length`.
- `agent:start` (`(ctx: { agentName?: string; depth: number; parentRunId?: string; providerName?: string; runId: string; startedAt: number; tracingContext?: Readonly<Record<string, string>> }) => void`): Fires once per `agent.run()` invocation, right after `session:start`
  (when a session is bound) and before the first `turn:before`. Symmetric
  with [`['agent:done']`](https://zidane.si/docs/reference/agent.md#AgentHooks).

  `tracingContext` carries opaque parent trace metadata propagated from
  the spawning agent (typically a W3C `{ traceparent, tracestate }`
  carrier injected by the parent's tracer on `spawn:before`). Tracers
  read it on the child run to continue the parent trace; absent on
  top-level runs.
- `artifact:recorded` (`(ctx: { artifact: SessionArtifact; callId: string; name: string; runId: string; turnId: string }) => void`): Fires once per artifact the session recorded from a tool result's
  reserved `metadata.artifacts` namespace (see `session/artifacts.ts`).
  Observability only — the artifact is already persisted; hosts use it
  to refresh badges / panels live. Never fires on sessionless runs or
  stores without artifact support.
- `background:adopt` (`(ctx: { command: string; cwd: string; outputPath: string; pid: number; startedAt: number; taskId: string }) => void`): A still-running background task was handed to THIS agent by its host —
  the agent was built on a host-owned handle that outlived the previous
  agent (a rebuild after a model swap, settings change, MCP login…; see
  `chat/execution-cache.ts`). The task's exit/stall callbacks now fire on
  this bus. Hosts that track running tasks register it exactly like
  `background:start`; unlike that hook it does not mean "just spawned".
- `background:exit` (`(ctx: { command: string; durationMs: number; exitCode: number; outputPath: string; signal?: Signals; status: 'exited' | 'killed'; taskId: string }) => void`): Fires exactly once per task when the child process terminates —
  natural exit, host-issued kill, or `agent.destroy()` teardown. Same
  payload the agent enqueues into `pendingTaskNotifications` for
  injection on the next turn; useful for hosts that want to render
  a "task done" toast immediately rather than wait for the model's
  next prompt.
- `background:reassign` (`(ctx: { childId?: string; command: string; fromHandleId: string; outputPath: string; pid: number; startedAt: number; taskId: string; toHandleId: string }) => void`): Ownership of a background task was transferred from one execution
  handle to another — typically the spawn tool reassigning a
  subagent's still-running tasks up to the parent's handle so they
  outlive the subagent's `destroy()`.

  Hosts that listed the task under the source handle should update
  their view to reflect the new owner. The TUI's `backgroundTasks`
  registry drops the `childId` tag on the matching entry so the
  `ctrl+k` picker shows it as parent-level (and `killBackgroundTask`
  actually finds it).

  Fires on the new owner's hook bus (the parent's, in the spawn
  case). The old owner is typically already torn down — wiring the
  event the other way would deliver it to a destroyed hookable.
- `background:reconcile` (`(ctx: { exits: TaskExitInfo[] }) => void`): Fires at the start of an `agent.run()` that has background exits to
  deliver, BEFORE they are rendered into `<task-notification>`
  blocks (runs with nothing to deliver skip the hook). `exits` merges
  the push-fed queue (`background:exit` arrivals since the last run)
  with a pull-based reconcile against
  `ExecutionContext.listBackground` — so exits land even when the
  context never calls `onExit` (remote / durable contexts can't push
  from a timer into a host journal).

  This is the journalable seam for durable-execution hosts: the hook
  runs inside the `run()` await chain, so a handler may replace
  `ctx.exits` with a recorded copy, making the wall-clock-derived
  `durationMs`, and so the whole `<task-notification>` block,
  replay-stable. A replay-mode durable runtime already records the
  `listBackground` poll as a `reconcile` step.

  Mutable: handlers may reorder, filter, or substitute entries. The
  post-hook array is what the model sees.
- `background:stall` (`(ctx: { bytesWritten: number; command: string; outputPath: string; stalledForMs: number; taskId: string }) => void`): The stall watchdog (`AgentBehavior.backgroundStallWatchdogMs`)
  detected a background task with no output for the configured window.
  One-shot per quiet period — re-arms after fresh output. The agent
  queues a `<task-stall>` notification for the model's next turn;
  hosts can additionally render a "task may be stuck" hint.
- `background:start` (`(ctx: { command: string; cwd: string; outputPath: string; pid: number; startedAt: number; taskId: string }) => void`): Fires when a background task is spawned via `ExecutionContext.execBackground`.
  Observational — telemetry consumers can mint a "task started" span.

  `outputPath` is the file the context is streaming stdout+stderr to;
  the model reads it via `read_file` on demand. `pid` is the OS pid of
  the spawned shell wrapper (a process-group leader on POSIX, so a
  `process.kill(-pid)` on cleanup tears down the whole subtree).
- `budget:exceeded` (`(ctx: { budget: number; bytes: number; turn: number; turnId: string }) => void`): Fires when a turn's total tool-output bytes exceed `behavior.toolOutputBudget`.
  Measured post-`tool:transform`. Loop injects a synthetic user message after
  the tool-results turn instructing the model to summarize.
- `cache:break` (`(info: CacheBreakInfo) => void | Promise<void>`): Fires when a turn read back materially less cached prefix than the
  previous turn established — a prompt-cache break. Emitted by
  `installCacheBreakLogger` (zidane's cache-telemetry module), so like
  `compact:*` it only fires when that subsystem is installed on this
  agent's hook bus; the loop itself never emits it.

  The payload names the request dimensions that changed (`changed`) with
  human-readable diffs (`details`). An empty `changed` means the request
  was byte-stable at this wire and the provider still dropped the prefix —
  TTL expiry, provider eviction, or (behind a format-casting gateway)
  churn introduced by the cast itself; see [`CacheBreakInfo`](https://zidane.si/docs/reference/agent.md#CacheBreakInfo) for how
  to tell them apart. Hosts surface this as UI (TUI footer chip), metrics,
  or alerts; the text `log` line remains available for grep-based
  workflows and, unlike this hook, stays silent on unattributable breaks.
- `cache:drift` (`(info: CacheDriftInfo) => void | Promise<void>`): Fires at `turn:before` when the outgoing request DIVERGES from the
  previous completed turn's — the predictive counterpart of `'cache:break'`.
  Emitted by `installCacheBreakLogger` from the request bytes alone, so it
  works on providers that report no cache usage and on format-casting
  gateways where this wire is the last observable point before the cast.

  Drift + healthy `cacheRead` afterwards = the provider absorbed the change;
  drift + a `'cache:break'` on the same turn = the break's attribution.
  Intentional rewrites (compaction cuts, stale-read stubs) drift by design —
  `divergedAt` tells them apart; see `CacheDriftInfo`.

  Delivery is fire-and-forget: `turn:before` rides the loop's PROPAGATING
  hook timeout, so the emitter does not await consumers. Synchronous
  handlers complete before the request goes out; async handlers detach and
  must not assume the turn is still in flight; errors route to the
  installer's `onEmitError`, never into the run.
- `child:artifact:recorded` (`(ctx: { artifact: SessionArtifact; callId: string; childId: string; depth: number; name: string; runId: string; turnId: string }) => void`): Bubbled from a subagent's `artifact:recorded` (shared-session children). Same payload + `childId` / `depth`.
- `child:background:exit` (`(ctx: { childId: string; command: string; depth: number; durationMs: number; exitCode: number; outputPath: string; signal?: Signals; status: 'exited' | 'killed'; taskId: string }) => void`): Bubbled from a subagent's `background:exit`. Same payload + `childId` / `depth`.
- `child:background:reassign` (`(ctx: { childId: string; command: string; depth: number; fromHandleId: string; outputPath: string; pid: number; startedAt: number; taskId: string; toHandleId: string }) => void`): Bubbled from a subagent's `background:reassign`. Fires when a
  grandchild's tasks get promoted to the child's handle on the
  grandchild's destroy — gives the parent a chance to mirror the
  ownership change in any UI it maintains.
- `child:background:start` (`(ctx: { childId: string; command: string; cwd: string; depth: number; outputPath: string; pid: number; startedAt: number; taskId: string }) => void`): Bubbled from a subagent's `background:start`. Same payload + `childId` / `depth`.
- `child:mcp:tool:gate` (`(ctx: McpToolHookContext & { block: boolean; childId: string; depth: number; reason: string; result?: string | ToolResultContent[] }) => void`): Bubbled `mcp:tool:gate` from a subagent, with `childId` and `depth`.
- `child:repeat-guard:exceeded` (`(ctx: { action: 'steer' | 'block' | 'abort'; childId: string; count: number; depth: number; threshold: number; tool: string; turnId: string }) => void`): Bubbled sibling of [`['repeat-guard:exceeded']`](https://zidane.si/docs/reference/agent.md#AgentHooks) — a subagent tripped the repeat guard.
- `child:spawn:before` (`(ctx: ChildSpawnHookContext) => void`): Nested spawn lifecycle — a SUBAGENT's `spawn:before` / `spawn:complete` /
  `spawn:error`, re-emitted on its parent's bus (and chained up to the
  root). Payload is the spawned agent's record plus `parentChildId`, the
  subagent that spawned it; see [`ChildSpawnIdentity`](https://zidane.si/docs/reference/index.md#ChildSpawnIdentity) for why there is
  no `childId`. Observational copies: `child:spawn:before` carries no
  `tracingContext` (the spawner's own tracer owns propagation). The
  root's own `spawn:*` keeps firing for direct children only.
- `child:spawn:complete` (`(ctx: ChildRunStats & ChildSpawnIdentity) => void`): Bubbled `spawn:complete` from a subagent.
- `child:spawn:error` (`(ctx: Omit<SpawnHookContext, 'tracingContext'> & ChildSpawnIdentity & { error: Error }) => void`): Bubbled `spawn:error` from a subagent.
- `child:stream:end` (`(ctx: StreamHookContext & { childId: string; depth: number; text: string }) => void`): Bubbled `stream:end` from a subagent.
- `child:stream:error` (`(ctx: StreamHookContext & { childId: string; depth: number; err: unknown }) => void`): Bubbled `stream:error` from a subagent's turn. See [`['stream:error']`](https://zidane.si/docs/reference/agent.md#AgentHooks).
- `child:stream:retry` (`(ctx: StreamHookContext & { attempt: number; childId: string; delayMs: number; depth: number; discardedText?: true; discardedThinking?: true; err: unknown; maxAttempts: number; nextAttempt: number; requestId?: string; statusCode?: number }) => void`): Bubbled [`['stream:retry']`](https://zidane.si/docs/reference/agent.md#AgentHooks) from a spawned child — lets hosts surface subagent retries.
- `child:stream:server_tool_result` (`(ctx: StreamHookContext & { childId: string; content: unknown; depth: number; toolName: string; toolUseId: string }) => void`): Bubbled `stream:server_tool_result` from a subagent. See [`['stream:server_tool_result']`](https://zidane.si/docs/reference/agent.md#AgentHooks).
- `child:stream:server_tool_use` (`(ctx: StreamHookContext & { childId: string; depth: number; id: string; input: Record<string, unknown>; name: string }) => void`): Bubbled `stream:server_tool_use` from a subagent. See [`['stream:server_tool_use']`](https://zidane.si/docs/reference/agent.md#AgentHooks).
- `child:stream:text` (`(ctx: StreamHookContext & { childId: string; delta: string; depth: number; text: string }) => void`): Bubbled `stream:text` from a subagent. See the comment above for how child events bubble.
- `child:stream:thinking` (`(ctx: StreamHookContext & { childId: string; delta: string; depth: number; thinking: string }) => void`): Bubbled `stream:thinking` from a subagent.
- `child:tool:after` (`(ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; isError?: boolean; outcome?: ToolOutcome; outputBytes: number; result: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> }) => void`): Bubbled `tool:after` from a subagent, with `childId` and `depth`.
- `child:tool:before` (`(ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; priorContent?: string; runToolCounts: Readonly<Record<string, number>> }) => void`): Bubbled `tool:before` from a subagent, with `childId` and `depth`.
- `child:tool:cancelled` (`(ctx: ToolHookContext & { childId: string; depth: number; reason: string; runToolCounts: Readonly<Record<string, number>> }) => void`): Bubbled sibling of [`['tool:cancelled']`](https://zidane.si/docs/reference/agent.md#AgentHooks) — fires when a
  subagent's in-flight tool was cancelled (either directly on the parent
  via `agent.cancelTool(callId)` reaching a child's call, or by the child
  agent's own consumers). Symmetric with `child:tool:error` and
  `child:tool:after` so the TUI's transcript renderer can paint the
  cancelled badge against the right subagent row.
- `child:tool:dispatched` (`(ctx: ToolHookContext & { childId: string; depth: number; outcome: 'execute' | 'gate-substitute' | 'gate-block' | 'unknown' | 'invalid-input'; reason?: string; runToolCounts: Readonly<Record<string, number>> }) => void`): Bubbled sibling of [`['tool:dispatched']`](https://zidane.si/docs/reference/agent.md#AgentHooks) — fires when a
  subagent dispatches a call. Symmetric with `child:tool:after` so live
  UI rendering of subagent transcripts stays paired across all five
  dispatch paths (gate-block/gate-substitute/unknown/invalid-input/execute).
- `child:tool:error` (`(ctx: ToolHookContext & { childId: string; depth: number; error: Error }) => void`): Bubbled `tool:error` from a subagent.
- `child:tool:gate` (`(ctx: ToolHookContext & ToolGateResumeFields & { block: boolean; childId: string; depth: number; metadata: ToolResultMetadata; reason: string; result?: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> }) => void`): Gate-style child events. Unlike the other `child:*` events, the bubble
  passes the **same `ctx` reference** the subagent's loop is awaiting on:
  setting `ctx.block` / `ctx.reason` / `ctx.result` on a parent listener
  propagates straight back to the child, refusing or substituting the call.

  Use these to gate subagent tool calls (native + MCP) from the parent
  without registering listeners on every child agent. The parent's own
  `tool:gate` / `mcp:tool:gate` listeners are NOT auto-shared with
  children — that would also share their budgets and dedup state.
- `child:tool:progress` (`(ctx: ToolHookContext & { childId: string; depth: number; progress: ToolProgress }) => void`): Bubbled `tool:progress` from a subagent, with `childId` and `depth`.
- `child:tool:transform` (`(ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; isError: boolean; metadata: ToolResultMetadata; outputBytes: number; result: string | ToolResultContent[] }) => void`): Same-ref bubble of a subagent's `tool:transform` hook. Parent
  listeners may mutate `ctx.result` to rewrite the child tool's
  output — used by the TUI to append per-edit annotation blocks
  onto a child agent's `multi_edit` / `edit` / `write_file`
  results when a parent-level approval gate produced a partial
  decision.
- `child:turn:after` (`(ctx: { aborted?: boolean; childId: string; depth: number; message: SessionTurn; toolCounts: { run: Readonly<Record<string, number>>; turn: Readonly<Record<string, number>> }; turn: number; turnId: string; usage: TurnUsage }) => void`): Bubbled `turn:after` from a subagent.
- `child:validation:reject` (`(ctx: ToolHookContext & { childId: string; depth: number; reason: string; schema: Record<string, unknown> }) => void`): Bubbled sibling of [`['validation:reject']`](https://zidane.si/docs/reference/agent.md#AgentHooks) — a subagent's call was rejected for invalid input.
- `compact:end` (`(ctx: CompactEndHookContext) => void`): Fires exactly once after every `compact:start`, on success and failure
  (`ctx.status`). On success it carries the replaced/dropped/restored
  counts and the post-compact `effectiveTokens` estimate so hosts can drop
  their context indicator immediately. On failure it carries `error`; an
  auto-compaction's run continues with the un-compacted history (the
  reactive overflow net still applies) and the circuit breaker counts the
  failure, while `agent.compact()` rejects with the error (or resolves
  `compacted: false` when it was aborted).
- `compact:progress` (`(ctx: CompactProgressHookContext) => void`): Fires (throttled) while the compaction summary streams — carries a
  rough cumulative output-token estimate (`estOutputTokens`, chars ÷ 4)
  so hosts can paint live progress next to the "Compacting…" state.
  Zero or more per `compact:start`; best-effort and observational.
- `compact:restore` (`(ctx: { add: (call: { input: Record<string, unknown>; name: string; output: string }) => void; session?: Session }) => void | Promise<void>`): Fires while a compaction builds the turns it re-attaches after the
  summary. `add` contributes a synthetic tool call and its result (a
  preset keeping its state across the summary: the plan as `todoread`).
  Journaled with the restoration, so a replay re-attaches the same.
- `compact:start` (`(ctx: CompactStartHookContext) => void`): Fires when loop-native auto-compaction (`behavior.autoCompact`) crosses
  its threshold, or `agent.compact()` starts (`ctx.reason: 'manual'`), and
  the summary call is about to run, BEFORE the provider round-trip.
  Distinct from `context:overflow` (the reactive, wire-only
  `context_exceeded` net) — this is the heavy compaction that summarizes
  older turns and mutates the session.

  Use it to surface a "Compacting…" banner / footer state. Observational
  — handlers cannot cancel the compaction. Always paired with exactly one
  `compact:end`.
- `content-ref:resolve` (`(ctx: { block: ResolveContentRefBlock; childId?: string; data?: string; depth?: number }) => void`): Resolves persisted content refs into provider-bound payload bytes.

  Fires only when wire assembly sees a block carrying `ref`. Handlers set
  `ctx.data` to the resolved payload. The bytes are wire-only and are never
  written back to `session.turns`.
- `context:overflow` (`(ctx: StreamHookContext & { attempt: number; err: unknown }) => void`): Fires when the loop catches a `context_exceeded` provider error and, before
  giving up, performs an emergency in-loop compaction (aggressive tail
  elision of older `tool_result` bodies) and re-issues `provider.stream()`.
  Does NOT fire when compaction can't shrink the request any further — that
  lands on `stream:error` (the terminal failure).

  Runs independently of `behavior.compactStrategy`: the recovery calls the
  tail compactor directly, so it protects runs that never opted into
  client-side compaction. It can only shrink tool-result-heavy histories — a
  request whose mandatory text alone overflows still surfaces the error.

  - `attempt` — 1-indexed recovery pass (each pass keeps fewer trailing turns
    intact and elides more; bounded).
  - `err` — the raw `context_exceeded` error that triggered recovery.

  Observational — handlers cannot cancel the recovery.
- `context:transform` (`(ctx: { messages: SessionMessage[] }) => void`): Fires per request with the session messages about to be sent. Mutate `ctx.messages` to rewrite the context for this turn.
- `decision:end` (`(ctx: DecisionEndHookContext) => void`): Fires once per decision, success or failure, after `decision:start`.
  `ok: false` carries the failure `reason`; `ok: true` carries `answers`
  and `usage` (decision usage is NOT folded into run totals).
- `decision:start` (`(ctx: DecisionStartHookContext) => void`): Fires when `agent.decide` / `ctx.decide` starts a decision, before
  validation and the provider call. `provider` / `model` are absent when no
  decision provider could be resolved. Contexts of `decision:*` hooks carry
  no `decide`, so a decision can't recursively trigger another.
- `execution:ready` (`(ctx: { handle: ExecutionHandle; spawned: boolean }) => void`): Fires exactly once per agent, the first time the execution handle is bound
  — right after the lazy `execution.spawn()` (or immediately on the first run
  when a handle was injected via `AgentOptions.handle`). `spawned` is
  `true` when the agent minted the handle, `false` for an injected one.

  The seam durable hosts use to record the minted handle id (e.g. a sandbox
  id) for re-attach on replay/resume — journal it here, then re-attach via
  `AgentOptions.handle` + the provider's reconnect option on the next
  invocation. Observational; the handle is not mutable.
- `history:repair` (`(ctx: HistoryRepairHookContext) => void`): Fires after a provider stream settles when the loop scrubbed
  provider-rejected reasoning state from canonical history (e.g.
  Anthropic signature-400 poisoning). In-memory turns and the bound
  session are already repaired; hosts mirroring turns into an external
  durable store must heal it using `ctx.turnIds` or the next reload
  re-poisons the run. Observational. See `HistoryRepairHookContext`.
- `mcp:auth:error` (`(ctx: { error: Error; name: string }) => void`): Interactive login failed (browser error, abort, exchange failure).
- `mcp:auth:required` (`(ctx: { name: string; reason: 'no-tokens' | 'auto-promoted'; transport: 'sse' | 'stdio' | 'streamable-http' }) => void`): Fires when a server's bootstrap was skipped because it requires OAuth
  login and no tokens are stored. The host (TUI) surfaces this as a
  "needs-auth" status so the user can opt in to interactive login via
  `loginMcpServer`. Pairs with `mcp:bootstrap:end ok:false` — both fire,
  since the bootstrap did fail (just for a recoverable reason).

  `reason`:

  - `'no-tokens'` — config has `auth: 'oauth'` set (or normalized from
    Cursor's `authMethod: 'mcpOAuth'`) and the credential store is empty.
  - `'auto-promoted'` — server returned 401 + RFC 9728 metadata. No
    static Authorization header was set, so the server is eligible for
    OAuth login even though the config didn't explicitly request it.
- `mcp:auth:success` (`(ctx: { name: string }) => void`): Interactive login completed and tokens were persisted.
- `mcp:auth:url` (`(ctx: { name: string; url: string }) => void`): Fires during an INTERACTIVE OAuth login flow — when the SDK wants the
  user agent to navigate to the authorization URL. Hosts open a browser
  and surface the URL in the TUI (in case the browser auto-open fails).
  Distinct from `mcp:auth:required` (which fires from bootstrap when
  tokens are missing); this only fires from `loginMcpServer`.
- `mcp:bootstrap:end` (`(ctx: { durationMs: number; name: string; transport: string } & ({ cached?: boolean; lazy?: boolean; ok: true; toolCount: number; warnings?: string[] } | { error: Error; ok: false })) => void`): Fires at the end of a per-server bootstrap attempt. `durationMs` spans from
  the matching `mcp:bootstrap:start`. On `ok: false` carries the originating
  error so consumers can log / trace without relying on a separate `mcp:error`.

  `lazy: true` on the success arm signals the server registered its tools
  without opening a connection (see `McpServerConfig.lazyConnect`); the
  connect cost is deferred to the first `tools/call`. `cached: true` signals
  the bootstrap skipped `tools/list` because the host provided
  `cachedTools` — orthogonal to `lazy` (a server can be cached and eager,
  cached and lazy, or neither).

  `warnings` (success arm only) collects soft signals that surfaced during
  bootstrap without aborting it — currently just "server rejected
  `notifications/initialized` with 4xx" from the tolerant client. Hosts
  can render a "connected with caveats" badge from this; consumers that
  don't care can ignore the field.
- `mcp:bootstrap:settled` (`(ctx: { results: readonly ({ durationMs: number; name: string; transport: string } & ({ lazy?: boolean; ok: true; toolCount: number } | { error?: Error; ok: false; skipped?: boolean }))[] }) => void`): Fires exactly ONCE per bootstrap batch, after every server's attempt
  has settled. `results` is sorted by server name — a deterministic
  aggregate, unlike the per-server `mcp:bootstrap:end` events which
  fire in network-completion order inside the parallel bootstrap.
  Durable-execution hosts that journal bootstrap outcomes should hook
  this instead of buffering + sorting the per-server stream.
- `mcp:bootstrap:start` (`(ctx: { name: string; transport: string }) => void`): Fires at the start of a per-server bootstrap attempt, before any network I/O.
  Pairs with `mcp:bootstrap:end` and is always emitted, regardless of outcome.
- `mcp:close` (`(ctx: { name: string }) => void`): Fires when an MCP server connection is closed.
- `mcp:connect` (`(ctx: { lazy?: boolean; name: string; tools: string[]; transport: string }) => void`): Fires once a server's client is live (transport connected, SDK handshake
  complete). For servers bootstrapped eagerly, fires right after bootstrap;
  for servers with `lazyConnect: true`, fires after the first `tools/call`
  triggers the deferred connect — `lazy: true` lets the host distinguish.
- `mcp:error` (`(ctx: { error: Error; name: string }) => void`): Fires when an MCP server fails to connect or errors fatally.
- `mcp:tool:after` (`(ctx: McpToolHookContext & { outputBytes: number; result: string | ToolResultContent[] }) => void`): Fires after an MCP tool call returns, with the result.
- `mcp:tool:before` (`(ctx: McpToolHookContext) => void`): Fires before an MCP tool call is sent to the server.
- `mcp:tool:error` (`(ctx: McpToolHookContext & { error: Error }) => void`): Fires when an MCP tool call throws.
- `mcp:tool:gate` (`(ctx: McpToolHookContext & { block: boolean; reason: string; result?: string | ToolResultContent[] }) => void`): MCP-side counterpart of `tool:gate`. Same shape: set `block` to refuse,
  set `result` to substitute a successful payload and skip the upstream
  MCP `callTool`. When both are set across the handler chain, `block` wins.

  Fires INSIDE the MCP wrapper's `execute`, after the loop's `tool:gate`
  already ran. Does **not** carry `runToolCounts` — those are loop-level
  and already exposed on `tool:gate` for MCP tools (which are registered
  as agent tools under their namespaced name `mcp_<server>_<tool>`). Use
  `tool:gate` for budget / dedup logic; reserve `mcp:tool:gate` for
  MCP-specific concerns (per-server routing, transport-aware refusals).
- `mcp:tool:transform` (`(ctx: McpToolHookContext & { outputBytes: number; result: string | ToolResultContent[] }) => void`): Fires before the MCP tool result is returned. Mutate `ctx.result` to rewrite it. `outputBytes` is the size before any mutation.
- `mcp:tools:filter` (`(ctx: { server: string; tools: { description?: string | null; inputSchema?: unknown; name: string }[]; transport: 'sse' | 'stdio' | 'streamable-http' }) => void`): Fires once per server after `listTools()` and after the config-side filters
  (`enabledTools` / `disabledTools` / `toolFilter`) have applied, but BEFORE
  tools are registered. Handlers may mutate `ctx.tools` in place — splicing,
  reordering, or replacing entries — to further narrow what the model sees.

  Composes with config-side filters: config drops tools the host's static
  policy excludes; this hook is the runtime escape hatch for per-user, per-
  environment, or capability-driven decisions that the config can't express.

  Items are upstream tool descriptors (NOT yet namespaced as `mcp_<server>_<tool>`).
- `mcp:tools:list` (`(ctx: { name: string; tools: readonly McpToolSchema[]; transport: 'sse' | 'stdio' | 'streamable-http' }) => void`): Fires once per successful bootstrap with the **raw, pre-filter** tool
  schemas the server advertised via `tools/list` (or that the host
  pre-seeded via `McpServerConfig.cachedTools`). Useful for hosts that
  want to:

  - Persist a per-server tool catalog to disk so a settings UI can
    show every available tool — including ones currently hidden by
    `disabledTools` — without re-bootstrapping.
  - Build per-tool toggle UIs that reflect the full server surface.
  - Diff successive bootstraps to detect newly-added / removed tools.

  Read-only — handlers must NOT mutate `tools`. Shape the model-facing
  list via `mcp:tools:filter` instead (which fires immediately after
  this hook and IS the mutation surface).

  Does NOT fire when the bootstrap fails. Does NOT fire on lazy-connect
  servers' deferred first-call (the schemas were already known from
  `cachedTools` at the agent-construction boundary).
- `mcp:warn` (`(ctx: { message: string; name: string }) => void`): Diagnostic line from a server. For fatal failures hook `mcp:error`.
- `model:fallback` (`(ctx: StreamHookContext & { discardedThinking?: true; err: Error; from: { model: string; provider: string }; targetIndex: number; to: { model: string; provider: string } }) => void`): Fires when a stream fails terminally on the active provider+model target
  and the loop advances to the next entry of the run's fallback chain
  (`AgentRunOptions.model` array / multi-provider fan-out) instead of
  surfacing the error. Fires once per advancement; the terminal failure of
  the LAST target still lands on `stream:error` as usual.

  Fallback is run-sticky: after this fires, the rest of the run stays on
  `to` (no ping-pong back to the primary); the next `run()` starts from the
  primary target again.

  - `from` / `to` — provider name + model id of the exhausted and newly
    active targets.
  - `targetIndex` — 0-based index of `to` in the resolved target list.
  - `err` — the classified error that exhausted `from` (an
    `AgentProviderError` whose `limit` says when it was a rate limit, spent
    quota or overload); the raw provider error is its `cause`.
  - `discardedThinking` — `true` when the failed attempt streamed
    reasoning before it failed; same meaning as on `stream:retry`.

  Observational — handlers cannot cancel the fallback. To disable fallback,
  configure a single provider+model target.

  Attribution note: `agent:start`'s `providerName` comes from the PRIMARY
  target and is not updated when a fallback advances — this hook (and
  per-turn `TurnUsage.modelId`) is the source of
  truth for which target actually served a given stretch of the run.
- `oauth:refresh` (`(ctx: OAuthRefreshHookContext) => void`): Fires when the provider refreshes its OAuth credentials during a run.
- `output` (`(ctx: { output: Record<string, unknown>; schema: Record<string, unknown> }) => void`): Fires with the structured output the model produced and the schema it was validated against.
- `pairing:repair` (`(ctx: PairingRepair & { turnId?: string }) => void`): Fires once per repair performed by the pre-send pairing pass
  ([`ensureToolResultPairing`](https://zidane.si/docs/reference/index.md#ensureToolResultPairing)). The pass runs immediately before every
  provider call and patches over the wire-level corruption modes that
  Anthropic / OpenAI 400 on (orphan `tool_use` / `tool_result`, duplicate
  ids, compaction-stranded blocks).

  Observational — handlers can mirror to Sentry/PostHog/etc. for
  postmortems on new corruption sources but cannot suppress the repair.
  The companion `behavior.strictToolPairing` flag is the consumer escape
  hatch for "throw instead of repair" (training-data collectors).

  `turnId` lets handlers correlate a repair burst with the run-loop turn
  that observed it. Absent on repairs detected from the synthetic
  fallback path in `agent.run()`'s error catch (the orphan-tool_use
  fallback for crash recovery), where no turn is in flight.
- `provider:diagnostic` (`(ctx: ProviderDiagnostic & { runId: string; turnId: string }) => void`): Fires when the provider reports a diagnostic event for a turn.
- `provider:input-transformations` (`(ctx: InputTransformationsHookContext) => void`): Fires when the provider dropped replayed thinking blocks server-side
  before sampling (Anthropic preserved-thinking binding, reported via
  `input_transformations`). Nothing was mutated on our side — contrast
  `'history:repair'`. A `prefix_binding_mismatch` entry means this host
  rewrote system / tools / an earlier message since the block was
  produced: the same edit `'cache:break'` attributes, now also costing the
  model its reasoning. Observational. See `InputTransformationsHookContext`.
- `repeat-guard:exceeded` (`(ctx: { action: 'steer' | 'block' | 'abort'; count: number; threshold: number; tool: string; turnId: string }) => void`): Fires when the consecutive-identical `AgentBehavior.repeatGuard`
  escalates on a tracked tool. In chain mode, `count` is the number of
  back-to-back identical calls including the offending one; in window mode it
  is the number of occurrences inside the configured window. `action`
  reflects the escalation: `'block'` refused this call via `tool:gate`;
  `'steer'` let it execute and appended a steering message to the result;
  `'abort'` additionally aborts the run via the agent's `AbortController`
  (the loop then fires `agent:abort`). The `'abort'` event fires BEFORE the
  abort so consumers can distinguish a guard abort from a user-driven cancel.
- `replay:done` (`(ctx: Record<string, never>) => void`): Durable-execution replay finished — the invocation transitioned from
  journal replay to live execution (the first non-journaled provider
  stream or tool body just started).

  The framework core never fires this: it is emitted by durable-runtime
  adapters (see `createReplayTracker({ hooks })` in `zidane/restate`) once
  per handler invocation. Hosts that would otherwise infer liveness
  from side channels (counting live provider calls, watching stream
  hooks) should key on this instead. Fires at most once per invocation;
  never fires when the journal replays to completion without going
  live (nothing left to execute — and nothing left to abort).
- `resume:mismatch` (`(ctx: { reason: string; resumeTurn: number; runId: string; sessionId: string; storedTurns: readonly number[] }) => void`): `Agent.resume` refused a run whose stored step records are of another
  loop turn than the one its saved turns resume at (a history rewrite):
  asking for keys of the wrong turn would miss a pending unsafe call and
  run it again. Also fires when the run's saved turns leave a tool call
  unanswered before their last turn (they were rewritten after the loop
  saved them). The run is no longer resumable: its records are dropped,
  its dangling tool calls are reported interrupted (a spawned child's are
  left to the next top-level run), and `resume()` resolves `null`.
- `run:beforeEnd` (`(ctx: { followUp: (message: string) => void; runId: string; session?: Session; turn: number }) => void | Promise<void>`): Fires once per would-be final turn: the model answered with no tool call
  and nothing is queued. A handler that calls `followUp(message)` keeps the
  run going with that message as a user turn (the first message wins).
  Journaled as a loop decision, so a replay takes the same branch. Not
  fired after an abort or an error.
- `session:end` (`(ctx: SessionHookContext & { runId: string; session?: Session; status: SessionEndStatus; turnRange: [number, number] }) => void`): Fires when a run ends, with its status and the range of turn indexes it produced. `session` is the run's session (absent on hand-built calls).
- `session:meta` (`(ctx: SessionHookContext & { key: string; value: unknown }) => void`): Fires when a session metadata key is set.
- `session:save` (`(ctx: SessionHookContext) => void`): Fires when the session is saved.
- `session:start` (`(ctx: SessionHookContext & { prompt: string; runId: string }) => void`): Fires when a run starts on a session, with the run id and prompt label.
- `session:turns` (`(ctx: SessionHookContext & { count: number; turns: SessionTurn[] }) => void`): Fires when new turns are flushed to the session, with the turns and the resulting turn count.
- `skills:activate` (`(ctx: { skill: SkillConfig; via: ActivationVia }) => void`): Fires when a skill is activated. `via` says what triggered it.
- `skills:catalog` (`(ctx: { catalog: string; skills: SkillConfig[] }) => void`): Fires with the rendered skills catalog text and the skills it lists.
- `skills:deactivate` (`(ctx: { reason: DeactivationReason; skill: SkillConfig }) => void`): Fires when a skill is deactivated. `reason` says why.
- `skills:resolve` (`(ctx: { skills: SkillConfig[] }) => void`): Fires once skills have been resolved, with the resolved skill list.
- `spawn:before` (`(ctx: SpawnHookContext) => void`): Fires when a spawned subagent is about to start.
- `spawn:complete` (`(ctx: ChildRunStats) => void`): Fires when a spawned subagent finishes, with its run stats.
- `spawn:error` (`(ctx: SpawnHookContext & { error: Error }) => void`): Fires when a spawned subagent fails.
- `spawn:queued` (`(ctx: SpawnQueuedContext) => void`): A top-level `spawn` call found every slot taken and waits for one (it
  starts, with `spawn:before`, once a running child finishes). Never fires
  for a subagent's spawn: that one is refused at the cap instead.
- `steer:inject` (`(ctx: { content: readonly PromptPart[]; id?: string; text: string }) => void`): Fires when the loop adds a queued message to the conversation as a user
  turn: an `Agent.steer` or `Agent.followUp` message, or a
  message of the agent's own (a nudge, a warning). `id` is the id that the
  caller gave to `steer` or `followUp`. `text` joins the text parts.
- `stream:end` (`(ctx: StreamHookContext & { text: string }) => void`): Fires when the model's text stream for the turn ends, with the full text.
- `stream:error` (`(ctx: StreamHookContext & { err: unknown; requestId?: string; statusCode?: number }) => void`): Fires when the provider's stream rejects BEFORE `stream:end` — provider
  errors, network blips, malformed `tools` payloads (400 invalid_request_error).

  `err` is the original thrown value, not the typed `AgentProviderError`
  the loop subsequently constructs; subscribe here to capture the native
  SDK exception (status codes, request ids) for telemetry without having
  to walk `cause` chains. Observational — the loop still wraps + throws
  after this hook resolves, so handlers cannot suppress the failure.

  Best-effort convenience fields extracted from common SDK shapes
  (Anthropic, OpenAI, OpenRouter, Cerebras) when present on `err`:

  - `statusCode` — HTTP status (e.g. 400, 429, 500, 502, 529)
  - `requestId` — provider request id for grep-the-logs correlation

  Both absent when the SDK doesn't expose them (network/early rejects,
  stream-internal protocol errors). Walk `err.cause` chains yourself for
  deeper diagnostics.

  Does NOT fire on user-initiated aborts; those land on `agent:abort`
  instead. Use `err instanceof Error && err.name === 'AbortError'` to
  distinguish if you also subscribe here for raw error logging.
- `stream:progress` (`(ctx: StreamProgressHookContext) => void`): Fires (throttled, \~4/s) while the parent turn streams and once more,
  unthrottled, right before `stream:end`. Carries the tokens streamed so far
  (provider-counted when the wire reports mid-stream usage, else chars ÷ 4)
  and a running USD estimate for the turn. Hosts paint live counters from
  it; `turn:after` settles the figure. Not bubbled for subagents — their
  spend lands at `child:turn:after`. Observational and best-effort.
- `stream:retry` (`(ctx: StreamHookContext & { attempt: number; delayMs: number; discardedText?: true; discardedThinking?: true; err: unknown; maxAttempts: number; nextAttempt: number; requestId?: string; statusCode?: number }) => void`): Fires when the loop catches a `retryable` provider error and is about
  to sleep before re-issuing `provider.stream()`. Does NOT fire on the
  terminal failure (the attempt the loop gives up on) — that lands on
  `stream:error` instead.

  Use this hook to surface "Anthropic overloaded — retrying in Ns" in
  the TUI, count retries in observability, or short-circuit further
  retry logic from an outer harness.

  - `attempt` — 1-indexed number of the attempt that just failed, counted
    in its retry budget: transport failures (a dropped socket) have their
    own, longer one, separate from `behavior.retry`.
  - `nextAttempt` — `attempt + 1`, the attempt that will run after the sleep.
  - `delayMs` — the wait the loop is about to take. Already accounts for
    any `retry-after` / `retry-after-ms` header the server returned and
    the configured `maxDelayMs` cap.
  - `err` — the raw thrown value (same as `stream:error.err`).
  - `statusCode` / `requestId` — extracted via the same helper as
    `stream:error` when available.

  Observational — handlers cannot cancel the retry. To disable
  provider-classified retries, set `behavior.retry.maxAttempts: 1`.
- `stream:server_tool_result` (`(ctx: StreamHookContext & { content: unknown; toolName: string; toolUseId: string }) => void`): Pairs with [`['stream:server_tool_use']`](https://zidane.si/docs/reference/agent.md#AgentHooks) — fires when the
  provider closes the matching server-tool-result block. `content` is the
  raw block payload (either an error object or an array of result entries
  carrying `encrypted_content` / `encrypted_index`); consumers format it
  for display.
- `stream:server_tool_use` (`(ctx: StreamHookContext & { id: string; input: Record<string, unknown>; name: string }) => void`): Fires mid-stream when a provider that executes a tool server-side
  (currently: Anthropic's `web_search_20250305`) closes the
  server-tool-invocation content block. Symmetric with `tool:before` for
  function tools — surface the call into the transcript at the moment the
  model issued it, without waiting for the assistant message to finalize.

  Shape is intentionally generic so future server tools reuse the same
  hook. `id` is the block's tool-use id (echoed back to the provider on
  follow-up turns); pair with `stream:server_tool_result` via that id.

  Observational — handlers cannot block or substitute, since the call
  has already executed server-side by the time we see it.
- `stream:start` (`(ctx: StreamHookContext & { startedAt: number }) => void`): Fires immediately before the loop calls `provider.stream()`. Pairs with
  `stream:end` / `stream:error`; lets observers timestamp the moment the
  request leaves the client to compute wait-vs-generate split and TTFT
  relative to provider dispatch (not run start).

  `startedAt` is `Date.now()` at fire time. Observational — handlers
  cannot mutate the in-flight `StreamOptions` (use `turn:before` /
  `context:transform` / `system:transform` for that).
- `stream:text` (`(ctx: StreamHookContext & { delta: string; text: string }) => void`): Fires for each text delta of the model response. `delta` is the new chunk and `text` the accumulated text.
- `stream:thinking` (`(ctx: StreamHookContext & { delta: string; thinking: string }) => void`): Fires for each thinking delta. `delta` is the new chunk and `thinking` the accumulated thinking text.
- `system:before` (`(ctx: { system: string }) => void`): Fires once per run, before the first turn, only when `AgentRunOptions.system` is set. `ctx.system`
  is that run-level string. Observational: the agent does not read `ctx.system` back, so use
  `system:transform` to rewrite the system prompt.
- `system:transform` (`(ctx: { messages: readonly SessionMessage[]; session?: Session; system: string; turn: number; turnId: string }) => void`): Fires per request, after `context:transform` and before the request goes
  out. Mutating `ctx.system` updates the system prompt the provider sends
  for this turn.

  ## Anything written here must be byte-stable across turns

  The system prompt sits ABOVE the whole conversation in the provider's cache
  prefix — Anthropic hashes tools → system → messages, cumulatively — so a
  one-byte change to `ctx.system` invalidates every message-level breakpoint
  and re-bills the entire transcript at the cache-write rate. Only `tools` and
  the static half of the system prompt survive, which is what makes this hard
  to spot: `cacheRead` stays large and non-zero (it becomes a CONSTANT equal to
  the scaffold) while cost climbs.

  The classic way to get this wrong is a live tally: interpolating something
  like `You have already read ${n} files…` re-renders the prompt on most turns
  and quietly re-bills the transcript on each one.

  Constant guidance strings, feature flags and run-stable facts are fine here.
  Live counters, timestamps, elapsed times, per-turn indices and anything
  derived from a growing tally are NOT — not even in the dynamic half below
  `SYSTEM_PROMPT_BOUNDARY`, which carries no `cache_control` of its own but is
  still inside the prefix that every message breakpoint hashes.

  Genuinely per-turn state belongs in the transcript, persisted as immutable
  history so it cannot shift under the prefix on a later turn. A wire-only
  append to the newest message does NOT work either: that message re-renders
  without the append next turn and the prefix diverges there instead. Install
  [`installCacheBreakLogger`](https://zidane.si/docs/reference/agent.md#installCacheBreakLogger) while developing anything that writes here.

  `messages` is read-only here; use `context:transform` for message
  surgery. `session` is `undefined` when the run is sessionless.
- `tool-batch:resumed` (`(ctx: { done: readonly string[]; resuming: readonly string[]; runId: string; turn: number; turnId: string }) => void`): Fires when a durable run resumes a tool batch a previous process left
  unfinished, before any call of it runs again. `done` calls were rebuilt
  from their step records (show them finished); `resuming` calls run
  again or are reported interrupted. Optional for hosts: one that ignores
  it shows the run as running and fills in the calls when the batch ends.

  Checkpoint runtimes only: in replay mode the loop cannot tell which calls
  are done before it issues them, and the hook would fire on every replay.
- `tool-budget:exceeded` (`(ctx: { count: number; max: number; mode: 'steer' | 'block'; tool: string; turnId: string }) => void`): Fires when a per-tool budget configured via `behavior.toolBudgets` is
  exceeded for a specific tool. `mode` reflects how the framework reacted:
  `'steer'` lets the call run and queues a post-turn nudge; `'block'`
  refuses the call outright with `Blocked: <message>`.

  `count` is the run-cumulative dispatched count just before this call.
  Use `turnId` to correlate with `turn:after` if you need the integer turn
  index. Distinct from `budget:exceeded` (byte-level) so consumers can
  subscribe specifically; both can fire in the same turn.
- `tool-results:after` (`(ctx: { message: SessionTurn; results: readonly ToolResult[]; turn: number; turnId: string }) => void`): Fires after a tool-results user turn is pushed onto the conversation,
  before any byte-budget enforcement or follow-up steering. Symmetric
  with `turn:after` but for the user-role tool_result turn that closes the
  round-trip.

  Why it exists: persistence layers must write tool_use/tool_result turn
  pairs together — if only the assistant turn (carrying tool_use blocks)
  is durable, a process death between turns leaves the DB with an orphan
  tool_use that Anthropic rejects on resume. Subscribe here to persist
  the tool-results turn before the loop continues.
- `tool-state:commit-failed` (`(ctx: { callId: string; error: Error; handled?: boolean; ops: readonly ToolStateOp[]; runId: string; toolName: string }) => void`): Fires when a settled tool call's durable state (`ToolContext.state`) could
  NOT be committed — the backing `AgentOptions.toolState` store
  rejected the write.

  The call itself is unaffected: the tool body already ran and its result is
  already on its way to the model, so failing the run over a bookkeeping
  write would be worse than losing it. But losing it silently is worse
  still — a dropped one-shot flag re-arms the exact bug durable tool state
  exists to prevent — so this is the seam for alerting on it. `ops` is the
  delta that did NOT land, in the order the tool staged it; replaying it by
  hand is a legitimate recovery.

  Without a handler (or with one that leaves `handled` unset) the loop logs
  to stderr. Set `handled = true` when your handler owns the reporting and
  you don't want the duplicate log line.
- `tool:after` (`(ctx: ToolHookContext & { coercions?: readonly string[]; isError: boolean; outcome?: ToolOutcome; outputBytes: number; result: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> }) => void`): Fires after a tool body produced a result (the execute path) or a
  `tool:gate` listener substituted one via `ctx.result` (the
  cache-substitute path). Carries the post-`tool:transform` payload —
  what the model actually sees on the wire.

  Does NOT fire on gate-block / unknown-tool / validation-reject
  paths: the loop synthesizes their `tool_result` text inline and
  sends it back to the model, but no tool body produced output and
  no `tool:transform` ran. Consumers wanting symmetric per-call
  notification across every dispatch path should listen to
  `tool:dispatched` instead (which fires once per call regardless of
  path, with an `outcome` discriminator).

  This narrow contract is deliberate: tracing spans opened in
  `tool:before` only get closed where they were opened, byte budgets
  only count real tool output, and chat-layer transcript renderers
  keep their `tool` ↔ `tool-result` pairing without special-casing
  the synthesized paths.
- `tool:before` (`(ctx: ToolHookContext & { coercions?: readonly string[]; priorContent?: string; runToolCounts: Readonly<Record<string, number>>; unknown?: boolean }) => void`): Fires before a tool body runs. Mutate `ctx.input` to rewrite the arguments the body sees.
- `tool:cancelled` (`(ctx: ToolHookContext & { reason: string; runToolCounts: Readonly<Record<string, number>> }) => void`): Fires when a single in-flight tool call is cancelled mid-flight via
  `agent.cancelTool(callId)` — typically the TUI's "cancel this tool"
  affordance. Distinguished from `tool:error` (the tool body threw),
  `agent:abort` (whole run aborted), and the batch-layer
  `INTERRUPT_MESSAGE_FOR_TOOL_USE` shape (sibling drain on full abort)
  so consumers can split the four causes.

  Semantics:

  - The tool's body has already received an abort signal (via the
    combined per-call + run signal); whether it actually unwound is
    up to the body's signal awareness.
  - The framework returns the canonical
    [`TOOL_USE_CANCELLED_MESSAGE`](https://zidane.si/docs/reference/index.md#TOOL_USE_CANCELLED_MESSAGE) to the model with
    `isError: true` so the assistant turn closes cleanly and the
    model can continue with the next batch call.
  - `tool:after` and `tool:transform` do **not** fire for a cancelled
    call — there's no real result the loop should account for.

  `runToolCounts` mirrors the `tool:gate` / `tool:before` snapshot for
  this call (frozen, pre-increment) so a single per-call observer can
  subscribe to either hook without re-walking the run.
- `tool:dispatched` (`(ctx: ToolHookContext & { outcome: 'execute' | 'gate-substitute' | 'gate-block' | 'unknown' | 'invalid-input'; reason?: string; runToolCounts: Readonly<Record<string, number>> }) => void`): Symmetric notification fired ONCE per call the model dispatched,
  regardless of the path the call ultimately took through the loop.
  Every call that produces a tool_result closes with exactly one
  terminal event, so pair `tool:dispatched` with that terminal to
  reconstruct message history / close per-call spans:

  - `outcome: 'execute'` → closes with `tool:after` OR, if the call was
    cancelled in-flight via `agent.cancelTool`, with `tool:cancelled`
    (NOT `tool:after`). A span keyed on `dispatched(execute)→after`
    will leak on cancel — also listen for `tool:cancelled`.
  - `outcome: 'gate-substitute'` → closes with `tool:after`.
  - `outcome: 'gate-block' | 'invalid-input'` → no `tool:after` (the
    body never ran); the dispatch event itself carries the result.
  - `outcome: 'unknown'` → no `tool:after`; closes after
    `tool:unknown` (+ optional `tool:error`).
    The strict direction always holds: no `tool:after` fires without a
    preceding `tool:dispatched`.

  Distinct from `tool:before`: `tool:before` fires when a tool's body
  is about to execute (the "I'm running this tool" notification)
  OR — as a special case — once with `unknown: true` for hallucinated
  tool names, so UI consumers rendering tool cards still see the
  model's attempt. It still skips gate-block / gate-substitute /
  validation-reject paths. `tool:dispatched` fires on all five paths
  with an `outcome` discriminator so live consumers never see a
  `tool:after` whose matching dispatch event was suppressed.

  Strict ordering: `tool:dispatched` fires AFTER `tool:gate` /
  `validation:reject` / `tool:unknown` so the resolved `outcome` is
  always accurate. Fires BEFORE `tool:before` on the execute path so
  consumers binding both events see dispatched-then-before.

  `reason` is set only when `outcome === 'gate-block'` and carries the
  gate's refusal text (so consumers can render denied-diff badges, etc.
  without a side channel).
- `tool:error` (`(ctx: ToolHookContext & { error: Error; result?: string | ToolResultContent[] }) => void`): Fires when a tool throws during execution. Mutate `result` to substitute a
  tool-output payload that gets sent back to the model in place of the
  default `Tool error: <msg>` string — useful for OSS-model error rewriting
  (collapse stack traces, hide internal paths, prepend recovery hints).

  The post-hook value flows through `tool:transform` like a normal output, so
  downstream byte-budgeting and image-stripping still apply.
- `tool:gate` (`(ctx: ToolHookContext & ToolGateResumeFields & { block: boolean; metadata: ToolResultMetadata; reason: string; result?: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> }) => void`): Fires before validation, `tool:before`, and `execute`. Two ways to
  intercept:

  - Set `block = true` (with a `reason`) to refuse the call. The model
    sees a `tool_result` block with `content` = `Blocked: <reason>` and
    `is_error: true`. `tool:before` / `tool:after` do **not** fire;
    `tool:dispatched` does (with `outcome: 'gate-block'`) so trace /
    audit consumers see the refusal. The exact wire payload is:

    ```jsonc
    {
      "type": "tool_result",
      "tool_use_id": "<call id>",
      "content": "Blocked: <reason>",   // verbatim, no wrapping XML
      "is_error": true
    }
    ```

    The `Blocked: ` prefix is fixed (single space, no colon variations);
    the `<reason>` is whatever string the gate handler assigned to
    `ctx.reason` (defaults to `'Tool execution was blocked'` if no
    handler set it). Pinned by `test/per-run-hooks.test.ts` so SDK
    consumers can safely string-match for telemetry / eval scoring.
  - Set `result` to substitute a successful tool_result and skip
    execution. The model sees the substitute as a normal tool_result;
    `tool:before` does not fire, but `tool:after` and `tool:transform`
    do — so byte budgets, telemetry, and post-mutation hooks see the
    substitute. Useful for cache hits, dedup, idempotency guards,
    plan-mode synthetic acks.

  If multiple handlers along the chain set both `block` and `result`,
  `block` wins — refusal beats substitution, so a policy gate
  (skills allow-list, custom security) can always override an upstream
  consumer's cache substitute. Mirrors the writable-`result` shape on
  `tool:unknown` and `tool:error` so consumers learn one pattern.

  `runToolCounts` — frozen pre-call snapshot of per-tool dispatched
  counts in this run. Use it to self-throttle, drive observability, or
  implement budget guards. Counts every call that passed gate, including
  dedup substitutes; excludes `block`ed calls.

  Concurrent fleets see the same pre-batch snapshot on every member's
  `tool:gate`; the built-in budget / dedup middleware reserves per-call
  atomically so `behavior.toolBudgets` enforces correctly even when
  multiple `tool:gate` callbacks fire in the same microtask.

  Gate a tool call before it runs. Handlers share ONE mutable `ctx` and run
  sequentially in registration order (the order they were `hook()`-ed). The
  framework reads the final state once all handlers (including async ones,
  which are awaited) have run. Built-in behavior gates install during run
  setup and self-defer when another gate already set `block` / `result`.

  Precedence across the resolved state:

  - `block` beats `result`: if any handler left `block === true`, the call
    is refused (`Blocked: <reason>`, `isError: true`) and any `result`
    substitute is dropped. Security/refusal beats convenience/substitution.
  - Among handlers, it is effectively last-writer-wins per field (each just
    mutates the shared `ctx`). Cooperative built-ins self-defer — e.g.
    `repeat-guard` early-returns when `ctx.block || ctx.result` is already
    set, so it never double-penalizes a call another guard already broke.
    Host handlers composing with the built-ins should do the same
    (check `ctx.block`/`ctx.result` before acting) to keep precedence
    predictable.

  Set `block = true` (+ `reason`) to refuse, or assign `result` to
  short-circuit with a synthetic successful tool_result (the body never
  runs; `tool:after` still fires). Mutating `ctx.input` rewrites the args
  the body and downstream hooks see. `metadata` rides the refused
  tool_result (read only when the call is blocked).

  Resumed calls (`resumed`): a checkpoint run resumed after a process
  death dispatches again the calls whose step was recorded, which passed
  the gate in the process that died. Their gate runs again so handlers
  restore what they keep per call, without asking anyone: the framework
  ignores `result` for them, and `block` too unless the call's body runs
  again. What a handler writes to
  `resumeState` (JSON, under a key of its own) is stored with the call's
  step record and comes back on a resumed call.
- `tool:progress` (`(ctx: ToolHookContext & { progress: ToolProgress }) => void`): Live output of a running tool (`ToolContext.reportProgress`): a codemode
  script's printed items, a command's output chunks. Fires between
  `tool:before` and the call's terminal event, never on replay of a
  recorded call. Nothing here reaches the model or the session.
- `tool:transform` (`(ctx: ToolHookContext & { coercions?: readonly string[]; isError: boolean; metadata: ToolResultMetadata; outputBytes: number; result: string | ToolResultContent[] }) => void`): Fires after a tool produces output and before the result goes back to the model. Mutate `ctx.result` to rewrite it. `outputBytes` is the size before any mutation.
- `tool:unknown` (`(ctx: ToolHookContext & { result?: string | ToolResultContent[]; suppressError: boolean }) => void`): Fires before the generic "Unknown tool" error when the model invokes a tool
  that isn't registered (hallucinated names, dropped MCP servers, dangling
  aliases). Mutate `result` to substitute a friendly response or set
  `suppressError: true` to skip the companion `tool:error` emission.

  Fires for any unknown tool name — including hallucinated MCP-style names
  (`mcp_<server>_xxx`); branch on `name.startsWith('mcp_')` to differentiate.
- `tracing:redact` (`(ctx: { kind: 'system' | 'prompt' | 'tool-input' | 'tool-result' | 'mcp-tool-input' | 'mcp-tool-result' | 'stream-text' | 'stream-thinking' | 'decision-answers'; meta?: Readonly<Record<string, unknown>>; redacted: string; value: string }) => void`): Privacy seam invoked by observability adapters (tracing, metrics,
  logging, persistence sinks) before attaching potentially-sensitive
  payloads as span attributes / log lines / external storage.

  Handlers mutate `ctx.redacted` — the field starts equal to `value` and
  the final post-hook string is what consumers ship downstream. Default
  (no handlers) is no redaction. Multiple handlers compose left-to-right;
  the last writer wins.

  `kind` lets a single handler do shape-aware scrubbing (system prompts
  vs. tool args vs. tool output have very different secret surfaces).
  `meta` carries context-specific identifiers (`toolName`, `server`,
  `turnId`, `callId`) the adapter knows at the call site — handlers use
  it for policy decisions ("skip redaction on internal `read_file`
  results"). Adapters that don't fire this hook simply attach raw values.

  Synchronous-only by design: observability stays on the hot path. Heavy
  scrubbing (regex over MB of output) belongs in `tool:transform` instead.
- `turn:after` (`(ctx: { aborted?: boolean; cumulativeUsage: Readonly<{ cacheCreation: number; cacheRead: number; cost?: number; input: number; output: number; turns: number }>; message: SessionTurn; toolCounts: { run: Readonly<Record<string, number>>; turn: Readonly<Record<string, number>> }; turn: number; turnId: string; usage: TurnUsage }) => void`): Fires after each assistant turn (before its tool-result follow-up
  dispatches; the loop iterates back to a fresh `turn:before` once the
  tool results are produced).

  `toolCounts.turn` — calls **emitted** by the model in this assistant
  turn, keyed by canonical tool name. Reflects what the model asked for,
  regardless of downstream gate outcome. Most useful for spotting per-turn
  spikes ("the model called todowrite 4 times in one turn").

  `toolCounts.run` — cumulative running counter of **dispatched** calls
  scoped to this `runId`, captured at fire time. Excludes calls that were
  `block`ed by `tool:gate` handlers. Includes calls short-circuited via
  `tool:gate` `result` substitution (the model still asked, the framework
  just answered without the tool running). Resumed sessions start a fresh
  run with empty counts.

  Both fields are frozen snapshots; mutate-safe.

  `aborted` — runtime emissions always include this boolean; it is optional
  on the type only so tests/adapters that manually synthesize `turn:after`
  payloads do not break (treat absent as `false`). `true` when the run's
  `AbortController` was already tripped at the moment this `turn:after`
  fired. This is the case to branch on instead of `usage.finishReason` when
  emulating Stop-hook semantics: an aborted turn can still carry
  `finishReason: 'stop'` (e.g. a final text turn the user cancelled
  mid-stream), so `finishReason` alone cannot distinguish "model finished"
  from "we killed it".

  Covers the streaming-abort path (cancel landed while the turn was
  streaming) and any abort that landed before this fired. NOTE: a
  `tool:gate` / `repeat-guard` abort trips DURING the tool batch, which runs
  after this `turn:after` — so the offending turn reports `aborted: false`
  here (its `finishReason` is `'tool-calls'`, never `'stop'`, so a
  `'stop'`-keyed consumer won't misfire). That abort surfaces on the
  subsequent `agent:abort`. For the post-completion window (host queues a
  follow-up after a clean `turn:after`, then the run is aborted), rely on
  `Agent.steer`/`Agent.followUp` returning `false` once aborted.
  `false` on every normal turn boundary.
- `turn:before` (`(ctx: { options: StreamOptions; turn: number; turnId: string }) => void`): Fires immediately before each turn's `provider.stream()` call, after
  `context:transform` + pairing repair + `system:transform`. The
  `options` object IS mutable as a last-mile escape hatch (per-turn
  reminders, dynamic tags, audit wrapping), but prefer
  [`['context:transform']`](https://zidane.si/docs/reference/agent.md#AgentHooks) for any mutation that touches
  `messages` — it runs upstream of the defensive pairing pass and stays
  cheaper. Loop re-runs `ensureToolResultPairing` + `ensureEndsWithUserMessage`
  after this hook so a `messages` mutation here can't leak a
  `tool_result must be preceded by a tool_call` 400 to the wire, but you
  pay one extra walk for the privilege.
- `usage` (`(ctx: { totalIn: number; totalOut: number; turn: number; turnId: string; usage: TurnUsage }) => void`): Fires after each turn with that turn's token usage and the run's cumulative input and output tokens.
- `validation:coerce` (`(ctx: ToolHookContext & { coercions: readonly string[]; schema: Record<string, unknown> }) => void`): Fires when `validateToolArgs` successfully auto-coerced one or more input
  fields to satisfy the tool's `inputSchema`. **Only fires when at least one
  coercion happened** — never on perfectly-shaped inputs. Useful for counting
  model "wrongness rate" without re-running validation downstream.

  `coercions` lists the field names that were coerced. The values landed in
  the input that the tool actually received; consumers wanting before/after
  comparison can re-run `validateToolArgs(ctx.input, ctx.schema)`.
- `validation:reject` (`(ctx: ToolHookContext & { reason: string; schema: Record<string, unknown> }) => void`): Fires when `validateToolArgs` rejects an input that could not be auto-coerced
  to satisfy the tool's `inputSchema`. Observational — the tool call still
  surfaces a `Validation error: …` string back to the model. Useful for
  counting validation failures separately from runtime tool errors.

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

### `AgentOptions`

Interface.

Options for [`createAgent`](https://zidane.si/docs/reference/agent.md#createAgent). Only `provider` is required.

```ts
interface AgentOptions
```

**Properties**

- `approveToolCall?` (`ToolCallApprover`): Resolves a `tool.call` interceptor's `'ask'` verdict through the host's
  approval flow. Without it, `'ask'` blocks the call.
- `artifacts?` (`boolean`): Session artifacts master switch (docs/ARTIFACTS.md). `false` makes the
  loop ignore the reserved `metadata.artifacts` namespace entirely — no
  store writes, no `artifact:recorded` — regardless of which detectors or
  tools are composed. Hosts mirror `Settings.artifacts` here. Default: on.
- `behavior?` (`AgentBehavior`): Agent-level behavior defaults (overridden by run-level behavior)
- `behaviorTransforms?` (`readonly ExtensionOrderedContribution<ExtensionBehaviorTransform>[]`): Behavior transforms contributed by extensions, applied before each run.
- `clock?` (`AgentClock`): Time + UUID source for all journaled metadata (turn ids, `createdAt`
  stamps, `runId`s, hook payloads consumers may persist). Defaults to
  [`DEFAULT_AGENT_CLOCK`](https://zidane.si/docs/reference/types.md#DEFAULT_AGENT_CLOCK) (`Date.now()` / `crypto.randomUUID()`).

  Override at the run level via `AgentRunOptions.clock`. Live-only
  measurements (TTFT, elapsed counters) keep `Date.now()` regardless.

  Ignored on a replay-mode durable runtime (`AgentOptions.durable`):
  the run uses the runtime's clock, so replay regenerates byte-identical
  session metadata across attempts.
- `contextBreakdownSections?` (`readonly ContextBreakdownContributor[]`): Extension-owned context panel contributors. Hosts that already spread
  `buildExtensionPreset(registry)` can pass `registry.contextBreakdownSections`
  here without re-folding the whole registry and duplicating hooks/tools.
- `decisions?` (`false | AgentDecisionsOptions`): Decisions configuration for `agent.decide` / `ctx.decide`.

  - omitted: use the first provider in `provider` that implements
    `decisions`, with its default decision model.
  - an object: pin the provider, default model and/or timeout.
  - `false`: decisions are disabled; every call returns
    `{ ok: false, reason: 'unavailable' }`.
- `durable?` (`DurableRuntime | DurableOptions`): Durable runtime every model call, tool call and loop decision goes
  through, so a run can resume after a process death (`zidane/durable`):
  the runtime itself, or [`DurableOptions`](https://zidane.si/docs/reference/durable.md#DurableOptions) to also say what `run()`
  does with a run a dead process left. Unset: the agent's memory runtime,
  and runs record no steps, so `resume()` rejects.
- `eager?` (`boolean`): Pre-connect MCP servers in the background as soon as `createAgent` returns,
  instead of deferring the bootstrap to the first `agent.run()`.

  Useful when MCP latency is the dominant cost of a cold start: callers that
  construct the agent early (e.g. at process init) can hide the bootstrap
  behind other setup work. If bootstrap fails while a `run()` / `warmup()`
  is awaiting the same in-flight promise, that caller sees the error.
  Otherwise the failure is dropped and the next `run()` / `warmup()`
  retries the connection.

  Also starts skills resolution when skills are configured. No-op when there
  are no MCP servers and no skills. Default: `false`.
- `execution?` (`ExecutionContext`): Execution context: where tools run. Defaults to in-process.
- `extensionRuntime?` (`AgentExtensionRuntime`): The extension runtime's registries (`ExtensionRegistry.runtime`): the
  interceptor chains the agent runs at its call sites, and the tools
  catalog it replays per run. Folded from `extensions.runtime` when unset.
  Absent: no interceptor call site runs.
- `extensions?` (`ExtensionRegistry`): Pre-built [`ExtensionRegistry`](https://zidane.si/docs/reference/extensions.md#ExtensionRegistry) to fold into this agent, synchronously.

  Build it first (async) via
  `buildExtensionRegistry(defs.map(inlineLoadedExtension))`, then pass the
  RESULT here — `createAgent` is sync, so it takes the merged registry, not
  raw definitions. Folded when the agent is created (see
  `foldExtensionRegistry`), with explicit `AgentOptions` winning on
  collision:

  - `tools` — merged by name; `mcpServers` — deduped by name (explicit
    wins on collision).
  - `skills` — last-wins: an explicit `skills` REPLACES the extension's
    wholesale (composePresets doesn't per-skill merge).
  - `hooks` — bucketed per event; handlers from BOTH the registry and
    `hooks` fire (registry first).
  - system fragments — appended AFTER `system` (blank-line separated).
  - subagents — applied to whichever `spawn` tool survives, via
    `spawn.withSubagents`.

  Config is NOT folded: `ctx.config.get` reads at runtime through the
  registry's own captured `readSettings` getter (a registry built without
  one returns declared defaults). `themes` / `agents` / `providers` are
  host/picker concerns and intentionally stay out of the agent.
- `extensionScope?` (`ExtensionCallScope`): For a subagent: the session and spawning run its interceptor inputs
  name (`ToolCall.sessionId`, `ToolCall.parentRunId`). The spawn tool
  sets it with `extensionRuntime`; read only when `extensionRuntime` is set.
- `handle?` (`ExecutionHandle | (() => Promise<ExecutionHandle>)`): Pre-bound execution handle. When set, the agent uses it verbatim and does
  NOT call `execution.spawn()` — and does NOT `destroy()` it on teardown,
  since the host owns its lifecycle.

  The durable-execution use case: a host that needs the spawn to be a
  recorded, replay-stable side effect drives `execution.spawn()` itself as a
  step of its durable runtime and hands the resulting handle here. On replay
  the recorded handle comes back identical, so the agent re-attaches to the
  same sandbox/container instead of minting a fresh one (which the internal
  lazy spawn, being unrecorded, cannot guarantee). Pair with a context that can re-attach by handle id (e.g.
  `createSandboxContext`, whose handle id IS the sandbox id) and a provider
  `sandboxId` for the underlying backend.

  For host-observation without driving spawn yourself, see the
  `execution:ready` hook, which fires once with the bound handle.

  A function form resolves the host-owned handle lazily, at the moment the
  agent would otherwise spawn (first run / `ensureExecution()`) — for hosts
  that build agents synchronously but keep one handle across rebuilds
  (`chat/execution-cache.ts`). Same ownership contract: never spawned, never
  destroyed by the agent.
- `hooks?` (`Partial<{ 'agent:abort': ((ctx: { reason?: string; runId?: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { reason?: string; runId?: string } & DecideHookContext, ...rest: []) => void)[]; 'agent:done': ((ctx: AgentStats & DecideHookContext, ...rest: []) => void) | ((ctx: AgentStats & DecideHookContext, ...rest: []) => void)[]; 'agent:start': ((ctx: { agentName?: string; depth: number; parentRunId?: string; providerName?: string; runId: string; startedAt: number; tracingContext?: Readonly<Record<string, string>> } & DecideHookContext, ...rest: []) => void) | ((ctx: { agentName?: string; depth: number; parentRunId?: string; providerName?: string; runId: string; startedAt: number; tracingContext?: Readonly<Record<..., ...>> } & DecideHookContext, ...rest: []) => void)[]; 'artifact:recorded': ((ctx: { artifact: SessionArtifact; callId: string; name: string; runId: string; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { artifact: SessionArtifact; callId: string; name: string; runId: string; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'background:adopt': ((ctx: { command: string; cwd: string; outputPath: string; pid: number; startedAt: number; taskId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { command: string; cwd: string; outputPath: string; pid: number; startedAt: number; taskId: string } & DecideHookContext, ...rest: []) => void)[]; 'background:exit': ((ctx: { command: string; durationMs: number; exitCode: number; outputPath: string; signal?: Signals; status: 'exited' | 'killed'; taskId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { command: string; durationMs: number; exitCode: number; outputPath: string; signal?: Signals; status: 'exited' | 'killed'; taskId: string } & DecideHookContext, ...rest: []) => void)[]; 'background:reassign': ((ctx: { childId?: string; command: string; fromHandleId: string; outputPath: string; pid: number; startedAt: number; taskId: string; toHandleId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { childId?: string; command: string; fromHandleId: string; outputPath: string; pid: number; startedAt: number; taskId: string; toHandleId: string } & DecideHookContext, ...rest: []) => void)[]; 'background:reconcile': ((ctx: { exits: TaskExitInfo[] } & DecideHookContext, ...rest: []) => void) | ((ctx: { exits: TaskExitInfo[] } & DecideHookContext, ...rest: []) => void)[]; 'background:stall': ((ctx: { bytesWritten: number; command: string; outputPath: string; stalledForMs: number; taskId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { bytesWritten: number; command: string; outputPath: string; stalledForMs: number; taskId: string } & DecideHookContext, ...rest: []) => void)[]; 'background:start': ((ctx: { command: string; cwd: string; outputPath: string; pid: number; startedAt: number; taskId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { command: string; cwd: string; outputPath: string; pid: number; startedAt: number; taskId: string } & DecideHookContext, ...rest: []) => void)[]; 'budget:exceeded': ((ctx: { budget: number; bytes: number; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { budget: number; bytes: number; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'cache:break': ((ctx: CacheBreakInfo & DecideHookContext, ...rest: []) => void | Promise<void>) | ((ctx: CacheBreakInfo & DecideHookContext, ...rest: []) => void | Promise<void>)[]; 'cache:drift': ((ctx: CacheDriftInfo & DecideHookContext, ...rest: []) => void | Promise<void>) | ((ctx: CacheDriftInfo & DecideHookContext, ...rest: []) => void | Promise<void>)[]; 'child:artifact:recorded': ((ctx: { artifact: SessionArtifact; callId: string; childId: string; depth: number; name: string; runId: string; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { artifact: SessionArtifact; callId: string; childId: string; depth: number; name: string; runId: string; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'child:background:exit': ((ctx: { childId: string; command: string; depth: number; durationMs: number; exitCode: number; outputPath: string; signal?: Signals; status: 'exited' | 'killed'; taskId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { childId: string; command: string; depth: number; durationMs: number; exitCode: number; outputPath: string; signal?: Signals; status: 'exited' | 'killed'; taskId: string } & DecideHookContext, ...rest: []) => void)[]; 'child:background:reassign': ((ctx: { childId: string; command: string; depth: number; fromHandleId: string; outputPath: string; pid: number; startedAt: number; taskId: string; toHandleId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { childId: string; command: string; depth: number; fromHandleId: string; outputPath: string; pid: number; startedAt: number; taskId: string; toHandleId: string } & DecideHookContext, ...rest: []) => void)[]; 'child:background:start': ((ctx: { childId: string; command: string; cwd: string; depth: number; outputPath: string; pid: number; startedAt: number; taskId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { childId: string; command: string; cwd: string; depth: number; outputPath: string; pid: number; startedAt: number; taskId: string } & DecideHookContext, ...rest: []) => void)[]; 'child:mcp:tool:gate': ((ctx: McpToolHookContext & { block: boolean; childId: string; depth: number; reason: string; result?: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void) | ((ctx: McpToolHookContext & { block: boolean; childId: string; depth: number; reason: string; result?: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void)[]; 'child:repeat-guard:exceeded': ((ctx: { action: 'steer' | 'block' | 'abort'; childId: string; count: number; depth: number; threshold: number; tool: string; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { action: 'steer' | 'block' | 'abort'; childId: string; count: number; depth: number; threshold: number; tool: string; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'child:spawn:before': ((ctx: Omit<SpawnHookContext, 'tracingContext'> & ChildSpawnIdentity & DecideHookContext, ...rest: []) => void) | ((ctx: Omit<SpawnHookContext, 'tracingContext'> & ChildSpawnIdentity & DecideHookContext, ...rest: []) => void)[]; 'child:spawn:complete': ((ctx: ChildRunStats & ChildSpawnIdentity & DecideHookContext, ...rest: []) => void) | ((ctx: ChildRunStats & ChildSpawnIdentity & DecideHookContext, ...rest: []) => void)[]; 'child:spawn:error': ((ctx: Omit<SpawnHookContext, 'tracingContext'> & ChildSpawnIdentity & { error: Error } & DecideHookContext, ...rest: []) => void) | ((ctx: Omit<SpawnHookContext, 'tracingContext'> & ChildSpawnIdentity & { error: Error } & DecideHookContext, ...rest: []) => void)[]; 'child:stream:end': ((ctx: StreamHookContext & { childId: string; depth: number; text: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { childId: string; depth: number; text: string } & DecideHookContext, ...rest: []) => void)[]; 'child:stream:error': ((ctx: StreamHookContext & { childId: string; depth: number; err: unknown } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { childId: string; depth: number; err: unknown } & DecideHookContext, ...rest: []) => void)[]; 'child:stream:retry': ((ctx: StreamHookContext & { attempt: number; childId: string; delayMs: number; depth: number; discardedText?: true; discardedThinking?: true; err: unknown; maxAttempts: number; nextAttempt: number; requestId?: string; statusCode?: number } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { attempt: number; childId: string; delayMs: number; depth: number; discardedText?: true; discardedThinking?: true; err: unknown; maxAttempts: number; nextAttempt: number; requestId?: string; statusCode?: number } & DecideHookContext, ...rest: []) => void)[]; 'child:stream:server_tool_result': ((ctx: StreamHookContext & { childId: string; content: unknown; depth: number; toolName: string; toolUseId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { childId: string; content: unknown; depth: number; toolName: string; toolUseId: string } & DecideHookContext, ...rest: []) => void)[]; 'child:stream:server_tool_use': ((ctx: StreamHookContext & { childId: string; depth: number; id: string; input: Record<string, unknown>; name: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { childId: string; depth: number; id: string; input: Record<string, unknown>; name: string } & DecideHookContext, ...rest: []) => void)[]; 'child:stream:text': ((ctx: StreamHookContext & { childId: string; delta: string; depth: number; text: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { childId: string; delta: string; depth: number; text: string } & DecideHookContext, ...rest: []) => void)[]; 'child:stream:thinking': ((ctx: StreamHookContext & { childId: string; delta: string; depth: number; thinking: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { childId: string; delta: string; depth: number; thinking: string } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:after': ((ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; isError?: boolean; outcome?: ToolOutcome; outputBytes: number; result: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; isError?: boolean; outcome?: ToolOutcome; outputBytes: number; result: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:before': ((ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; priorContent?: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; priorContent?: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:cancelled': ((ctx: ToolHookContext & { childId: string; depth: number; reason: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; depth: number; reason: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:dispatched': ((ctx: ToolHookContext & { childId: string; depth: number; outcome: 'execute' | 'gate-substitute' | 'gate-block' | 'unknown' | 'invalid-input'; reason?: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; depth: number; outcome: 'execute' | 'gate-substitute' | 'gate-block' | 'unknown' | 'invalid-input'; reason?: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:error': ((ctx: ToolHookContext & { childId: string; depth: number; error: Error } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; depth: number; error: Error } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:gate': ((ctx: ToolHookContext & ToolGateResumeFields & { block: boolean; childId: string; depth: number; metadata: ToolResultMetadata; reason: string; result?: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & ToolGateResumeFields & { block: boolean; childId: string; depth: number; metadata: ToolResultMetadata; reason: string; result?: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:progress': ((ctx: ToolHookContext & { childId: string; depth: number; progress: ToolProgress } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; depth: number; progress: ToolProgress } & DecideHookContext, ...rest: []) => void)[]; 'child:tool:transform': ((ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; isError: boolean; metadata: ToolResultMetadata; outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; coercions?: readonly string[]; depth: number; isError: boolean; metadata: ToolResultMetadata; outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void)[]; 'child:turn:after': ((ctx: { aborted?: boolean; childId: string; depth: number; message: SessionTurn; toolCounts: { run: Readonly<Record<string, number>>; turn: Readonly<Record<string, number>> }; turn: number; turnId: string; usage: TurnUsage } & DecideHookContext, ...rest: []) => void) | ((ctx: { aborted?: boolean; childId: string; depth: number; message: SessionTurn; toolCounts: { run: Readonly<Record<..., ...>>; turn: Readonly<Record<..., ...>> }; turn: number; turnId: string; usage: TurnUsage } & DecideHookContext, ...rest: []) => void)[]; 'child:validation:reject': ((ctx: ToolHookContext & { childId: string; depth: number; reason: string; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { childId: string; depth: number; reason: string; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void)[]; 'compact:end': ((ctx: (CompactHookContext & { droppedCount: number; durationMs: number; effectiveTokens: number; replacedCount: number; restoredFiles: number; restoredSkills: number; status: 'success'; summaryModel: string } & DecideHookContext) | (CompactHookContext & { durationMs: number; error: unknown; status: 'error' } & DecideHookContext), ...rest: []) => void) | ((ctx: (CompactHookContext & { droppedCount: number; durationMs: number; effectiveTokens: number; replacedCount: number; restoredFiles: number; restoredSkills: number; status: 'success'; summaryModel: string } & DecideHookContext) | (CompactHookContext & { durationMs: number; error: unknown; status: 'error' } & DecideHookContext), ...rest: []) => void)[]; 'compact:progress': ((ctx: CompactProgressHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: CompactProgressHookContext & DecideHookContext, ...rest: []) => void)[]; 'compact:restore': ((ctx: { add: (call: { input: Record<string, unknown>; name: string; output: string }) => void; session?: Session } & DecideHookContext, ...rest: []) => void | Promise<void>) | ((ctx: { add: (call: { input: Record<..., ...>; name: string; output: string }) => void; session?: Session } & DecideHookContext, ...rest: []) => void | Promise<void>)[]; 'compact:start': ((ctx: CompactStartHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: CompactStartHookContext & DecideHookContext, ...rest: []) => void)[]; 'content-ref:resolve': ((ctx: { block: ResolveContentRefBlock; childId?: string; data?: string; depth?: number } & DecideHookContext, ...rest: []) => void) | ((ctx: { block: ResolveContentRefBlock; childId?: string; data?: string; depth?: number } & DecideHookContext, ...rest: []) => void)[]; 'context:overflow': ((ctx: StreamHookContext & { attempt: number; err: unknown } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { attempt: number; err: unknown } & DecideHookContext, ...rest: []) => void)[]; 'context:transform': ((ctx: { messages: SessionMessage[] } & DecideHookContext, ...rest: []) => void) | ((ctx: { messages: SessionMessage[] } & DecideHookContext, ...rest: []) => void)[]; 'decision:end': ((ctx: DecisionEndHookContext) => void) | ((ctx: DecisionEndHookContext) => void)[]; 'decision:start': ((ctx: DecisionStartHookContext) => void) | ((ctx: DecisionStartHookContext) => void)[]; 'execution:ready': ((ctx: { handle: ExecutionHandle; spawned: boolean } & DecideHookContext, ...rest: []) => void) | ((ctx: { handle: ExecutionHandle; spawned: boolean } & DecideHookContext, ...rest: []) => void)[]; 'history:repair': ((ctx: HistoryRepairHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: HistoryRepairHookContext & DecideHookContext, ...rest: []) => void)[]; 'mcp:auth:error': ((ctx: { error: Error; name: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { error: Error; name: string } & DecideHookContext, ...rest: []) => void)[]; 'mcp:auth:required': ((ctx: { name: string; reason: 'no-tokens' | 'auto-promoted'; transport: 'sse' | 'stdio' | 'streamable-http' } & DecideHookContext, ...rest: []) => void) | ((ctx: { name: string; reason: 'no-tokens' | 'auto-promoted'; transport: 'sse' | 'stdio' | 'streamable-http' } & DecideHookContext, ...rest: []) => void)[]; 'mcp:auth:success': ((ctx: { name: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { name: string } & DecideHookContext, ...rest: []) => void)[]; 'mcp:auth:url': ((ctx: { name: string; url: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { name: string; url: string } & DecideHookContext, ...rest: []) => void)[]; 'mcp:bootstrap:end': ((ctx: ({ durationMs: number; name: string; transport: string } & { cached?: boolean; lazy?: boolean; ok: true; toolCount: number; warnings?: string[] } & DecideHookContext) | ({ durationMs: number; name: string; transport: string } & { error: Error; ok: false } & DecideHookContext), ...rest: []) => void) | ((ctx: ({ durationMs: number; name: string; transport: string } & { cached?: boolean; lazy?: boolean; ok: true; toolCount: number; warnings?: ...[] } & DecideHookContext) | ({ durationMs: number; name: string; transport: string } & { error: Error; ok: false } & DecideHookContext), ...rest: []) => void)[]; 'mcp:bootstrap:settled': ((ctx: { results: readonly ({ durationMs: number; name: string; transport: string } & ({ lazy?: ...; ok: ...; toolCount: ... } | { error?: ...; ok: ...; skipped?: ... }))[] } & DecideHookContext, ...rest: []) => void) | ((ctx: { results: readonly ({ durationMs: ...; name: ...; transport: ... } & (... | ...))[] } & DecideHookContext, ...rest: []) => void)[]; 'mcp:bootstrap:start': ((ctx: { name: string; transport: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { name: string; transport: string } & DecideHookContext, ...rest: []) => void)[]; 'mcp:close': ((ctx: { name: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { name: string } & DecideHookContext, ...rest: []) => void)[]; 'mcp:connect': ((ctx: { lazy?: boolean; name: string; tools: string[]; transport: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { lazy?: boolean; name: string; tools: string[]; transport: string } & DecideHookContext, ...rest: []) => void)[]; 'mcp:error': ((ctx: { error: Error; name: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { error: Error; name: string } & DecideHookContext, ...rest: []) => void)[]; 'mcp:tool:after': ((ctx: McpToolHookContext & { outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void) | ((ctx: McpToolHookContext & { outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void)[]; 'mcp:tool:before': ((ctx: McpToolHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: McpToolHookContext & DecideHookContext, ...rest: []) => void)[]; 'mcp:tool:error': ((ctx: McpToolHookContext & { error: Error } & DecideHookContext, ...rest: []) => void) | ((ctx: McpToolHookContext & { error: Error } & DecideHookContext, ...rest: []) => void)[]; 'mcp:tool:gate': ((ctx: McpToolHookContext & { block: boolean; reason: string; result?: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void) | ((ctx: McpToolHookContext & { block: boolean; reason: string; result?: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void)[]; 'mcp:tool:transform': ((ctx: McpToolHookContext & { outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void) | ((ctx: McpToolHookContext & { outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void)[]; 'mcp:tools:filter': ((ctx: { server: string; tools: { description?: string | null; inputSchema?: unknown; name: string }[]; transport: 'sse' | 'stdio' | 'streamable-http' } & DecideHookContext, ...rest: []) => void) | ((ctx: { server: string; tools: { description?: ... | ... | ...; inputSchema?: unknown; name: string }[]; transport: 'sse' | 'stdio' | 'streamable-http' } & DecideHookContext, ...rest: []) => void)[]; 'mcp:tools:list': ((ctx: { name: string; tools: readonly McpToolSchema[]; transport: 'sse' | 'stdio' | 'streamable-http' } & DecideHookContext, ...rest: []) => void) | ((ctx: { name: string; tools: readonly McpToolSchema[]; transport: 'sse' | 'stdio' | 'streamable-http' } & DecideHookContext, ...rest: []) => void)[]; 'mcp:warn': ((ctx: { message: string; name: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { message: string; name: string } & DecideHookContext, ...rest: []) => void)[]; 'model:fallback': ((ctx: StreamHookContext & { discardedThinking?: true; err: Error; from: { model: string; provider: string }; targetIndex: number; to: { model: string; provider: string } } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { discardedThinking?: true; err: Error; from: { model: string; provider: string }; targetIndex: number; to: { model: string; provider: string } } & DecideHookContext, ...rest: []) => void)[]; 'oauth:refresh': ((ctx: OAuthRefreshHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: OAuthRefreshHookContext & DecideHookContext, ...rest: []) => void)[]; output: ((ctx: { output: Record<string, unknown>; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void) | ((ctx: { output: Record<string, unknown>; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void)[]; 'pairing:repair': ((ctx: PairingRepair & { turnId?: string } & DecideHookContext, ...rest: []) => void) | ((ctx: PairingRepair & { turnId?: string } & DecideHookContext, ...rest: []) => void)[]; 'provider:diagnostic': ((ctx: ProviderDiagnostic & { runId: string; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: ProviderDiagnostic & { runId: string; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'provider:input-transformations': ((ctx: InputTransformationsHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: InputTransformationsHookContext & DecideHookContext, ...rest: []) => void)[]; 'repeat-guard:exceeded': ((ctx: { action: 'steer' | 'block' | 'abort'; count: number; threshold: number; tool: string; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { action: 'steer' | 'block' | 'abort'; count: number; threshold: number; tool: string; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'replay:done': ((ctx: Record<string, never> & DecideHookContext, ...rest: []) => void) | ((ctx: Record<string, never> & DecideHookContext, ...rest: []) => void)[]; 'resume:mismatch': ((ctx: { reason: string; resumeTurn: number; runId: string; sessionId: string; storedTurns: readonly number[] } & DecideHookContext, ...rest: []) => void) | ((ctx: { reason: string; resumeTurn: number; runId: string; sessionId: string; storedTurns: readonly number[] } & DecideHookContext, ...rest: []) => void)[]; 'run:beforeEnd': ((ctx: { followUp: (message: string) => void; runId: string; session?: Session; turn: number } & DecideHookContext, ...rest: []) => void | Promise<void>) | ((ctx: { followUp: (message: string) => void; runId: string; session?: Session; turn: number } & DecideHookContext, ...rest: []) => void | Promise<void>)[]; 'session:end': ((ctx: SessionHookContext & { runId: string; session?: Session; status: SessionEndStatus; turnRange: [number, number] } & DecideHookContext, ...rest: []) => void) | ((ctx: SessionHookContext & { runId: string; session?: Session; status: SessionEndStatus; turnRange: [number, number] } & DecideHookContext, ...rest: []) => void)[]; 'session:meta': ((ctx: SessionHookContext & { key: string; value: unknown } & DecideHookContext, ...rest: []) => void) | ((ctx: SessionHookContext & { key: string; value: unknown } & DecideHookContext, ...rest: []) => void)[]; 'session:save': ((ctx: SessionHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: SessionHookContext & DecideHookContext, ...rest: []) => void)[]; 'session:start': ((ctx: SessionHookContext & { prompt: string; runId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: SessionHookContext & { prompt: string; runId: string } & DecideHookContext, ...rest: []) => void)[]; 'session:turns': ((ctx: SessionHookContext & { count: number; turns: SessionTurn[] } & DecideHookContext, ...rest: []) => void) | ((ctx: SessionHookContext & { count: number; turns: SessionTurn[] } & DecideHookContext, ...rest: []) => void)[]; 'skills:activate': ((ctx: { skill: SkillConfig; via: ActivationVia } & DecideHookContext, ...rest: []) => void) | ((ctx: { skill: SkillConfig; via: ActivationVia } & DecideHookContext, ...rest: []) => void)[]; 'skills:catalog': ((ctx: { catalog: string; skills: SkillConfig[] } & DecideHookContext, ...rest: []) => void) | ((ctx: { catalog: string; skills: SkillConfig[] } & DecideHookContext, ...rest: []) => void)[]; 'skills:deactivate': ((ctx: { reason: DeactivationReason; skill: SkillConfig } & DecideHookContext, ...rest: []) => void) | ((ctx: { reason: DeactivationReason; skill: SkillConfig } & DecideHookContext, ...rest: []) => void)[]; 'skills:resolve': ((ctx: { skills: SkillConfig[] } & DecideHookContext, ...rest: []) => void) | ((ctx: { skills: SkillConfig[] } & DecideHookContext, ...rest: []) => void)[]; 'spawn:before': ((ctx: SpawnHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: SpawnHookContext & DecideHookContext, ...rest: []) => void)[]; 'spawn:complete': ((ctx: ChildRunStats & DecideHookContext, ...rest: []) => void) | ((ctx: ChildRunStats & DecideHookContext, ...rest: []) => void)[]; 'spawn:error': ((ctx: SpawnHookContext & { error: Error } & DecideHookContext, ...rest: []) => void) | ((ctx: SpawnHookContext & { error: Error } & DecideHookContext, ...rest: []) => void)[]; 'spawn:queued': ((ctx: SpawnQueuedContext & DecideHookContext, ...rest: []) => void) | ((ctx: SpawnQueuedContext & DecideHookContext, ...rest: []) => void)[]; 'steer:inject': ((ctx: { content: readonly PromptPart[]; id?: string; text: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { content: readonly PromptPart[]; id?: string; text: string } & DecideHookContext, ...rest: []) => void)[]; 'stream:end': ((ctx: StreamHookContext & { text: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { text: string } & DecideHookContext, ...rest: []) => void)[]; 'stream:error': ((ctx: StreamHookContext & { err: unknown; requestId?: string; statusCode?: number } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { err: unknown; requestId?: string; statusCode?: number } & DecideHookContext, ...rest: []) => void)[]; 'stream:progress': ((ctx: StreamProgressHookContext & DecideHookContext, ...rest: []) => void) | ((ctx: StreamProgressHookContext & DecideHookContext, ...rest: []) => void)[]; 'stream:retry': ((ctx: StreamHookContext & { attempt: number; delayMs: number; discardedText?: true; discardedThinking?: true; err: unknown; maxAttempts: number; nextAttempt: number; requestId?: string; statusCode?: number } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { attempt: number; delayMs: number; discardedText?: true; discardedThinking?: true; err: unknown; maxAttempts: number; nextAttempt: number; requestId?: string; statusCode?: number } & DecideHookContext, ...rest: []) => void)[]; 'stream:server_tool_result': ((ctx: StreamHookContext & { content: unknown; toolName: string; toolUseId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { content: unknown; toolName: string; toolUseId: string } & DecideHookContext, ...rest: []) => void)[]; 'stream:server_tool_use': ((ctx: StreamHookContext & { id: string; input: Record<string, unknown>; name: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { id: string; input: Record<string, unknown>; name: string } & DecideHookContext, ...rest: []) => void)[]; 'stream:start': ((ctx: StreamHookContext & { startedAt: number } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { startedAt: number } & DecideHookContext, ...rest: []) => void)[]; 'stream:text': ((ctx: StreamHookContext & { delta: string; text: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { delta: string; text: string } & DecideHookContext, ...rest: []) => void)[]; 'stream:thinking': ((ctx: StreamHookContext & { delta: string; thinking: string } & DecideHookContext, ...rest: []) => void) | ((ctx: StreamHookContext & { delta: string; thinking: string } & DecideHookContext, ...rest: []) => void)[]; 'system:before': ((ctx: { system: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { system: string } & DecideHookContext, ...rest: []) => void)[]; 'system:transform': ((ctx: { messages: readonly SessionMessage[]; session?: Session; system: string; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { messages: readonly SessionMessage[]; session?: Session; system: string; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'tool-batch:resumed': ((ctx: { done: readonly string[]; resuming: readonly string[]; runId: string; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { done: readonly string[]; resuming: readonly string[]; runId: string; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'tool-budget:exceeded': ((ctx: { count: number; max: number; mode: 'steer' | 'block'; tool: string; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { count: number; max: number; mode: 'steer' | 'block'; tool: string; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'tool-results:after': ((ctx: { message: SessionTurn; results: readonly ToolResult[]; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { message: SessionTurn; results: readonly ToolResult[]; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void)[]; 'tool-state:commit-failed': ((ctx: { callId: string; error: Error; handled?: boolean; ops: readonly ToolStateOp[]; runId: string; toolName: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { callId: string; error: Error; handled?: boolean; ops: readonly ToolStateOp[]; runId: string; toolName: string } & DecideHookContext, ...rest: []) => void)[]; 'tool:after': ((ctx: ToolHookContext & { coercions?: readonly string[]; isError: boolean; outcome?: ToolOutcome; outputBytes: number; result: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { coercions?: readonly string[]; isError: boolean; outcome?: ToolOutcome; outputBytes: number; result: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'tool:before': ((ctx: ToolHookContext & { coercions?: readonly string[]; priorContent?: string; runToolCounts: Readonly<Record<string, number>>; unknown?: boolean } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { coercions?: readonly string[]; priorContent?: string; runToolCounts: Readonly<Record<string, number>>; unknown?: boolean } & DecideHookContext, ...rest: []) => void)[]; 'tool:cancelled': ((ctx: ToolHookContext & { reason: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { reason: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'tool:dispatched': ((ctx: ToolHookContext & { outcome: 'execute' | 'gate-substitute' | 'gate-block' | 'unknown' | 'invalid-input'; reason?: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { outcome: 'execute' | 'gate-substitute' | 'gate-block' | 'unknown' | 'invalid-input'; reason?: string; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'tool:error': ((ctx: ToolHookContext & { error: Error; result?: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { error: Error; result?: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void)[]; 'tool:gate': ((ctx: ToolHookContext & ToolGateResumeFields & { block: boolean; metadata: ToolResultMetadata; reason: string; result?: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & ToolGateResumeFields & { block: boolean; metadata: ToolResultMetadata; reason: string; result?: string | ToolResultContent[]; runToolCounts: Readonly<Record<string, number>> } & DecideHookContext, ...rest: []) => void)[]; 'tool:progress': ((ctx: ToolHookContext & { progress: ToolProgress } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { progress: ToolProgress } & DecideHookContext, ...rest: []) => void)[]; 'tool:transform': ((ctx: ToolHookContext & { coercions?: readonly string[]; isError: boolean; metadata: ToolResultMetadata; outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { coercions?: readonly string[]; isError: boolean; metadata: ToolResultMetadata; outputBytes: number; result: string | ToolResultContent[] } & DecideHookContext, ...rest: []) => void)[]; 'tool:unknown': ((ctx: ToolHookContext & { result?: string | ToolResultContent[]; suppressError: boolean } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { result?: string | ToolResultContent[]; suppressError: boolean } & DecideHookContext, ...rest: []) => void)[]; 'tracing:redact': ((ctx: { kind: 'system' | 'prompt' | 'tool-input' | 'tool-result' | 'mcp-tool-input' | 'mcp-tool-result' | 'stream-text' | 'stream-thinking' | 'decision-answers'; meta?: Readonly<Record<string, unknown>>; redacted: string; value: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { kind: 'system' | 'prompt' | 'tool-input' | 'tool-result' | 'mcp-tool-input' | 'mcp-tool-result' | 'stream-text' | 'stream-thinking' | 'decision-answers'; meta?: Readonly<Record<..., ...>>; redacted: string; value: string } & DecideHookContext, ...rest: []) => void)[]; 'turn:after': ((ctx: { aborted?: boolean; cumulativeUsage: Readonly<{ cacheCreation: number; cacheRead: number; cost?: number; input: number; output: number; turns: number }>; message: SessionTurn; toolCounts: { run: Readonly<Record<string, number>>; turn: Readonly<Record<string, number>> }; turn: number; turnId: string; usage: TurnUsage } & DecideHookContext, ...rest: []) => void) | ((ctx: { aborted?: boolean; cumulativeUsage: Readonly<{ cacheCreation: number; cacheRead: number; cost?: number; input: number; output: number; turns: number }>; message: SessionTurn; toolCounts: { run: Readonly<Record<..., ...>>; turn: Readonly<Record<..., ...>> }; turn: number; turnId: string; usage: TurnUsage } & DecideHookContext, ...rest: []) => void)[]; 'turn:before': ((ctx: { options: StreamOptions; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void) | ((ctx: { options: StreamOptions; turn: number; turnId: string } & DecideHookContext, ...rest: []) => void)[]; usage: ((ctx: { totalIn: number; totalOut: number; turn: number; turnId: string; usage: TurnUsage } & DecideHookContext, ...rest: []) => void) | ((ctx: { totalIn: number; totalOut: number; turn: number; turnId: string; usage: TurnUsage } & DecideHookContext, ...rest: []) => void)[]; 'validation:coerce': ((ctx: ToolHookContext & { coercions: readonly string[]; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { coercions: readonly string[]; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void)[]; 'validation:reject': ((ctx: ToolHookContext & { reason: string; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void) | ((ctx: ToolHookContext & { reason: string; schema: Record<string, unknown> } & DecideHookContext, ...rest: []) => void)[] }>`): Agent-lifetime hook registrations. Registered once when `createAgent`
  returns; identical in effect to calling `agent.hooks.hook(event, fn)` for
  each entry. Use this to bake observability / policy hooks into a
  [`Preset`](https://zidane.si/docs/reference/presets.md#Preset) so consumers get them automatically via `...spread`.

  Handlers may be a single function or an array — the array form is what
  `composePresets()` produces when multiple presets register handlers for
  the same event. Unknown event names throw at agent construction so typos
  never silently no-op.

  For per-run handlers (auto-detached at run end) use
  `AgentRunOptions.hooks`.
- `mcpConfigTransforms?` (`readonly ExtensionOrderedContribution<ExtensionMcpConfigTransform>[]`): MCP config transforms contributed by extensions.
- `mcpConnector?` (`McpConnector`): Override how `mcpServers` are connected and their tools discovered —
  see [`McpConnector`](https://zidane.si/docs/reference/agent.md#McpConnector). The default connects via
  [`connectMcpServers`](https://zidane.si/docs/reference/mcp.md#connectMcpServers); supply your own to inject a custom transport /
  auth provider. To journal discovered tools for a durable runtime, use
  `mcpToolWrap` instead — it composes with any connector.

  Invoked lazily on the first `run()` / `warmup()`, with the agent's
  hooks. A connector that bypasses [`connectMcpServers`](https://zidane.si/docs/reference/mcp.md#connectMcpServers) also bypasses
  its config normalization, so prefer delegating to it.
- `mcpServers?` (`McpServerConfig[]`): MCP servers to connect and expose as tools
- `mcpToolWrap?` (`McpToolWrap`): Wrap every discovered MCP tool right after bootstrap, before it's
  registered into the agent's tool map — see [`McpToolWrap`](https://zidane.si/docs/reference/agent.md#McpToolWrap). This is
  the seam native tools have (you wrap them before `createAgent`) that
  discovered MCP tools lack: the agent owns their discovery, so it applies
  the wrap on your behalf, for both eager and lazily-connected servers and
  regardless of which `mcpConnector` produced them.

  Not needed for durable execution: a replay-mode runtime records MCP tool
  calls as steps like native ones. Discovery, `mcp_<server>_<tool>`
  namespacing, per-server `instructions`, hooks, and OAuth all stay owned
  by the agent: the wrap only re-shapes `execute`.
- `name?` (`string`): Display name for the agent (used in traces/logs).
- `provider` (`Provider | Provider[]`): LLM provider(s) driving the agent.

  A single `Provider` is the common case. Passing an array enables
  run-level fallback chains: `AgentRunOptions.model` entries reference
  providers by `Provider.name`, and bare-string model ids fan out across
  every provider in array order. Names must be unique within the array.
  The FIRST provider is the primary — agent-level surfaces (`agent.meta`,
  `getContextBreakdown`, the default model when `model` is omitted) read it.
- `readState?` (`ReadStateMap`): Explicit read-state map for `read_file` dedup + `requireReadBeforeEdit`
  tracking. When omitted, the read-state lives in a `WeakMap<Session, …>`
  keyed by the agent's session; providing an explicit map lets a parent
  agent share its tracking with a sessionless child (the canonical caller
  is `spawn`'s `shareReadState: true` option). Tools resolve uniformly
  via `resolveReadStateMap(ctx)` so the explicit map (when present)
  wins over the session-keyed lookup.
- `runTransforms?` (`readonly ExtensionOrderedContribution<ExtensionRunTransform>[]`): Run option transforms contributed by extensions, applied before each run.
- `session?` (`Session`): Session for identity, turn persistence, and run tracking. With
  `durable`, prefer `sessionId`: `createAgent` throws when this
  session has a store other than `durable.stores.session` (a session with
  no store passes).
- `sessionId?` (`string`): Id of the session to run in, loaded from `durable.stores.session`, or
  created there when it does not exist yet. Needs `durable`; throws with
  `session` too. The session loads on the first `run()`, `resume()`,
  `compact()` or `warmup()`, or in the background at `createAgent` when
  `eager` is set and there are MCP servers or skills to warm.
  `agent.session` is `null` until then.
- `skillPolicies?` (`readonly ExtensionOrderedContribution<ExtensionSkillPolicy>[]`): Skill activation policies contributed by extensions.
- `skills?` (`SkillsConfig`): Skills configuration
- `system?` (`string`): Default system prompt injected when no system is provided at run time.
- `toolAliases?` (`Record<string, string>`): Map canonical tool names to LLM-facing (aliased) names.

  Aliasing is **LLM-boundary-only**: the alias is what the provider's tool spec
  carries and what the model calls the tool; the canonical name is what lives in
  `session.turns` and what the agent uses to look up the tool implementation.
- `toolCatalogTransforms?` (`readonly ExtensionOrderedContribution<ExtensionToolCatalogTransform>[]`): Tool catalog transforms contributed by extensions.
- `toolDescriptions?` (`ToolDescriptions`): Override the LLM-facing description of any tool — native, host-provided,
  or MCP-discovered — keyed by **canonical** name (`shell`,
  `mcp_<server>_<tool>`; same keys as `toolAliases`, so aliases and
  MCP name sanitization never enter into it).

  A string replaces the description; a function receives the current text
  (after every framework rewrite — e.g. `shell`'s per-run background gate)
  and returns the new one, which is the form to use for amendments:

  ```ts
  toolDescriptions: {
    shell: cur => `${cur}\nNever invoke sudo.`,
    mcp_github_create_pull_request: 'Open a PR. Always target `develop`.',
  }
  ```

  Applied once per run, last, so it reaches every surface that reads
  `spec.description`: the wire tool list, the lazy `<searchable_tools>`
  catalog, `tool_search` results, and the context breakdown. Keys that
  match no registered tool are ignored (an MCP server may be offline for
  that run). Composes through presets (later wins per key) and is
  inherited by spawned subagents.
- `toolKill?` (`{ defaultMessage?: string }`): Tune `Agent.cancelTool`'s per-call kill registry.

  - `defaultMessage` — reason stamped on the abort signal (and surfaced to
    the `tool:cancelled` hook) when `cancelTool(callId)` is called without an
    explicit reason. Defaults to `'user-cancelled-tool'`.

  The registry also handles the pre-registration race: a `cancelTool` issued
  for a call the loop hasn't dispatched yet (the host saw the id in the
  streamed turn) is parked and applied the moment the call registers, rather
  than dropped. No configuration needed for that — it's always on.
- `toolResultMetadata?` (`readonly ExtensionOrderedContribution<ExtensionToolResultMetadataRegistration>[]`): Tool result metadata registrations contributed by extensions.
- `tools?` (`Record<string, ToolDef>`): Tool definitions available to the agent. Defaults to no tools.
- `toolState?` (`ToolStateStore`): Durable backing for `ToolContext.state` — the key/value map tools use
  for information that must survive a resume (a one-shot payload already
  delivered, a cursor, a dedup marker).

  Runtime-agnostic on purpose: with `durable`, a `session` or `sessionId`,
  and no store here, the runtime's `stores.toolState(<session id>)` is
  used, and any host can implement the same two methods against Redis /
  Postgres / a file. With no store configured `ctx.state` is still present and functional, backed
  by an in-memory map scoped to this agent instance — so tools written
  against it work identically on the local CLI path, just without
  durability.

  The snapshot is loaded once per agent, before the first tool dispatch;
  writes are staged per call and committed when the call settles.

  NOT inherited by subagents. A child you build yourself can be handed the
  same store, but `createSpawnTool` has no `shareToolState` counterpart to
  `shareReadState`, so tools inside a spawned subagent see an isolated
  in-memory map and their writes never reach this store.

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

### `CacheBreakInfo`

Interface.

Structured payload of the `'cache:break'` agent hook, emitted by
[`installCacheBreakLogger`](https://zidane.si/docs/reference/agent.md#installCacheBreakLogger) whenever a turn reads back materially less
prefix than the previous turn established (see `expectedReadAtLeast` /
`expectedReadRatio` on [`CacheBreakLoggerOptions`](https://zidane.si/docs/reference/agent.md#CacheBreakLoggerOptions)).

`changed` names the request dimensions that differ from the previous turn —
the culprit when the host rewrote something. An EMPTY `changed` is itself a
signal: the request was byte-stable AT THIS WIRE and the provider still
failed to serve the prefix. Three suspects, in the order to check them:

1. TTL expiry — a long pause since the previous turn (compare wall-clock
   gaps; the default window is \~5 minutes).
2. Provider-side eviction — no pause, no pattern; typically isolated.
3. Downstream transformation churn — on architectures where a gateway CASTS
   this request into another provider's format (Anthropic → OpenAI
   Responses, Converse, …), these dimensions only cover the PRE-cast bytes.
   A transform that isn't prefix-monotonic (unstable id mapping, per-request
   injections, config-derived fields like reasoning effort) breaks the
   vendor's cache while reading as byte-stable here. Recurring
   empty-`changed` breaks on a cast route point at the transform, not at
   this agent — instrument the gateway's OUTPUT with the same positional
   hashing (`hashMessages`) to name the churn.

```ts
interface CacheBreakInfo
```

**Properties**

- `cacheCreation` (`number`): Tokens written to the cache on the turn that broke.
- `cacheRead` (`number`): Tokens read from the cache on the turn that broke.
- `changed` (`CacheDimensionName[]`): Request dimensions that changed since the previous turn; empty ⇒ suspect TTL/eviction.
- `details` (`Partial<Record<CacheDimensionName, string>>`): Human-readable per-dimension diffs, keyed by the entries of `changed`.
- `expectedAtLeast` (`number`): The floor that tripped: `max(expectedReadAtLeast, ratio-derived floor)`.
- `model` (`string`): Model id of the request that broke — pair with a provider key to price the break.
- `rebilledTokens` (`number`): Approximate tokens re-billed by this break: the prefix the previous turn
  had cached minus what this turn actually read back. That portion was
  re-written at the cache-write rate (\~1.25x input) instead of read at the
  cache-read rate (\~0.1x), so the extra spend is roughly
  `1.15 x rebilledTokens` input-token equivalents. Approximate because the
  healthy-turn counterfactual (the genuine new-transcript delta) is folded
  into `cacheCreation` and cannot be separated out.
- `systemSections?` (`CacheSystemSection[]`): The parts of the system prompt that changed, when `changed` includes `'system'` — see `CacheDimensionDiff.systemSections`.
- `turn` (`number`): 1-based turn index as counted by the detector — monotonic across the
  INSTALLER'S lifetime, i.e. every `agent.run()` on this hook bus. It does
  NOT reset per run the way `turn:after`'s `ctx.turn` does, so don't
  correlate the two on any prompt after the first; use `turnId` for that.
- `turnId` (`string`): Id of the turn that broke the cache.

Source: [src/cache-telemetry.ts:215](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L215)

### `CacheBreakLoggerOptions`

Interface.

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

```ts
interface CacheBreakLoggerOptions
```

**Properties**

- `expectedReadAtLeast?` (`number`): Absolute floor for "this turn missed cache". The logger reports when
  `cacheRead` falls below the threshold AND the dimensions differ from the
  previous turn.

  Defaults to `1`, which on its own only catches TOTAL cache loss. That is not
  the common failure: when the system prompt or the message prefix is
  rewritten, `tools` + the static system block still match, so `cacheRead`
  stays comfortably non-zero — pinned to a CONSTANT equal to that scaffold —
  while the entire conversation re-bills at the cache-write rate. An
  absolute-only threshold reports such a run as perfectly healthy.

  `CacheBreakLoggerOptions.expectedReadRatio` is the fix and is on by
  default; this field remains as the absolute floor beneath it.
- `expectedReadRatio?` (`number`): Detect PARTIAL-prefix breaks by comparing this turn's `cacheRead` against
  the prefix the previous turn established (`cacheRead + cacheCreation`).
  The conversation only grows, so a healthy turn reads back at least what the
  one before it wrote; reading materially less means something at or before
  the last message changed.

  Defaults to `0.8` — the slack absorbs tokenizer jitter and any uncached
  dynamic system block, which is re-billed by design. Set `0` to disable and
  detect only total loss through `expectedReadAtLeast`.
- `log?` (`(line: string) => void`): Sink for log lines. Defaults to `console.error`. Use a [`Logger`](https://zidane.si/docs/reference/index.md#Logger) or
  a test buffer here when you want the output to land somewhere other than
  stderr.

  NOT every line is a dimension diff: unless `onEmitError` is
  provided, `'cache:break'` handler errors are also routed here (prefixed
  `cache-break: 'cache:break' handler threw:`). A host that counts `log`
  invocations as "dimension changes" — the journal-mode wiring — should
  either pass `onEmitError` or count via the `'cache:break'` hook instead.
- `onEmitError?` (`(error: unknown) => void`): Sink for errors thrown by `'cache:break'` hook consumers. The emission is
  try/caught so a UI bug in a handler can't unwind into the loop and abort
  the run; this is where that error surfaces. Defaults to the `log`
  channel (never a louder one — hosts route `log` away from stderr for good
  reasons). Pass a dedicated sink when `log` invocations are being counted
  as metrics.
- `sessionBaseline?` (`boolean`, default `true`): Share the comparison baseline with other agents bound to the same session.
  When on, the last request of a top-level run on a session is kept per
  session id at process level, and a newly built agent compares its first
  request against it, so a cache break caused by rebuilding the agent is
  reported instead of starting from a blank slate. Subagent runs never
  share. Set `false` to keep the baseline per logger.
- `verbose?` (`boolean`): When `true`, emit a log line on EVERY turn (not just cache breaks) with
  the per-dimension hashes. Useful for offline correlation; off by default
  to keep the logger zero-noise on happy paths.

Source: [src/cache-telemetry.ts:298](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L298)

### `CacheDimensionDiff`

Interface.

Result of [`diffCacheDimensions`](https://zidane.si/docs/reference/agent.md#diffCacheDimensions).

```ts
interface CacheDimensionDiff
```

**Properties**

- `changed` (`readonly CacheDimensionName[]`): Dimension names whose hashes differ between the two snapshots.
- `details` (`Readonly<Partial<Record<CacheDimensionName, string>>>`): Per-dimension details — only filled for changed dimensions.
- `messagePrefixDivergedAt?` (`number`): Message index where the prefixes diverged, when `changed` includes
  `'messagePrefix'`. Chunk-aligned values (multiples of the compaction
  chunk) point at tail compaction / media age-out; non-aligned values point
  at stale-read stubbing or host-side history surgery.
- `systemSections?` (`readonly CacheSystemSection[]`): The parts of `system` that changed, when `changed` includes `'system'`
  and both snapshots carry `CacheDimensionSnapshot.systemSections`.
  Empty when the change could not be traced to one part.

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

### `CacheDimensionSnapshot`

Interface.

Snapshot of per-request dimensions that contribute to the provider-side
prompt cache key. Hashed at `turn:before` and stashed for diffing on the
next request.

Hashes are FNV-1a 32-bit (cheap, no allocations on the hot path,
collision rate good enough for diff detection — we're not signing
anything). For verbose output the helper also captures byte lengths so
`system: 1024 → 1031` reads like a real diff line.

```ts
interface CacheDimensionSnapshot
```

**Properties**

- `effortHint` (`boolean`): Whether the level-derived effort hint is emitted — `true` when absent.
- `messageCount` (`number`): Number of messages in the request.
- `messageHashes` (`readonly number[]`): Per-message content hashes, in order. Compared POSITIONALLY against the
  previous turn so that appending messages — what every healthy turn does —
  is not mistaken for a rewrite. A single cumulative hash cannot make that
  distinction: it changes on plain growth too, and would name this dimension
  on literally every break, drowning the real culprit.

  Tracked because the expensive real-world breaks live here, not in the
  request scaffold: compaction, stale-read elision, media age-out and history
  trimming all rewrite the transcript mid-conversation, and none of them touch
  `system`, `tools`, or the model id. A run can lose its entire transcript to
  a cache miss with every other dimension reading perfectly stable.
- `model` (`string`): Model id.
- `systemBytes` (`number`): Byte length of `options.system`.
- `systemHash` (`number`): Hash of `options.system` (with boundary marker preserved).
- `systemSections?` (`Readonly<Partial<Record<CacheSystemSection, number>>>`): Hash of each part of `system` the snapshot could find, keyed by
  [`CacheSystemSection`](https://zidane.si/docs/reference/agent.md#CacheSystemSection). A section missing from the request has no key.
  The host's parts (`hostStatic`, `hostDynamic`) are hashed with the other
  sections removed, so a change in a catalog is not also blamed on the host.
  Absent on snapshots built before this field existed.
- `taskBudget` (`number`): Whole-task thinking budget (Anthropic adaptive-only) — `0` when absent.
- `thinking` (`string`): Thinking level — `'off'` when absent.
- `thinkingBudget` (`number`): Exact thinking budget — `0` when absent.
- `toolsCount` (`number`): Number of tools in the array.
- `toolsHash` (`number`): Hash of the JSON-serialized `options.tools` array.

Source: [src/cache-telemetry.ts:73](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L73)

### `CacheDriftInfo`

Interface.

Structured payload of the `'cache:drift'` agent hook, emitted by
[`installCacheBreakLogger`](https://zidane.si/docs/reference/agent.md#installCacheBreakLogger) at `turn:before` whenever the outgoing
request DIVERGES from the previous completed turn's — any tracked dimension,
including an in-place rewrite of the message prefix.

Drift is the PREDICTIVE counterpart of `'cache:break'`: it fires from the
request bytes alone, before (and regardless of whether) the provider
confirms a miss. That makes it the signal that works everywhere the
confirmed-cost path cannot see — providers that report no cache usage, and
format-casting gateways where this wire is the last observable point before
the cast. A turn that drifts here and then reports a healthy `cacheRead`
means the provider absorbed the change (e.g. only the tail moved); a turn
that drifts AND breaks gives the break its attribution for free.

Intentional rewrites drift too, by design — that is the attribution story
for planned invalidations: tail compaction and media age-out cut on
chunk-aligned indices (`divergedAt` a multiple of the compaction chunk),
stale-read stubbing rewrites a read's own position, host history surgery
lands anywhere. `details.messagePrefix` carries the same hint in prose.

```ts
interface CacheDriftInfo
```

**Properties**

- `changed` (`CacheDimensionName[]`): Dimensions that differ from the previous completed turn's request.
- `details` (`Partial<Record<CacheDimensionName, string>>`): Human-readable per-dimension diffs, keyed by the entries of `changed`.
- `divergedAt?` (`number`): Message index of an in-place prefix rewrite — see `CacheDimensionDiff.messagePrefixDivergedAt`.
- `messageCount` (`number`): Number of messages in the outgoing request.
- `model` (`string`): Model id of the OUTGOING request (post-fallback, pre-provider).
- `previousMessageCount` (`number`): Number of messages in the previous completed turn's request.
- `systemSections?` (`CacheSystemSection[]`): The parts of the system prompt that changed, when `changed` includes `'system'` — see `CacheDimensionDiff.systemSections`.
- `turn` (`number`): 1-based turn index, installer-lifetime monotonic — see `CacheBreakInfo.turn`.
- `turnId` (`string`): Id of the turn the outgoing request belongs to.

Source: [src/cache-telemetry.ts:272](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L272)

### `CachePromptSections`

Interface.

Texts of the system prompt sections a snapshot can name: the sections the
SDK appended and the host's rules block. Each must be a substring of the
request's `system` to count; anything else is ignored.

```ts
interface CachePromptSections
```

**Properties**

- `mcpInstructions?` (`string`): The MCP server instructions the SDK appended.
- `rules?` (`string`): The rules block the run passed as `contextSections.rulesBlock`.
- `skillsCatalog?` (`string`): The skills catalog the SDK appended.
- `toolsCatalog?` (`string`): The searchable tools catalog the SDK appended.

Source: [src/cache-telemetry.ts:142](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L142)

### `McpToolMeta`

Interface.

Identity of a discovered MCP tool, passed to [`McpToolWrap`](https://zidane.si/docs/reference/agent.md#McpToolWrap).

```ts
interface McpToolMeta
```

**Properties**

- `server` (`string`): Server name from `mcpServers` config (the `<server>` in `mcp_<server>_<tool>`).
- `toolName` (`string`): The server-side tool name, un-namespaced.

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

### `RunSummary`

Interface.

Postmortem snapshot of one `agent.run()`. Strictly serializable — every
field round-trips through `JSON.stringify` / `JSON.parse` without loss
so a log aggregator can ingest it as-is.

```ts
interface RunSummary
```

**Properties**

- `agentName?` (`string`): Name of the agent, when set.
- `blocks` (`RunSummaryBlock[]`): Tool calls that did not run.
- `budgetEvents` (`RunSummaryBudget[]`): Budget events recorded during the run.
- `byModel` (`RunSummaryByModel[]`): Totals broken down by model.
- `children?` (`RunSummary[]`): Postmortem snapshots of child runs that bubbled their stats up via
  `spawn:complete`. Only present when the run actually spawned.
- `depth` (`number`): Subagent depth. 0 is the top-level agent.
- `durationMs` (`number`): Wall-clock duration of the run.
- `endedAt` (`number`): Run end, in Unix milliseconds.
- `errors` (`RunSummaryError[]`): Errors recorded during the run.
- `pairingRepairs` (`Record<string, number>`): Counts of pairing repairs, keyed by repair mode.
- `parentRunId?` (`string`): Id of the parent run, for a subagent run.
- `repeatGuardEvents` (`RunSummaryRepeatGuard[]`): Repeat-guard escalations. An `action: 'steer'` entry means the tool still
  executed and got a steering message appended to its result. An
  `action: 'abort'` entry means the guard terminated the run via the agent's
  AbortController (the run finalizes as `'aborted'`).
- `runId?` (`string`): Id of the run.
- `startedAt` (`number`): Run start, in Unix milliseconds.
- `status` (`'completed' | 'aborted'`): How the run ended.
- `streamRetries` (`RunSummaryRetry[]`): Provider-stream retries the loop absorbed. Empty on healthy runs; on
  incident runs the summed `delayMs` explains wall time that appears
  nowhere else (not in turns, not in tool durations).
- `totals` (`RunSummaryTokens`): Token and cost totals, including child runs.
- `turns` (`number`): Number of turns of the run's own loop.
- `validationRejects` (`RunSummaryValidation[]`): Tool calls rejected by input validation.

Source: [src/run-summary.ts:168](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L168)

### `RunSummaryBlock`

Interface.

One tool call that did not run: blocked by a gate, unknown, or rejected for invalid input.

```ts
interface RunSummaryBlock
```

**Properties**

- `callId` (`string`): Id of the tool call.
- `outcome` (`'gate-block' | 'unknown' | 'invalid-input'`): Why the call did not run.
- `reason?` (`string`): Reason given by the gate, when there was one.
- `toolName` (`string`): Name of the tool the model called.

Source: [src/run-summary.ts:94](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L94)

### `RunSummaryBudget`

Interface.

One output-size or tool-count budget event.

```ts
interface RunSummaryBudget
```

**Properties**

- `kind` (`'bytes' | 'tool-count'`): Budget type: a byte budget or a per-tool call budget.
- `limit` (`number`): Configured budget.
- `mode?` (`'steer' | 'block'`): `mode` for `'tool-count'`; absent for byte budgets.
- `observed` (`number`): Value that crossed the budget: bytes or call count.
- `toolName?` (`string`): Tool name (for `'tool-count'`); absent for byte budgets.
- `turnId?` (`string`): Id of the turn the event happened in.

Source: [src/run-summary.ts:116](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L116)

### `RunSummaryByModel`

Interface.

Token and cost totals for one model within a run.

```ts
interface RunSummaryByModel
```

**Properties**

- `cacheCreation` (`number`): Tokens written to the prompt cache.
- `cacheRead` (`number`): Tokens read from the prompt cache.
- `cost` (`number`): Cost in USD.
- `input` (`number`): Input tokens, excluding cache tokens.
- `modelId` (`string`): Model id, or `'(unknown)'` when the provider did not report one.
- `output` (`number`): Output tokens.
- `turns` (`number`): Number of turns served by the model.

Source: [src/run-summary.ts:52](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L52)

### `RunSummaryCollector`

Interface.

Collector that builds a [`RunSummary`](https://zidane.si/docs/reference/agent.md#RunSummary) from agent hooks. Created by [`createRunSummaryCollector`](https://zidane.si/docs/reference/agent.md#createRunSummaryCollector).

```ts
interface RunSummaryCollector
```

**Properties**

- `install` (`(hooks: Hookable<AgentHooks>) => () => void`): Install the collector's hook handlers. Returns an uninstall fn.
- `latest` (`() => RunSummary | undefined`): Most-recent summary; `undefined` until the first `agent:done` fires.

Source: [src/run-summary.ts:235](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L235)

### `RunSummaryCollectorOptions`

Interface.

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

```ts
interface RunSummaryCollectorOptions
```

**Properties**

- `onSummary?` (`(summary: RunSummary) => void`): Called with the assembled [`RunSummary`](https://zidane.si/docs/reference/agent.md#RunSummary) on every `agent:done`.
  Synchronous — heavy I/O should be deferred (e.g. via `setImmediate`).

Source: [src/run-summary.ts:226](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L226)

### `RunSummaryError`

Interface.

One error recorded during a run.

```ts
interface RunSummaryError
```

**Properties**

- `callId?` (`string`): Id of the tool call, for tool errors.
- `childId?` (`string`): Spawn id of the child, for spawn errors.
- `errorType?` (`string`): Error class name, when known.
- `kind` (`'spawn' | 'tool' | 'mcp' | 'stream' | 'mcp-tool'`): Where the error came from: a provider stream, a tool, an MCP tool call, an MCP server, or a spawn.
- `message` (`string`): Error message.
- `requestId?` (`string`): Provider request id, when the provider reported one.
- `server?` (`string`): MCP server name, for MCP errors.
- `statusCode?` (`number`): HTTP status code, for provider errors.
- `toolName?` (`string`): Name of the tool, for tool errors.
- `turnId?` (`string`): Id of the turn the error happened in, when known.

Source: [src/run-summary.ts:70](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L70)

### `RunSummaryRepeatGuard`

Interface.

One repeat-guard escalation.

```ts
interface RunSummaryRepeatGuard
```

**Properties**

- `action` (`'steer' | 'block' | 'abort'`): What the guard did: blocked the call, steered the model, or aborted the run.
- `count` (`number`): Observed repeat count that tripped the guard.
- `threshold` (`number`): Configured repeat threshold.
- `toolName` (`string`): Name of the repeated tool.
- `turnId?` (`string`): Id of the turn the event happened in.

Source: [src/run-summary.ts:132](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L132)

### `RunSummaryTokens`

Interface.

Token and cost totals of a run.

```ts
interface RunSummaryTokens
```

**Properties**

- `cacheCreation` (`number`): Tokens written to the prompt cache.
- `cacheRead` (`number`): Tokens read from the prompt cache.
- `cost?` (`number`): Cost in USD. Absent when no price was known.
- `input` (`number`): Input tokens, excluding cache tokens.
- `output` (`number`): Output tokens.
- `ttftMs?` (`number`): First observable byte from the provider, ms from run start.

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

### `RunSummaryValidation`

Interface.

One tool call rejected by input validation.

```ts
interface RunSummaryValidation
```

**Properties**

- `callId` (`string`): Id of the tool call.
- `reason` (`string`): Validation failure message.
- `toolName` (`string`): Name of the tool the model called.

Source: [src/run-summary.ts:106](https://github.com/Tahul/zidane/blob/main/src/run-summary.ts#L106)

## Types

### `AgentHookMap`

Type.

Strongly-typed hook registration map accepted on `AgentOptions.hooks`
(and therefore on any [`Preset`](https://zidane.si/docs/reference/presets.md#Preset)). Each entry is a single handler or an
array of handlers — arrays are what [`composePresets`](https://zidane.si/docs/reference/presets.md#composePresets) produces when
multiple presets register handlers for the same event.

Handlers registered here live for the **lifetime of the agent**. They fire
across every `run()`, mirroring a manual `agent.hooks.hook(event, fn)` call
made right after `createAgent` returns. For per-run handlers (auto-detached
at run end) use `AgentRunOptions.hooks` instead.

```ts
type AgentHookMap = Partial<{ [K in keyof AgentHooks]: AgentHookHandlers[K] | AgentHookHandlers[K][] }>
```

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

### `CacheDimensionName`

Type.

Cache-affecting dimension names.

```ts
type CacheDimensionName =
  | 'system'
  | 'tools'
  | 'model'
  | 'thinking'
  | 'thinkingBudget'
  | 'taskBudget'
  | 'effortHint'
  | 'messagePrefix'
```

Source: [src/cache-telemetry.ts:175](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L175)

### `CacheSystemSection`

Type.

A part of the system prompt that a `system` change can be traced to.

- `skillsCatalog`: the skills catalog the SDK appends.
- `toolsCatalog`: the searchable tools catalog the SDK appends.
- `mcpInstructions`: the MCP server instructions the SDK appends.
- `rules`: the rules block the run passed as `contextSections.rulesBlock`.
- `hostStatic`: the rest of the prompt above `SYSTEM_PROMPT_BOUNDARY`, or
  the whole rest of the prompt when it has no boundary.
- `hostDynamic`: the rest of the prompt below `SYSTEM_PROMPT_BOUNDARY`.

```ts
type CacheSystemSection =
  | 'skillsCatalog'
  | 'toolsCatalog'
  | 'mcpInstructions'
  | 'rules'
  | 'hostStatic'
  | 'hostDynamic'
```

Source: [src/cache-telemetry.ts:129](https://github.com/Tahul/zidane/blob/main/src/cache-telemetry.ts#L129)

### `McpConnector`

Type.

Custom MCP connector — the seam that owns "connect `mcpServers`, discover
their tools, hand them back". Supply one via `AgentOptions.mcpConnector`
to inject a custom transport / auth provider (e.g. OAuth via
`connectMcpServers(configs, undefined, hooks, { buildAuthProvider })`).

To re-shape discovered tools, use the separate
`AgentOptions.mcpToolWrap` hook: it composes with whatever
connector is in play.

The agent passes its own `hooks` so a connector that delegates to
[`connectMcpServers`](https://zidane.si/docs/reference/mcp.md#connectMcpServers) can forward them and keep every `mcp:*` event
firing. A connector is responsible for whatever the default path does that
it cares about (config normalization, namespacing, per-server
`instructions`) — delegating to `connectMcpServers` preserves all of it.

The agent always passes `extras.execution` — a LAZY binding whose
`getHandle()` spawns/resolves the execution handle only when a
`scope: 'context'` stdio server actually builds its transport. Forward it
untouched to `connectMcpServers` (as `{ execution }`) or those servers
fail bootstrap; a connector that never builds such a transport (e.g. one
reusing a live parent connection) simply never triggers the spawn. There
is no ordering guarantee beyond that — do not assume a handle exists
before `getHandle()` resolves. Declaring only `(configs, hooks)` stays
valid for host-only setups.

```ts
type McpConnector = (configs: McpServerConfig[], hooks: Hookable<AgentHooks>, extras?: { execution?: McpExecutionBinding }) => Promise<McpConnection>
```

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

### `McpToolWrap`

Type.

Wrap each discovered MCP tool right after bootstrap, before it's registered
into the agent's tool map — see `AgentOptions.mcpToolWrap`. The MCP
counterpart to wrapping native tools yourself before `createAgent`.

```ts
type McpToolWrap = (tool: ToolDef, meta: McpToolMeta) => ToolDef
```

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

## Re-exports

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

- [zidane/tools](https://zidane.si/docs/reference/tools.md): [`getReadState`](https://zidane.si/docs/reference/tools.md#getReadState), [`hydrateReadStateFromSession`](https://zidane.si/docs/reference/tools.md#hydrateReadStateFromSession), [`hydrateReadStateFromSessionAsync`](https://zidane.si/docs/reference/tools.md#hydrateReadStateFromSessionAsync), [`HydrateReadStateOptions`](https://zidane.si/docs/reference/tools.md#HydrateReadStateOptions), [`ReadStateEntry`](https://zidane.si/docs/reference/tools.md#ReadStateEntry), [`readStateKey`](https://zidane.si/docs/reference/tools.md#readStateKey), [`ReadStateMap`](https://zidane.si/docs/reference/tools.md#ReadStateMap), [`resolveReadStateMap`](https://zidane.si/docs/reference/tools.md#resolveReadStateMap)
