> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents and tools

> The two building blocks of @duraton/agent-kit: agent() declares the model, instructions and tools; tool() declares one callable the model may use, with its schema, behaviour hints and context bounds.

## `agent`

`agent(ctx, id, options)` takes the workflow context, a stable step id, and:

<ResponseField name="model" type="string" required>
  The model each turn calls.
</ResponseField>

<ResponseField name="prompt" type="string" required>
  The task. Every turn after the first sees it again together with the tool results so far.
</ResponseField>

<ResponseField name="maxIterations" type="number" required>
  Hard cap on turns. Required, not defaulted: how long an agent may keep calling tools is your decision.
</ResponseField>

<ResponseField name="instructions" type="string">
  The system prompt - who the agent is and how it should behave.
</ResponseField>

<ResponseField name="tools" type="ToolDef[]">
  The tools the model may call, each from tool().
</ResponseField>

<ResponseField name="mcpServers" type="McpServerDef[]">
  External MCP servers whose tools join the ones above. Each server is discovered once as a durable step.
</ResponseField>

<ResponseField name="approval" type="ApprovalRule">
  The gate for every tool that has not answered for itself. See Which rule applies to a tool below.
</ResponseField>

<ResponseField name="maxApprovals" type="number">
  Ceiling on the human decisions this agent may ask for. Omitted, there is none. See Capping the decisions an agent asks for below.
</ResponseField>

<ResponseField name="output" type="Record<string, unknown>">
  A JSON Schema the final answer must satisfy. Set, the answer comes back parsed as the object rather than as text. See Answering in a shape below.
</ResponseField>

<ResponseField name="strategy" type="&#x22;function-calling&#x22;">
  How a turn is composed. Defaults to function-calling.
</ResponseField>

<ResponseField name="provider" type="ProviderName | AIProvider">
  A provider name resolves through the built-in registry; pass an AIProvider to run against your own adapter.
</ResponseField>

<ResponseField name="apiKey" type="string">
  Passed per call and never stored. Omit it to use the provider SDK's conventional env var.
</ResponseField>

<ResponseField name="maxTokens" type="number">
  Per-turn output cap, passed to the provider.
</ResponseField>

<ResponseField name="temperature" type="number">
  Passed to the provider when set.
</ResponseField>

<ResponseField name="stream" type="boolean">
  Stream each turn's tokens onto the run timeline as they arrive. Opt-in; the turn's durable result is unchanged either way. See [Streaming agent turns](/ai/streaming#streaming-agent-turns).
</ResponseField>

<ResponseField name="promptCache" type="PromptCacheScope">
  Ask the provider to bill this agent's repeated prefix at its own cached rate. "conversation" is the scope an agent wants: every turn re-sends the instructions, the tool declarations and every exchange so far. Opt-in, and refused rather than silently downgraded when the provider cannot place a cache breakpoint. See [Prompt caching](/reference/sdk/ai-steps#prompt-caching).
</ResponseField>

<ResponseField name="stopWhen" type="StopCondition[]">
  Named ceilings checked after each turn, in order; the first met ends the run and names itself. See Stopping an agent below.
</ResponseField>

<ResponseField name="stop" type="(ctx) => boolean">
  Optional early stop once a turn's tools have run; must be pure for replay. Prefer stopWhen.
</ResponseField>

It returns the loop's result: `final` (the model's answer), `iterations`, `stopReason`
(`"final"`, `"max-iterations"`, `"stopped"`, `"approval-budget"`, `"bail"` or `"guardrail"`),
`stoppedBy` - which named condition ended it, when one did - and `haltedBy`, which names the
guardrail on `"guardrail"`. See [Guardrails](/ai/guardrails).

## Stopping an agent

`maxIterations` bounds turns. It does not bound time, and it does not bound the work one
turn generates: a single turn calling twenty tools in parallel is one iteration and
twenty-one steps. `stopWhen` covers the rest.

| Condition               | Ends the run when                          | Config                                                                     |
| ----------------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
| `wallClock({ within })` | the loop has been running that long        | a duration string (`"5m"`) or milliseconds                                 |
| `maxSteps({ steps })`   | the loop has spent that many durable steps | turns, tool calls, guardrail passes, approvals and trims all count         |
| `hasToolCall({ tool })` | the completed turn called that tool        | whether it succeeded, was denied or was refused - the agent reached for it |

```ts theme={null}
import { hasToolCall, maxSteps, wallClock } from "@duraton/sdk";

const result = await agent(ctx, "triage", {
  model: "claude-opus-5",
  prompt: ticket.body,
  maxIterations: 8,
  stopWhen: [wallClock({ within: "5m" }), maxSteps({ steps: 20 }), hasToolCall({ tool: "escalate" })],
});

result.stopReason; // "stopped"
result.stoppedBy;  // "wallClock"
```

Conditions are asked in the order you list them and the first met wins, so the list reads
as a priority order. Each pass is its own durable step, which is what lets `wallClock`
read a real clock and still replay to the same decision - a replayed run re-walks its
committed turns in milliseconds, and a re-read clock would end it somewhere else.

Two other things follow from being data rather than a closure: a condition registers into
the workflow's agent manifest, so the console can show what bounds an agent before it has
ever run; and `stoppedBy` names the one that fired, on the result and on the run's trace.
The `stop` closure still works and is still the right tool for a predicate nobody needs to
read back - it reports `stopReason: "stopped"` with no `stoppedBy`, because a closure has
no name.

### Ending early on success

A tool that has already produced the answer says so with `bail()`, and the loop returns
that value instead of handing the result back for a turn it does not need.

```ts theme={null}
import { bail } from "@duraton/sdk";

tool({
  name: "lookup_order",
  handler: async (input) => {
    const order = await orders.find(input.id);
    return order.refunded ? bail({ status: "already refunded" }) : order;
  },
});
```

The run ends with `stopReason: "bail"` and the value as `final`. Every other tool the same
turn started still runs to completion first - a side effect that already happened is not
dropped from the record because a sibling finished sooner.

<Note>
  `bail()` is returned, never thrown. A tool handler runs inside a durable step, so a
  throw is committed as that step's failure - an early success would be indistinguishable
  from a broken tool, and would replay as a failure forever.
</Note>

## `tool`

A tool is one definition. Give it a name the model calls back with, a description it reads to
decide, and a JSON Schema for the input it must produce:

<ResponseField name="name" type="string" required>
  The tool's identity: what the model calls back with, and the tail of the durable step name its call writes. Must be unique within an agent.
</ResponseField>

<ResponseField name="description" type="string">
  What the tool does. This is what the model reads to decide whether to call it.
</ResponseField>

<ResponseField name="inputSchema" type="Record<string, unknown>">
  JSON Schema for the input the model must produce. A tool with none is declared as an object schema with no properties.
</ResponseField>

<ResponseField name="outputSchema" type="Record<string, unknown>">
  JSON Schema for what the tool returns. Nothing validates against it - it is published, so a surface can render a result before it has seen one.
</ResponseField>

<ResponseField name="toModelOutput" type="(output: unknown) => unknown">
  Project the result for the model only. The durable step still records what the tool returned. See Showing the model less than you record below.
</ResponseField>

<ResponseField name="annotations" type="ToolAnnotations">
  Behaviour hints - title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint. Hints, never gates. See Behaviour hints below.
</ResponseField>

<ResponseField name="handler" type="(input: unknown) => unknown | Promise<unknown>">
  Your own code. Runs as a durable step: the model's input in, the return value back to the model.
</ResponseField>

<ResponseField name="workflow" type="string">
  A workflow to run instead of a handler. The tool call becomes a linked child run.
</ResponseField>

<ResponseField name="app" type="string">
  The workflow tool's app; addressed like step.runWorkflow.
</ResponseField>

<ResponseField name="runner" type="string">
  Pin the workflow tool to a specific runner.
</ResponseField>

<ResponseField name="requiresApproval" type="boolean">
  Park the run on a human before this tool runs. See Tools a human has to approve below.
</ResponseField>

<ResponseField name="approval" type="ApprovalRule">
  Annotates the gate (risk, summary, policy, context, escalatesTo, allow, timeout, onTimeout) and decides whether it is raised at all (if, minRisk). Declaring one gates the tool; requiresApproval: false opts out of it.
</ResponseField>

<ResponseField name="integration" type="string">
  The integration this tool reaches, e.g. "google-calendar" - a free-text label, not a Credential id. Feeds the tools manifest below.
</ResponseField>

Exactly one of `handler` or `workflow` - a tool is backed by your code or by a workflow:

```ts theme={null}
const lookupOrder = tool({
  name: "lookup-order",
  description: "Fetch an order by id",
  inputSchema: { type: "object", properties: { orderId: { type: "string" } } },
  workflow: "orders.lookup",
  app: "orders",
});
```

That call is followable in both directions (call->run lineage). The tool's own durable step carries
`childRunId` - the id of the run it started - and the run it started carries `parentRunId`,
`parentStep` (the step it was spawned on) and `parentAttempt` (which attempt of that step spawned it).
So `GET /runs?parentRunId=<id>` returns every child one agent run started, and each child links back
to the exact call that made it. A handler-backed tool has neither end of the edge: it runs inside the
agent's own run. See the [runs API](/reference/api/runs).

<Note>
  A tool's `input` is raw model output. The schema tells the model what to produce; it does not
  validate what arrives, so parse it in the handler before you trust it.
</Note>

## Behaviour hints

`annotations` describes how a tool behaves, in the same field names MCP uses, so one declaration
means the same thing to Duraton and to any MCP client reading your tools:

```ts theme={null}
const dropIndex = tool({
  name: "drop-index",
  description: "Drop a database index",
  annotations: {
    title: "Drop index",
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: false,
    openWorldHint: false,
  },
  requiresApproval: true,
  handler: (input) => db.dropIndex(input.name),
});
```

| Hint              | Says                                                                |
| ----------------- | ------------------------------------------------------------------- |
| `title`           | A human-readable name for the tool, for a picker or a form label    |
| `readOnlyHint`    | The tool does not change anything                                   |
| `destructiveHint` | The tool can destroy or overwrite something                         |
| `idempotentHint`  | Calling it twice with the same input is the same as calling it once |
| `openWorldHint`   | The tool reaches something outside your system                      |

<Warning>
  These are hints, and a hint is not a gate. `destructiveHint: true` on its own stops nothing -
  `requiresApproval` and its [rule](/agent-kit/approvals#gating-some-calls-and-not-others) are what park the run on a
  person, and a hint is not one of the four names a rule can read. The hints exist so a surface can
  suggest the right default without every dangerous tool having to be marked by hand, and MCP defines
  them the same way: a client is told never to make tool-use decisions on annotations it received from
  a server it does not trust - on an [attached server](/agent-kit/providers-and-mcp#attaching-an-external-mcp-server) the hint is
  written by that server's operator, not by you. Set both on a tool that is genuinely destructive.
</Warning>

## What the model sees

Every turn sees the task, the exchanges so far, and your tools declared as function-calling
schemas. How the exchanges reach the model depends on the adapter:

| Adapter                                                                | What it gets                                                                                                                   |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Declares the `transcript` capability (the built-in Anthropic one does) | The task as the prompt, and the turns as structured `transcript` - which the adapter maps to its provider's native tool blocks |
| Declares nothing                                                       | The task with the turns rendered into it as text, exactly as before                                                            |

The fallback is not a lesser answer, just a lesser shape: a model reading its own tool calls as
prose is working in a form it was not trained on. Either way the turns are read from the loop's
recorded history and nothing else, so a replayed turn is identical to the original one.

## Bounding context

Nothing trims history by default, so an agent that runs long enough eventually hits the model's
context window and fails. Pass `context` and every turn - both a native `transcript` and the
rendered-text fallback above - sees a bounded view instead of the full, ever-growing history:

```ts theme={null}
const result = await agent(ctx, "support", {
  model: "claude-opus-4-8",
  prompt: `Resolve this ticket: ${ticket}`,
  tools: [searchPolicy, issueRefund],
  maxIterations: 20,
  context: { strategy: "count", keepLast: 10 },
});
```

| `strategy`     | What it does to the record                                                                                                                                                                                                                                                                              |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `count`        | Drops the oldest iterations beyond `keepLast`. A fresh view computed for each turn - never written back, so the run's own history still has everything                                                                                                                                                  |
| `token-budget` | Same drop, sized by `maxTokens` instead of a count - an approximation (`estimateTokens`), not a real tokenizer                                                                                                                                                                                          |
| `summarize`    | Once history exceeds `maxTokens`, overwrites the earliest surviving iteration's own result with a model-generated summary of everything older than `keepLast`, and drops the rest. Reuses that iteration's real id rather than inventing one - see [Context management](/ai/context-management) for why |

`count` and `token-budget` are free: recomputed fresh every turn, they never change what the run
itself remembers. `summarize` is the one that costs something, so it is the one that persists - a
later turn starts from the already-reduced history instead of paying to re-summarize the same
prefix every time. `summarize`'s model call rides its own durable step, so a replay reads the
recorded summary rather than generating a new one - full detail on that step and its replay
guarantee lives on the [Context management](/ai/context-management) page, the SDK-level page for the
loop-level `context` option this wraps.

<Note>
  Without `context`, an agent behaves exactly as it did before the option existed - the whole
  history reaches every turn. The option changes nothing for an agent that does not use it.
</Note>
