new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

AI SDK · Cookbook · all subjects

agent implementation

84 notes in this subject, read out of this brain and free to use. This is page 1 of 2.

prepareCall lifecycle callback in WorkflowAgent

prepareCall is called once before the agent loop starts. Use to transform model, instructions, or other settings based on runtime context. Can be defined in constructor (agent-wide) or in stream() (per-call). When both defined, both fire with constructor first.

prepareStep lifecycle callback in WorkflowAgent

prepareStep is called before each LLM step. Use to modify settings, manage context, or inject messages dynamically. Can modify temperature, toolChoice, and other generation settings per step. Can be defined in constructor or in stream() (per-call). When both defined, both fire with constructor first.

WorkflowAgent lifecycle callbacks

Lifecycle callbacks for logging and observability: experimental_onStart({ modelId, messages }), experimental_onStepStart({ stepNumber }), onToolExecutionStart({ toolCall }), onToolExecutionEnd({ toolCall, toolOutput }), onStepEnd({ usage, finishReason }), onEnd({ steps, totalUsage }). Can be defined in constructor (agent-wide) or in stream() (per-call). When both defined, both fire with constructor first.

Migrating from DurableAgent messages

DurableAgent returned uiMessages with collectUIMessages: true. WorkflowAgent returns ModelMessage[] on result.messages. Store UIMessage[] as source of truth, convert with convertToModelMessages() before passing to agent. No built-in ModelMessage to UIMessage conversion.

Migrating from DurableAgent tool approval

Replace Hook-based approval with needsApproval property on tool definition. Old: call waitForApprovalHook inside execute. New: set needsApproval: true on tool, agent handles approval and suspension.

Migrating from DurableAgent output parameter

Replace experimental_output with output. Import Output from @ai-sdk/workflow. Result moves from result.experimental_output to result.output.

Migrating from DurableAgent maxSteps to stopWhen

Replace maxSteps with stopWhen using stop conditions from ai package. Instead of maxSteps: 10, use stopWhen: isStepCount(10). Import isStepCount from 'ai'.

Migrating from DurableAgent stream output

DurableAgent wrote UIMessageChunk to writable. WorkflowAgent writes ModelCallStreamPart and converts at response boundary. Change writable type from getWritable<UIMessageChunk>() to getWritable<ModelCallStreamPart>(). In route handler, add createModelCallToUIChunkTransform() transform.

Migrating from DurableAgent to WorkflowAgent import

Change from: import { DurableAgent } from 'workflow/ai' to: import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow'. Install @ai-sdk/workflow package: npm install @ai-sdk/workflow.

InferWorkflowAgentUIMessage type inference

Infer UI message type for type-safe client components: ```ts import { WorkflowAgent, InferWorkflowAgentUIMessage } from '@ai-sdk/workflow'; const myAgent = new WorkflowAgent({ /* ... */ }); export type MyAgentUIMessage = InferWorkflowAgentUIMessage<typeof myAgent>; ``` Use this type when rendering messages in the UI.

Migrating from DurableAgent context

Replace experimental_context with runtimeContext (shared agent state) and toolsContext (per-tool state). Each tool's execute receives only its own validated entry as context parameter.

WorkflowAgent only exposes stream method

WorkflowAgent does not have a generate() method, only stream(). If migrating from DurableAgent.generate(), use stream() instead and read result.messages or result.output once promise resolves.

WorkflowAgent class and package

WorkflowAgent is exported from @ai-sdk/workflow. It is designed for building durable, resumable agents that run inside a workflow. Installation requires: npm install @ai-sdk/workflow workflow. The @ai-sdk/workflow package requires ai and zod as peer dependencies.

WorkflowAgent vs ToolLoopAgent comparison

ToolLoopAgent is in-memory, loses state on crash, has manual tool retries, and exposes both generate() and stream() methods. WorkflowAgent runs inside a workflow, survives restarts, has automatic tool retries via workflow steps, and only exposes stream() method as primary API. WorkflowAgent has built-in human approval that survives suspension. ToolLoopAgent is from the 'ai' package while WorkflowAgent is from '@ai-sdk/workflow'. WorkflowAgent stream output is ModelCallStreamPart written to a writable parameter, while ToolLoopAgent returns a streamText value.

WorkflowAgent constructor parameters

WorkflowAgent constructor accepts: model (string AI Gateway model ID or provider instance), instructions (string), tools (object with tool definitions), runtimeContext (optional shared agent state), toolsContext (optional per-tool context map), experimental_sandbox (optional sandbox session), prepareCall (optional async function called once before agent loop), prepareStep (optional async function called before each LLM call), and lifecycle callbacks (experimental_onStart, experimental_onStepStart, onToolExecutionStart, onToolExecutionEnd, onStepEnd, onEnd).

WorkflowAgent.stream() method parameters

stream() accepts: messages (ModelMessage[] array, not UIMessage[]), writable (WritableStream from getWritable<ModelCallStreamPart>()), output (optional structured output schema using Output.object()), stopWhen (optional stop condition like isStepCount or isLoopFinished), runtimeContext (optional per-call override), toolsContext (optional per-call override), prepareCall (optional per-call override), prepareStep (optional per-call override), experimental_sandbox (optional per-call override), and lifecycle callbacks (per-call overrides).

Model parameter in WorkflowAgent

The model parameter accepts two forms: (1) string as AI Gateway model ID (e.g., 'anthropic/claude-sonnet-4-6'), or (2) provider instance (e.g., openai('gpt-4o') from @ai-sdk/openai).

Converting UIMessage to ModelMessage for WorkflowAgent

WorkflowAgent.stream() expects ModelMessage[] not UIMessage[]. Use convertToModelMessages() from 'ai' package to convert UIMessage[] to ModelMessage[] before passing to stream(). This is required when receiving messages from useChat on the client.

WorkflowAgent workflow function setup

To use WorkflowAgent in a workflow: (1) mark function with 'use workflow' directive, (2) pass getWritable<ModelCallStreamPart>() to agent.stream() writable parameter, (3) start the workflow from an API route using start() from 'workflow/api'.

WorkflowAgent example with tool execution

```ts import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow'; import { convertToModelMessages, tool, type UIMessage } from 'ai'; import { getWritable } from 'workflow'; import { z } from 'zod'; export async function chat(messages: UIMessage[]) { 'use workflow'; const modelMessages = await convertToModelMessages(messages); const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', instructions: 'You are a flight booking assistant.', tools: { searchFlights: tool({ description: 'Search for available flights', inputSchema: z.object({ origin: z.string(), destination: z.string(), date: z.string(), }), execute: searchFlightsStep, }), bookFlight: tool({ description: 'Book a specific flight', inputSchema: z.object({ flightId: z.string(), passengerName: z.string(), }), execute: bookFlightStep, }), }, }); const result = await agent.stream({ messages: modelMessages, writable: getWritable<ModelCallStreamPart>(), }); return { messages: result.messages }; } ``` This example shows creating a WorkflowAgent inside a workflow function with tool definitions and streaming results.

Tool step marking with 'use step' directive

Mark tool execute functions with 'use step' to make them durable workflow steps. This enables automatic retries (default 3 attempts), persistence across process restarts, and visibility in workflow dashboard. Tools without 'use step' run as in-memory functions without durability guarantees.

WorkflowAgent tool needsApproval property

Set needsApproval on a tool definition for human-in-the-loop approval. needsApproval can be: (1) boolean true for always requiring approval, or (2) async function accepting input and returning boolean for conditional approval. When set, the agent pauses and emits an approval request to the writable stream. The workflow suspends until user approves or denies.

WorkflowAgent loop control with stopWhen

Control agent steps using stopWhen parameter: use isStepCount(n) to stop after n LLM calls, or isLoopFinished() to let agent run until all tool calls complete. Default is no maximum. Should pair isLoopFinished() with maxSteps to avoid runaway loops.

runtimeContext in WorkflowAgent

runtimeContext is shared agent state that flows through prepareStep, lifecycle callbacks, and onEnd. It is available in prepareCall, prepareStep, and lifecycle callbacks. Treat as immutable; return a new value from prepareStep to update for current and subsequent steps. Can be passed in constructor or per-call to stream(). Must be serializable (plain data: strings, numbers, booleans, arrays, objects, dates, URLs, maps, sets). Do not store functions, class instances, symbols, WeakMap, WeakSet, database clients, or SDK clients.

toolsContext in WorkflowAgent

toolsContext is a per-tool context map keyed by tool name. Each tool's execute function only sees its own validated entry as context parameter. Tools can declare a contextSchema using z.object() to validate their context entry before execution. Can be passed in constructor or per-call to stream(). Values must be serializable.

WorkflowAgent runtimeContext and toolsContext example

```ts const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', tools: { weather: tool({ description: 'Get the weather for a city.', inputSchema: z.object({ city: z.string() }), contextSchema: z.object({ defaultUnit: z.enum(['celsius', 'fahrenheit']), }), execute: async ({ city }, { context }) => ({ city, unit: context.defaultUnit, }), }), }, runtimeContext: { tenantId: 'tenant_123', requestId: 'req_abc', plan: 'enterprise', }, toolsContext: { weather: { defaultUnit: 'celsius' }, }, prepareStep: ({ runtimeContext }) => { if (runtimeContext.plan === 'enterprise') { return { temperature: 0.2 }; } return {}; }, }); ``` Shows how to define and use both runtimeContext (shared state) and toolsContext (per-tool validated state).

experimental_sandbox in WorkflowAgent

Pass a sandbox session when tools need an execution environment. Available to tool execute functions as experimental_sandbox parameter, and to prepareStep where it can be overridden. Do not store in runtimeContext or toolsContext (not durable). If a tool runs as separate workflow step, pass serializable sandbox identifiers and reattach inside that step.

prepareSandboxForHarness for native sandbox lifecycle

Use prepareSandboxForHarness() when owning native sandbox lifecycle and wanting to snapshot prepared sandbox manually. Call with {session: session.restricted(), harnesses: [array of harnesses], sandboxConfig}. Applies harness bootstrap recipes and sandboxConfig.onBootstrap, returns preparation metadata, does not stop or snapshot. After calling, stop native sandbox and create snapshot manually.

HarnessAgent adapter-specific settings

Adapter-specific settings like reasoningEffort belong on the adapter factory function, for example createCodex({reasoningEffort: 'high'}), not on HarnessAgent constructor.

HarnessAgent configuration parameters

HarnessAgent accepts: harness (adapter instance), sandbox (HarnessV1SandboxProvider), id (optional stable agent identifier), instructions (appended to runtime system/developer prompt or prepended to first user prompt), stopWhen (condition for finishing result after harness tool step), tools (AI SDK tools executed by host), activeTools (allowlist of built-in and host-executed tools), inactiveTools (denylist of built-in and host-executed tools), skills (instruction bundles), permissionMode (built-in tool permission mode), toolApproval (approval status map for host tools), sandboxConfig (working-directory and lifecycle hooks), telemetry, debug, onLog (observability).

HarnessAgent detach for multi-turn HTTP routes

For HTTP routes needing multi-turn continuity, use session.detach() after streaming results. This returns resumeState which should be persisted. Later, resume with agent.createSession({sessionId: chatId, resumeFrom: resumeState}) on the next request. If the turn is unfinished, resume state includes continuation state.

HarnessAgent resume from persistent state

To resume a session, load the persisted resumeState and pass it to agent.createSession() with sessionId: either agent.createSession({sessionId: chatId, resumeFrom: resumeState}) if resumeState exists, or agent.createSession({sessionId: chatId}) if not. HarnessAgent validates resume state matches the same harness adapter. If resume state includes unfinished turn, call continueStream() or continueGenerate() before sending new prompt.

HarnessAgent suspended turn continuation

For advanced workflows handing off active turn across process boundary, check session.hasUnfinishedTurn() and if true, call session.suspendTurn() to get continuationState. Persist it, then later create session with agent.createSession({sessionId: chatId, continueFrom: continuationState}). Call agent.continueStream() for incremental output or agent.continueGenerate() to drain the continued turn.

HarnessAgent stopWhen for semantic step boundaries

Pass stopWhen as a predicate or array of predicates to HarnessAgent constructor to finish result after completed harness tool step. Predicates run after real harness tool steps that can continue into another model step. When predicate matches, returned result finishes while underlying turn remains unfinished. A terminal text-only step instead consumes the turn's finish event. Has no default; when omitted, HarnessAgent continues until turn naturally finishes or pauses for host input. Resume stopped turn with createSession({continueFrom}) and continueStream() or continueGenerate().

HarnessAgent stopWhen example with isStepCount

Import isStepCount from 'ai'. Pass stopWhen: isStepCount(1) to HarnessAgent constructor to stop after 1 step. After generating result, check session.hasUnfinishedTurn() and if true, call session.suspendTurn() to get continueFrom state for later resumption.

HarnessAgent sandboxConfig configuration

Pass sandboxConfig object with: workDir (optional relative path to sandbox default working directory used as session working directory), bootstrapHash (string to invalidate snapshot when output should change), onBootstrap (runs during sandbox template creation after harness adapter bootstrap and before snapshot, use for expensive reusable setup), onSession (runs after each sandbox session acquired and working directory exists, use for per-session files). OnBootstrap receives {session, abortSignal}. OnSession receives {session, sessionWorkDir, abortSignal}.

HarnessAgent sandboxConfig example

Example showing sandboxConfig with workDir: 'repo', bootstrapHash: 'ripgrep-v1', onBootstrap installing ripgrep via apt-get, and onSession writing README.md to session working directory. OnBootstrap runs once for template, onSession runs for each session.

prepareHarnessSandboxTemplate function

Use prepareHarnessSandboxTemplate({harness, sandboxProvider, sandboxConfig}) to have sandbox provider create or refresh reusable template for one harness ahead of time. Accepts harness instance, sandbox provider created with createVercelSandbox, and sandboxConfig with bootstrapHash and onBootstrap.

prepareSandboxForHarness native lifecycle example

Create native sandbox with Sandbox.create(). Create sandboxProvider with createVercelSandbox({sandbox: nativeSandbox}). Create session from provider. Call prepareSandboxForHarness({session: session.restricted(), harnesses: [claudeCode, codex], sandboxConfig}). Stop native sandbox with nativeSandbox.stop() to get snapshot. Create new sandbox from snapshot with Sandbox.create({source: {type: 'snapshot', snapshotId: snapshot.id}}). Pass to createVercelSandbox({sandbox: sandboxFromSnapshot, bridgePorts: [4000]}).

HarnessAgent installation packages

Install @ai-sdk/harness (core harness package), a harness adapter such as @ai-sdk/harness-claude-code, and a sandbox provider. Bridge-backed harnesses like Claude Code and Codex require @ai-sdk/sandbox-vercel. Host-runtime harnesses like Pi can use either @ai-sdk/sandbox-vercel or @ai-sdk/sandbox-just-bash.

HarnessAgent basic setup example

Create a HarnessAgent instance with harness, sandbox, and instructions properties. Construct the agent at module scope to hold configuration. Pass a harness instance like claudeCode, create a sandbox with createVercelSandbox({runtime: 'node24', ports: [4000]}), and set instructions as a string. Example: new HarnessAgent({ harness: claudeCode, sandbox: createVercelSandbox({runtime: 'node24', ports: [4000]}), instructions: 'You are a careful coding assistant. Prefer small changes and explain tradeoffs.' })

HarnessAgent session creation and generate method

Call agent.createSession() to create a session. Use agent.generate({session, prompt: 'your prompt'}) to drain the turn and get a GenerateTextResult with the text property. Always call session.destroy() in a finally block for one-off scripts.

HarnessAgent message handling differs from model calls

HarnessAgent takes only the latest user message as fresh input when messages or message-array prompt is passed, not replaying full prior conversation into the harness. A trailing tool message continues the unfinished harness turn. This differs from model calls which usually send full message history. For chat routes, persist and resume the harness session instead of relying on message replay.

HarnessAgent session lifecycle methods

Four methods manage session lifecycle: session.destroy() stops runtime and discards resumability (use for one-off scripts); session.detach() parks runtime and sandbox, returns resume state, keeps sandbox warm for later attach; session.stop() saves resume state then stops runtime and sandbox; session.suspendTurn() is for advanced active-turn continuation across process boundary. Check session.hasUnfinishedTurn() to report if current turn must be continued or suspended before accepting new prompt.

createAgentUIStreamResponse purpose

The createAgentUIStreamResponse function executes an Agent, runs its streaming output as a UI message stream, and returns an HTTP Response object whose body is the live, streaming UI message output. It is designed for API routes that deliver real-time agent results, such as chat endpoints or streaming tool-use operations.

createAgentUIStreamResponse import path

The createAgentUIStreamResponse function is imported from the 'ai' package using: import { createAgentUIStreamResponse } from 'ai'

createAgentUIStreamResponse parameters table

Parameters for createAgentUIStreamResponse: | name | type | required | description | |------|------|----------|-------------| | agent | Agent | true | The agent instance to stream responses from. Must implement .stream({ prompt, ... }) and define the tools property. | | uiMessages | unknown[] | true | Array of input UI messages provided to the agent (e.g., user and assistant messages). | | abortSignal | AbortSignal | false | Optional abort signal to cancel streaming, e.g., on client disconnect. Should be an AbortSignal instance. | | timeout | number \| { totalMs?: number } | false | Timeout in milliseconds. Can be specified as a number or as an object with a totalMs property. The call will be aborted if it takes longer than the specified timeout. Can be used alongside abortSignal. | | experimental_sandbox | Experimental_SandboxSession | false | Optional experimental sandbox environment that is passed through to tool execution. Tools can access it from their execution context. | | options | CALL_OPTIONS | false | Optional agent call options, for agents with generic parameter CALL_OPTIONS. | | experimental_transform | StreamTextTransform \| StreamTextTransform[] | false | Optional stream transforms to post-process text output—the same as in lower-level streaming APIs. | | onStepEnd | GenerateTextOnStepEndCallback | false | Callback invoked after each agent step (LLM/tool call) completes. Useful for tracking token usage or logging intermediate steps. | | onStepFinish | GenerateTextOnStepFinishCallback | false | Deprecated. Use onStepEnd instead. This alias is only used as a fallback when onStepEnd is not provided. | | ...UIMessageStreamOptions | UIMessageStreamOptions | false | Other UI message output options—such as sendSources and more. | | headers | HeadersInit | false | Optional HTTP headers to include in the Response object. | | status | number | false | Optional HTTP status code. | | statusText | string | false | Optional HTTP status text. | | consumeSseStream | (options: { stream: ReadableStream<string> }) => PromiseLike<void> \| void | false | Optional function to consume the SSE stream. When provided, this function will be called with the SSE stream to handle consumption. |

createAgentUIStreamResponse return type

createAgentUIStreamResponse returns Promise<Response> whose body is a streaming UI message output from the agent. Use this as the return value of API/server handlers in serverless, Next.js, Express, Hono, or edge runtime contexts.

createAgentUIStreamResponse basic usage example

import { ToolLoopAgent, createAgentUIStreamResponse } from 'ai'; __PROVIDER_IMPORT__; const agent = new ToolLoopAgent({ model: __MODEL__, instructions: 'You are a helpful assistant.', tools: { weather: weatherTool, calculator: calculatorTool }, }); export async function POST(request: Request) { const { messages } = await request.json(); const abortController = new AbortController(); return createAgentUIStreamResponse({ agent, uiMessages: messages, abortSignal: abortController.signal, }); }

createAgentUIStreamResponse Next.js example

import { createAgentUIStreamResponse } from 'ai'; import { MyCustomAgent } from '@/agent/my-custom-agent'; export async function POST(request: Request) { const { messages } = await request.json(); return createAgentUIStreamResponse({ agent: MyCustomAgent, uiMessages: messages, sendSources: true, }); }

createAgentUIStreamResponse agent requirements

An agent used with createAgentUIStreamResponse must implement .stream({ prompt, ... }) method and define a tools property (even if it is just an empty object {}).

createAgentUIStreamResponse server-only API

createAgentUIStreamResponse should only be called in backend/server-side contexts such as API routes, edge/serverless/server route handlers, etc. It is not for browser use.

createAgentUIStreamResponse how it works

The createAgentUIStreamResponse function works in four steps: 1. UI Message Validation validates the incoming uiMessages array according to the agent's specified tools and requirements. 2. Model Message Conversion converts validated UI messages into the internal model message format for the agent. 3. Streaming Agent Output invokes the agent's .stream({ prompt, ... }) to get a stream of chunks (steps/UI messages), passing through options such as experimental_sandbox. 4. HTTP Response Creation wraps the output stream as a readable HTTP Response object that streams UI message chunks to the client.

pipeAgentUIStreamToResponse abort signal handling best practice

For best robustness, use an AbortSignal wired to Express or Hono client disconnects to ensure quick cancellation of agent computation and streaming.

pipeAgentUIStreamToResponse example usage

Example Express route handler showing how to use pipeAgentUIStreamToResponse: import { pipeAgentUIStreamToResponse } from 'ai'; import { openaiWebSearchAgent } from './openai-web-search-agent'; app.post('/chat', async (req, res) => { await pipeAgentUIStreamToResponse({ response: res, agent: openaiWebSearchAgent, uiMessages: req.body.messages, }); });

pipeAgentUIStreamToResponse import path

The pipeAgentUIStreamToResponse function is imported from the 'ai' package: import { pipeAgentUIStreamToResponse } from 'ai'

pipeAgentUIStreamToResponse purpose and usage

The pipeAgentUIStreamToResponse function runs an Agent and streams the resulting UI message output directly to a Node.js ServerResponse object. It is ideal for building real-time streaming API endpoints for chat and tool use in Node.js-based frameworks like Express, Hono, or custom Node servers.

pipeAgentUIStreamToResponse parameters

The pipeAgentUIStreamToResponse function accepts the following parameters: response (ServerResponse, required) - the Node.js ServerResponse object to pipe UI message stream output into; agent (Agent, required) - an agent instance implementing .stream({ prompt, ... }) and defining a tools property; uiMessages (unknown[], required) - array of input UI messages sent to the agent such as user/assistant message objects; abortSignal (AbortSignal, optional) - optional abort signal to cancel streaming on client disconnect; timeout (number | { totalMs?: number }, optional) - timeout in milliseconds, can be a number or object with totalMs property; experimental_sandbox (Experimental_SandboxSession, optional) - optional experimental sandbox environment passed through to tool execution; options (CALL_OPTIONS, optional) - optional agent call options for agents configured with generic parameter CALL_OPTIONS; experimental_transform (StreamTextTransform | StreamTextTransform[], optional) - optional stream text transformation(s) applied to agent output; onStepEnd (GenerateTextOnStepEndCallback, optional) - callback invoked after each agent step (LLM/tool call) completes for tracking token usage or logging; onStepFinish (GenerateTextOnStepFinishCallback, optional) - deprecated, use onStepEnd instead; ...UIMessageStreamResponseInit & UIMessageStreamOptions (object, optional) - options for streaming headers, status, SSE stream config, and UI message stream control.

pipeAgentUIStreamToResponse return value

The pipeAgentUIStreamToResponse function returns a Promise<void>. The function completes when the UI message stream has been fully sent to the provided ServerResponse.

pipeAgentUIStreamToResponse how it works

The pipeAgentUIStreamToResponse function works by: (1) calling the agent's .stream method with the provided UI messages and options, converting them into model messages as needed and passing through options such as experimental_sandbox; (2) piping the agent output as a UI message stream to the ServerResponse, sending data via streaming HTTP responses with appropriate headers; (3) handling abort signals by cancelling streaming as soon as the signal is triggered such as on client disconnect; (4) writing bytes directly to the ServerResponse without returning a response object.

Give your agent this brain