# Agents

> Build an agent, run a task, and control a run in flight.

`createAgent()` binds a provider, a system prompt, tools and defaults. `agent.run()` runs one task. It streams the reply and runs tool calls until the model stops, a limit trips or you abort the run.

## Create an agent

```ts title="agent.ts"
import { createAgent } from 'zidane'
import { basic } from 'zidane/presets'
import { anthropic } from 'zidane/providers'

const agent = createAgent({
  ...basic,
  provider: anthropic({ apiKey: 'sk-ant-...' }),
})

const stats = await agent.run({ prompt: 'build a REST API' })
console.log(`Done in ${stats.turns} turns`)
```

All options:

```ts
createAgent({
  provider,                          // required, an array enables fallback
  name: 'basic',                     // display name (traces/logs)
  system: 'You are a helpful...',
  tools: { shell, readFile },        // default: {}
  toolAliases: { shell: 'Bash' },    // canonical to LLM-facing names
  toolDescriptions: {                // override any tool's description (native or MCP)
    shell: cur => `${cur}\nNever invoke sudo.`,
    mcp_github_create_pull_request: 'Open a PR. Always target `develop`.',
  },
  session,
  behavior: {
    maxConcurrentTools: 10,          // default 10
    maxTurns: 50,                    // default: unlimited
    maxTokens: 16384,                // default: the model's max output
    thinkingBudget: 10240,
    cache: true,                     // default true
    schema: undefined,               // JSON Schema for structured output
    maxCostUsd: undefined,
    maxTotalTokens: undefined,
  },
  execution: createProcessContext(),
  mcpServers: [],
  eager: true,                       // pre-connect MCP in the background (default false)
  skills: {},
})
```

| Option | Details |
| - | - |
| `provider` | [Providers](https://zidane.si/docs/providers.md) |
| `hooks` | Agent-lifetime handlers, keyed by event name ([Hooks](https://zidane.si/docs/hooks.md)). An unknown event name throws at construction. |
| `tools`, `toolAliases`, `toolDescriptions` | [Tools](https://zidane.si/docs/tools.md) |
| `session` | [Sessions](https://zidane.si/docs/sessions.md) |
| `durable`, `sessionId` | [Crash recovery](https://zidane.si/docs/durability.md). `sessionId` needs `durable` and loads or creates that session in the runtime's store. |
| `behavior` | [Hooks](https://zidane.si/docs/hooks.md), [Errors](https://zidane.si/docs/errors.md#run-level-budgets) |
| `execution` | [Execution contexts](https://zidane.si/docs/execution-contexts.md) |
| `mcpServers`, `eager` | [MCP servers](https://zidane.si/docs/mcp.md) |
| `skills` | [Skills](https://zidane.si/docs/skills.md) |

## Run a task

```ts
await agent.run({
  prompt: 'your task',       // string or PromptPart[] (text, image, audio, video, document)
  model: 'claude-opus-4-7',  // or an ordered fallback list
  system: 'be concise',
  thinking: 'medium',
  behavior: { maxTurns: 10, maxTokens: 4096, thinkingBudget: 8192 },
  tools: {},                 // {} = no tools for this run
  signal: abortController.signal,
})
```

`run.behavior` overrides `agent.behavior`, which overrides the defaults. Omit `prompt` to resume a session that already has turns. `run()` rejects when another run of the same agent is in progress. For a `model` list, see [model fallback](https://zidane.si/docs/providers.md#model-fallback).

`systemNotes` adds notes to the run's prompt turn, to tell the model about a change (mode, environment, effort) without editing the system prompt. Anthropic models that accept mid-conversation system messages get them as `role: "system"` messages. Other models get `<system-reminder>` text. On Anthropic, a note with `effort` changes the effort level, per message where the model supports it, so the top-level effort and the cache stay. Other providers ignore `effort`. `systemFreeze` sends a saved system prompt in place of the fresh one, so the prompt cache keeps hitting. Both are in [keeping the prefix stable](https://github.com/Tahul/zidane/blob/main/docs/SKILL.md#keeping-the-prefix-stable).

## Presets

A preset is a `Partial<AgentOptions>` without `provider`, `execution`, `session` or `mcpConnector`. `basic` and its tools are listed in [built-in tools](https://zidane.si/docs/tools.md#built-in-tools).

```ts
import { basic, basicTools, composePresets, definePreset } from 'zidane/presets'

createAgent({ ...basic, provider, system: 'be concise' })
createAgent({ ...definePreset({ name: 'researcher', tools: basicTools }), provider })
createAgent({ ...composePresets(basic, mine), provider })  // merges hooks and tools per field
createAgent({ provider })                                   // no tools
```

A plain spread is shallow. The `hooks` or `tools` of a later preset replace the earlier ones. `composePresets` merges them. It keeps only the preset fields that it knows how to merge, so pass `extensions`, `decisions`, `artifacts` and `clock` to `createAgent` directly.

## Thinking

| Level | Effect |
| - | - |
| `off` | No reasoning. |
| `minimal`, `low`, `medium`, `high`, `xhigh`, `max` | Effort tier, mapped to the provider's reasoning field (Anthropic `output_config.effort`, OpenRouter `reasoning.effort`, and so on). Providers clamp to the tiers they support. |
| `adaptive` | The model decides per turn. Anthropic only. Other providers send no reasoning. |

`behavior.thinkingBudget` pins an exact token budget on top of a level:

```ts
await agent.run({ prompt: '…', thinking: 'high' })
await agent.run({ prompt: '…', thinking: 'high', behavior: { thinkingBudget: 50000 } })  // exact
```

| Key | Does |
| - | - |
| `modelOptions` (run) or `behavior.modelOptions` | Extra model options, such as Anthropic's fast mode: `{ fast: true }` |
| `behavior.effortHint: false` | Stops sending the effort hint to Anthropic |
| `behavior.taskBudget` | Caps thinking across a whole task (Anthropic adaptive thinking only) |
| `behavior.thinkingDecay` | Lowers the budget on later turns ([tool dispatch](https://zidane.si/docs/hooks.md#tool-dispatch)) |

Thinking is saved as `{ type: 'thinking', text }` session blocks and streams through `stream:thinking`.

## Structured output

`behavior.schema` forces the final response to a JSON Schema. On the final turn only, zidane forces a synthetic output tool with `toolChoice`. The result lands on `stats.output` and fires the `output` hook.

```ts
const stats = await agent.run({
  prompt: 'Extract the entities',
  behavior: {
    schema: {
      type: 'object',
      properties: { name: { type: 'string' }, age: { type: 'number' } },
      required: ['name', 'age'],
    },
  },
})
console.log(stats.output) // { name: 'Alice', age: 30 }
```

With Zod v4, pass `zodToJsonSchema(z.toJSONSchema(schema))`. It strips `$schema`, which some providers reject.

## Run control

```ts
agent.isRunning             // boolean
agent.turns                 // SessionTurn[]
agent.steer('new focus')    // inject mid-run, delivered between tool calls; also takes PromptPart[]
agent.followUp('next task') // queue for after the run finishes
agent.abort()               // cancel the whole run
await agent.suspend({ graceMs: 5000 }) // stop so a later resume() continues it
await agent.resume()        // continue the run a dead process left, or null
agent.cancelTool(callId)    // cancel one in-flight tool call
await agent.reset()         // clear turns and queues
await agent.warmup()        // pre-connect MCP and resolve skills (idempotent)
await agent.waitForIdle()   // wait for the current run to finish
await agent.compact()       // summarize older turns now (sessions › compaction)
agent.cacheExpiresAt()      // epoch ms when the prompt cache likely expires, or null
agent.cacheImpact({ model: 'claude-haiku-4-5' }) // { tokens, reasons, expired }
await agent.destroy()       // tear down the execution context and MCP connections
```

`cancelTool(callId)` aborts the `ctx.signal` of that call, fires `tool:cancelled` and returns a cancelled result with `isError: true`. Other calls in the batch keep running. It returns `true` when it cancelled a live call. A repeat call returns `false`. A cancel sent before the call starts waits and applies when the call registers.

`steer` works only during a run. The model gets the message at the next tool boundary. `followUp` waits until the model returns a final answer, then continues the same run with the message. Both return `false` and queue nothing when no run is in flight or the run is aborted. The apps keep one queue per session instead ([queue and steer](https://zidane.si/docs/apps.md#queue-and-steer)).

`suspend({ graceMs })` stops the run so `resume()` can continue it later, for a host that drains before it exits. The model stream stops at once. Running tool calls get `graceMs` ms to finish (unset: they all finish). A call still running after that stays pending. `run()` resolves with `suspended: true`. On a replay-mode runtime (`restateRuntime`, which replays a journal), `suspend()` aborts the run instead.

`resume()` continues the session's run that a dead process left unfinished, with the options its run record kept. It resolves `null` when there is nothing to continue. It rejects without a checkpoint-mode runtime in `durable` (SQLite or memory, which store finished steps as rows). Details: [crash recovery](https://zidane.si/docs/durability.md).

`cacheExpiresAt()` adds the cache lifetime of the last request (`behavior.cacheTtl`, else 5 minutes) to the time it was sent. The provider can evict earlier. `cacheImpact(change)` estimates the prompt tokens that a change (`model`, `thinking`, `system: true`, `tools: true`) would make the next request read again instead of from the cache. It compares the change with the last request as is, so do not ask about a change that you send without touching the request, such as effort in a note. [Compaction by hand](https://zidane.si/docs/sessions.md#compact-by-hand) covers `compact()`.

`agent.killBackgroundTask(taskId)` stops a [background task](https://zidane.si/docs/tools.md#background-tasks) from host code. The model sees the exit as a `<task-notification>` on its next turn.

`await using agent = createAgent(...)` calls `destroy` at scope exit (`Symbol.asyncDispose`, Node 20+ or Bun).

## Usage and cost

```ts
import { flattenTurns, statsByModel } from 'zidane'

stats.totalIn / stats.totalOut / stats.cost  // parent run plus every spawned child
stats.turns / stats.turnUsage                // parent loop only
stats.children                               // ChildRunStats[] in completion order
stats.timeTillFirstTokenMs                   // ms to first stream or tool event

flattenTurns(stats)   // every TurnUsage in the tree (DFS)
statsByModel(stats)   // Map<modelId, { input, output, cost, cacheRead, cacheCreation, turns }>
```

## Behavior options

`behavior` holds every loop setting. Set it on the agent or per run. The value of the run wins. Types and defaults are in [`AgentBehavior`](https://zidane.si/docs/reference/types.md).

| Purpose | Keys | Details |
| - | - | - |
| Turns and budgets | `maxTurns`, `maxTurnsWarning`, `maxTurnsWarningMessage`, `maxCostUsd`, `maxTotalTokens`, `maxWallMs` | [Run-level budgets](https://zidane.si/docs/errors.md#run-level-budgets) |
| Retries and timeouts | `retry`, `providerStreamStartTimeoutMs`, `providerStreamTimeoutMs`, `providerStreamIdleTimeoutMs`, `mcpConnectTimeoutMs`, `hookTimeoutMs`, `spawnTimeoutMs`, `destroyTimeoutMs`, `toolBatchTimeoutMs` | [Retry with backoff](https://zidane.si/docs/errors.md#retry-with-backoff). The watchdogs fail a stuck stream, connector, hook, spawn or teardown after that many ms. |
| Output and thinking | `maxTokens`, `thinkingBudget`, `thinkingDecay`, `taskBudget`, `effortHint`, `thinkingOverrunPolicy`, `modelOptions`, `schema`, `resumeOnMaxTokens`, `maxConsecutiveLengthResumes`, `maxConsecutivePauseTurns`, `maxConsecutiveRefusalResumes` | [Thinking](#thinking), [structured output](#structured-output). The resume keys continue a reply cut by the output cap, an Anthropic `pause_turn` or a refusal, up to that many times. |
| Model metadata | `modelCatalog`, `contextWindow` | Override a model's output ceiling, pricing, context window or reasoning support |
| Prompt cache | `cache`, `cacheTtl` | [Prompt caching](https://zidane.si/docs/providers.md#prompt-caching) |
| Tool scheduling | `maxConcurrentTools`, `toolBatchExecutor` | [Tool dispatch](https://zidane.si/docs/hooks.md#tool-dispatch). `toolBatchExecutor` replaces the scheduler. |
| Tool output | `toolOutputBudget`, `toolOutputBudgetExcludeTools`, `readFileDefaults`, `readLineNumbers`, `elideStaleReads` | [Output and context budgets](https://zidane.si/docs/hooks.md#output-and-context-budgets) |
| Guards and nudges | `dedupReads`, `dedupTools`, `repeatGuard`, `toolBudgets`, `requireReadBeforeEdit`, `trackShellReads`, `writeFileExistingPolicy`, `textToolCallRescue`, `unknownToolRecovery`, `progressNudges`, `shellToolNudges` | [Read dedup and guards](https://zidane.si/docs/hooks.md#read-dedup-and-read-before-edit-guard). The nudges add one-line hints when the model stalls, writes a tool call as text, calls an unknown tool, or uses `shell` for work a dedicated tool does. |
| Compaction | `autoCompact`, `compactStrategy`, `compactThreshold`, `compactThresholdWindowFraction`, `compactLowWaterFraction`, `compactSkip`, `compactKeepTurns` | [Compaction](https://zidane.si/docs/sessions.md#compaction) |
| Media | `mediaKeepTurns`, `mediaBudget`, `maxImageEdge` | Replace old images and documents in the request with markers after N messages (`mediaKeepTurns`) or near the provider's limits (`mediaBudget`). Shrink large PNG images once (`maxImageEdge`). |
| Saving results to disk | `persistTurns`, `persistThreshold`, `persistExcludeTools`, `persistDir`, `persistMaxBytes` | Save each turn as it ends, and replace large tool results with a short preview plus a file path |
| Background tasks | `tasksDir`, `shellLongTimeoutMs`, `disableBackgroundTasks`, `backgroundOutputCap`, `backgroundStallWatchdogMs` | [Background tasks](https://zidane.si/docs/tools.md#background-tasks) |
| MCP and tool discovery | `toolDisclosure`, `toolSearch`, `mcpToolNameSeparator`, `surfaceMcpInstructions`, `codemode` | [MCP](https://zidane.si/docs/mcp.md), [codemode](https://zidane.si/docs/reference/codemode.md) |
| Message repair | `strictToolPairing`, `maxPairingRepairsPerTurn` | zidane repairs a tool call that has no result before it sends the request. `strictToolPairing` fails instead, with `AgentToolPairingError` ([errors](https://zidane.si/docs/errors.md)). |
| Web tools | `fetchUrlAllowHosts`, `fetchUrlCacheTtlMs` | Limit `fetch_url` and `web_search` to some hosts, and cache fetched pages |
| Host data | `extensions` | A bag for host settings that zidane itself never reads |

### Other agent options

| Option | Does |
| - | - |
| `runTransforms`, `behaviorTransforms`, `toolCatalogTransforms`, `mcpConfigTransforms` | Rewrite run options, behavior, the tool list or MCP configs before each run. [Extensions](https://zidane.si/docs/extensions.md) fill these. |
| `skillPolicies` | Turn a skill on from the run's context |
| `toolResultMetadata`, `contextBreakdownSections` | Read metadata on tool results, and add rows to a context breakdown |
| `readState`, `toolState` | Durable storage for read tracking and `ToolContext.state` |
| `mcpToolWrap` | Wrap every MCP tool after discovery |
| `toolKill` | Tune `cancelTool`. `toolKill.defaultMessage` sets the reason when `cancelTool` gets none. |
| `handle` | An execution handle that the host owns. The agent never spawns or destroys it. |
| `mcpConnector` | Replace the MCP connector, for a custom transport or auth provider ([OAuth 2.1](https://zidane.si/docs/mcp.md#oauth-21)) |
| `decisions` | Pin the provider, model or timeout for `agent.decide`, or `false` to turn decisions off |
| `clock` | The time and UUID source. A replay-mode durable runtime (`restateRuntime`) uses its own journaled clock instead. |
| `extensions` | A merged extension registry. [Extensions](https://zidane.si/docs/extensions.md) fill it. |
| `artifacts` | `false` turns off session artifacts. Default on. |

`run()` also takes `modelOptions`, `resumeRunId` (continue the turns of a past run), `runLabel`, `contextSections`, `tracingContext`, `clock`, `leaseRenewMs` (how often a top-level checkpoint run renews its session's lease, default 60 s), and `depth`, `parentRunId` and `parentCallId` for hosts that run sub-agents themselves. See [`AgentRunOptions`](https://zidane.si/docs/reference/types.md).

## Reference

- [`zidane/agent`](https://zidane.si/docs/reference/agent.md)
- [`zidane/types`](https://zidane.si/docs/reference/types.md)
- [`zidane/presets`](https://zidane.si/docs/reference/presets.md)
