# AI SDK Agents

You are building agents with the AI SDK (`ai`) in TypeScript: a model on the server calling tools in a loop, then answering in text or a typed object. Write code for the versions in `package.json`, not the ones in your training data: agents became a first-class API in AI SDK 6, and AI SDK 7 renamed several of their options.

## Before you write code

- Read the `ai` version in `package.json` and write that major's names, never a mix (7 still accepts most 6 names as deprecated aliases):

| AI SDK 6 | AI SDK 7 |
| --- | --- |
| `stepCountIs(n)` | `isStepCount(n)` |
| `system` on `generateText` (agents take `instructions` in both) | `instructions` |
| `onFinish`, `onStepFinish` | `onEnd`, `onStepEnd` |
| `needsApproval` on the tool | `toolApproval` on the agent, `generateText` or `streamText` |
| `experimental_context` | `toolsContext` with the tool's `contextSchema`, and `runtimeContext` |
| `experimental_telemetry: { isEnabled: true }` | `telemetry`, after `registerTelemetry(new OpenTelemetry())` from `@ai-sdk/otel` |
| `result.totalUsage`, `result.response.messages` | `result.usage`, `result.responseMessages` |
| `MockLanguageModelV3` from `ai/test` | `MockLanguageModelV4` |

- Read the docs that ship in `node_modules/ai/docs/`: `03-agents/` (building agents, loop control, call options, subagents) and `03-ai-sdk-core/15-tools-and-tool-calling.mdx`. Check the API there before using one you are not sure of.
- Find the project's existing agents, tools and model config, and reuse them. Never invent a model id.

## Pick the smallest shape

- One answer, no tools: a single `generateText` or `streamText` call, with `output` for JSON.
- Steps you know in advance (extract, classify, draft): plain code calling the model once per step. It is cheaper, testable and predictable; don't make a model choose a fixed order.
- Open-ended tool use: a `ToolLoopAgent` in its own module, reused by routes, Server Functions, jobs and tests.

```ts
// lib/agents/support.ts, AI SDK 7 (stepCountIs on 6)
import { isStepCount, ToolLoopAgent, type InferAgentUIMessage } from "ai";

export function createSupportAgent(user: SessionUser) {
  return new ToolLoopAgent({
    model: supportModel,
    instructions: SUPPORT_INSTRUCTIONS,
    tools: createSupportTools(user.id),
    stopWhen: isStepCount(8),
  });
}

export type SupportMessage = InferAgentUIMessage<
  ReturnType<typeof createSupportAgent>
>;

export async function answerSupport(user: SessionUser, prompt: string) {
  const { text, steps } = await createSupportAgent(user).generate({
    prompt,
    abortSignal: AbortSignal.timeout(60_000),
  });
  return { steps: steps.length, text };
}
```

- Scope tools to the caller: build them in a factory that closes over the signed-in user, as above, or pass the user through context (`experimental_context` on 6, `toolsContext` on 7). For one shared agent with typed per-request inputs, use `callOptionsSchema` and `prepareCall`, called with `generate({ prompt, options })`.

## Loop control

- A `ToolLoopAgent` stops after 20 steps by default; `generateText` and `streamText` after one. Always set `stopWhen`; an array stops at whichever condition is met first, such as `[isStepCount(10), hasToolCall("finish")]`.
- A budget is a custom stop condition: `({ steps }) => boolean` summing `step.usage.inputTokens` and `outputTokens`, or counting calls to an expensive tool. Never use `isLoopFinished()` without one.
- `prepareStep({ stepNumber, steps, messages })` returns overrides for the next step: `activeTools` to narrow tools per phase, `toolChoice: { type: "tool", toolName }` to force one, `model` to switch, `messages` to compact a long run (`pruneMessages`). On 7, returned `messages` and `instructions` carry forward to later steps; on 6 they apply to that step only.
- A tool without `execute` ends the loop when called. A `done` tool whose input is the final answer, with `toolChoice: "required"`, makes the model finish through it; read its input from `result.steps.at(-1)?.toolCalls`.

## Structured output

- Set `output: Output.object({ schema })` on the agent (or on `generateText`) and read `result.output`. `generateObject` and `streamObject` are deprecated.
- The output is one more step: leave room in `stopWhen`. If the loop ends on a tool call, reading `result.output` throws `NoOutputGeneratedError`; a schema mismatch throws `NoObjectGeneratedError`. Catch both.
- Give every field `.describe()`, prefer enums to free strings, and keep schemas flat.

## Tool design

- One job per tool, named as a verb (`searchOrders`, `refundOrder`). The `description` says when to use it and what it returns.
- `inputSchema` in Zod with `.describe()`, enums, and bounds (`.min()`, `.max()`). Set `strict: true` where the provider supports it.
- Return small typed JSON: ids, names, a summary, `hasMore`, not whole rows, HTML or files. Paginate and truncate. `toModelOutput` sends the model a smaller view while the UI keeps the full output.
- Make writes idempotent: use `toolCallId` from `execute`'s second argument as the idempotency key, so a retried step never refunds twice.
- Return expected failures as data the model can act on: `{ ok: false, error: "No order with that id. Search by email." }`. Thrown errors also reach the model as tool errors, so never put stack traces, SQL or secrets in messages.
- Pass `execute`'s `abortSignal` to `fetch` and database calls.
- Authorize inside `execute` with the server's user. Tool input is model output: never accept ids that grant access from it, and treat text the agent read from documents or the web as data, not instructions.
- Keep the active set small; split large tool sets by phase with `activeTools`.

## Approvals

- Gate tools that spend money, delete, send messages or run code: `needsApproval: true` or `async (input) => boolean` on the tool on 6; on 7, `toolApproval: { refundOrder: ({ amount }) => (amount > 100 ? "user-approval" : undefined) }` (statuses: `"user-approval"`, `"approved"`, `"denied"`, `"not-applicable"`).
- The run then ends with a `tool-approval-request` part in `result.content`. Ask the person, append a `role: "tool"` message with `{ type: "tool-approval-response", approvalId, approved, reason }` after the response messages, and call again. In a chat UI, `useChat`'s `addToolApprovalResponse` does this.
- Load the conversation from your database rather than trusting the client's copy, so nobody can forge an approval; `generateText` and `streamText` also take `experimental_toolApprovalSecret` to sign them. Tell the agent not to retry a denied call.
- A run that waits hours for a person or must survive a deploy belongs in a durable workflow or job queue, not a held-open request.

## MCP tools

- Use `createMCPClient` from `@ai-sdk/mcp` with `transport: { type: "http", url, headers }`; stdio only for local tools. Load `await client.tools({ schemas })` to take only the tools you name, with typed inputs; `client.tools()` loads everything the server offers.
- Close the client when the run ends: in the end callback for streams, in `finally` for `generate`.
- Connect only to servers you trust, treat their output as untrusted text, and gate their write tools behind approval.

## Streaming to a UI

- In a route: `return createAgentUIStreamResponse({ agent, uiMessages: messages, abortSignal: req.signal })`, and type the client with `useChat<SupportMessage>()`.
- Stream progress as data parts from `createUIMessageStream({ execute: ({ writer }) => … })` with `writer.write({ type: "data-status", id, data })`; writing the same `id` again updates the part in place.
- A subagent is a tool whose `execute` is an `async function*` yielding the subagent's UI message (`readUIMessageStream`), with `toModelOutput` returning only its final summary to the parent. Pass the parent's `abortSignal` down.

## Budgets and observability

- Every run has a step cap, a token budget, `maxOutputTokens` and a deadline: `abortSignal: AbortSignal.timeout(ms)` or `timeout: { totalMs, stepMs }`.
- Log each step in `onStepFinish` (6) or `onStepEnd` (7): tool calls, finish reason, usage. Record each run's total usage against the user.
- For traces, register an OpenTelemetry provider in `instrumentation.ts`, then set `experimental_telemetry: { isEnabled: true, functionId }` per call on 6. On 7, also call `registerTelemetry(new OpenTelemetry())` there and set `telemetry: { functionId }`.

## Do not

- Do not write AI SDK 5 agent code (`Experimental_Agent` with `system`), `maxSteps`, `experimental_output`, `experimental_prepareStep` or tool `parameters`, and do not hand-roll a `while` loop around `generateText` that `stopWhen` and `prepareStep` already cover.
- Do not put secrets or other users' data in instructions or tool descriptions: they go to the provider and can come back in a reply.
- Do not run model-written code or shell commands outside a sandbox and an approval.

## Before you finish

- Run the type checker, and test tools by calling `execute` directly, including the auth and `{ ok: false }` paths.
- Test the loop with a mock model from `ai/test` whose `doGenerate` is an array of results, one per step (tool calls, then text), and assert on `result.steps` and the model's `doGenerateCalls`. Copy the result shape from `node_modules/ai/docs/03-ai-sdk-core/55-testing.mdx`: it differs between spec versions.
- Run the agent once against the real model and read `result.steps`: which tools ran, how many steps, what it cost.
- Say which step cap, budget, timeout and approvals you set.
