DurableAgent
The @workflow/ai package is currently in active development and should be considered experimental.
The DurableAgent class enables you to create AI-powered agents that can maintain state across workflow steps, call tools, and gracefully handle interruptions and resumptions.
Tool calls can be implemented as workflow steps for automatic retries, or as regular workflow-level logic utilizing core library features such as sleep() and Hooks.
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
async function getWeather({ city }: { city: string }) {
"use step";
return `Weather in ${city} is sunny`;
}
async function myAgent() {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
system: "You are a helpful weather assistant.",
temperature: 0.7,
tools: {
getWeather: {
description: "Get weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: getWeather,
},
},
});
// The agent will stream its output to the workflow
// run's default output stream
const writable = getWritable<UIMessageChunk>();
const result = await agent.stream({
messages: [{ role: "user", content: "How is the weather in San Francisco?" }],
writable,
});
// result contains messages, steps, and optional structured output
console.log(result.messages);
}API Signature
Class
| Name | Type | Description |
|---|---|---|
model | any | |
tools | any | |
system | any | |
generationSettings | any | |
toolChoice | any | |
telemetry | any | |
generate | () => void | |
stream | <TTools extends TBaseTools = TBaseTools, OUTPUT = never, PARTIAL_OUTPUT = never>(options: DurableAgentStreamOptions<TTools, OUTPUT, PARTIAL_OUTPUT>) => Promise<...> |
DurableAgentOptions
| Name | Type | Description |
|---|---|---|
model | string | (() => Promise<LanguageModelV2>) | The model provider to use for the agent.
This should be a string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-opus'),
or a step function that returns a LanguageModelV2 instance. |
tools | ToolSet | A set of tools available to the agent. Tools can be implemented as workflow steps for automatic retries and persistence, or as regular workflow-level logic using core library features like sleep() and Hooks. |
system | string | Optional system prompt to guide the agent's behavior. |
toolChoice | ToolChoice<ToolSet> | The tool choice strategy. Default: 'auto'. |
experimental_telemetry | TelemetrySettings | Optional telemetry configuration (experimental). |
maxOutputTokens | number | Maximum number of tokens to generate. |
temperature | number | Temperature setting. The range depends on the provider and model.
It is recommended to set either temperature or topP, but not both. |
topP | number | Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either temperature or topP, but not both. |
topK | number | Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. |
presencePenalty | number | Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
frequencyPenalty | number | Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
stopSequences | string[] | Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences. |
seed | number | The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. |
maxRetries | number | Maximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism. |
abortSignal | AbortSignal | Abort signal for cancelling the operation. |
headers | Record<string, string | undefined> | Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. |
providerOptions | SharedV2ProviderOptions | Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. |
DurableAgentStreamOptions
| Name | Type | Description |
|---|---|---|
messages | ModelMessage[] | The conversation messages to process. Should follow the AI SDK's ModelMessage format. |
system | string | Optional system prompt override. If provided, overrides the system prompt from the constructor. |
writable | WritableStream<UIMessageChunk> | The stream to which the agent writes message chunks. For example, use getWritable<UIMessageChunk>() to write to the workflow's default output stream. |
preventClose | boolean | If true, prevents the writable stream from being closed after streaming completes. Defaults to false (stream will be closed). |
sendStart | boolean | If true, sends a 'start' chunk at the beginning of the stream. Defaults to true. |
sendFinish | boolean | If true, sends a 'finish' chunk at the end of the stream. Defaults to true. |
stopWhen | StopCondition<NoInfer<ToolSet>> | StopCondition<NoInfer<ToolSet>>[] | Condition for stopping the generation when there are tool results in the last step. When the condition is an array, any of the conditions can be met to stop the generation. |
maxSteps | number | Maximum number of sequential LLM calls (steps), e.g. when you use tool calls. A maximum number can be set to prevent infinite loops in the case of misconfigured tools. By default, it's unlimited (the agent loops until completion). |
toolChoice | ToolChoice<TTools> | The tool choice strategy. Default: 'auto'. Overrides the toolChoice from the constructor if provided. |
activeTools | NoInfer<keyof TTools>[] | Limits the tools that are available for the model to call without changing the tool call and result types in the result. |
experimental_telemetry | TelemetrySettings | Optional telemetry configuration (experimental). |
experimental_context | unknown | Context that is passed into tool execution. Experimental (can break in patch releases). |
experimental_output | OutputSpecification<OUTPUT, PARTIAL_OUTPUT> | Optional specification for parsing structured outputs from the LLM response.
Use Output.object({ schema }) for structured output or Output.text() for text output. |
includeRawChunks | boolean | Whether to include raw chunks from the provider in the stream. When enabled, you will receive raw chunks with type 'raw' that contain the unprocessed data from the provider. This allows access to cutting-edge provider features not yet wrapped by the AI SDK. Defaults to false. |
experimental_repairToolCall | ToolCallRepairFunction<TTools> | A function that attempts to repair a tool call that failed to parse. |
experimental_transform | StreamTextTransform<TTools> | StreamTextTransform<TTools>[] | Optional stream transformations. They are applied in the order they are provided. The stream transformations must maintain the stream structure for streamText to work correctly. |
experimental_download | DownloadFunction | Custom download function to use for URLs. By default, files are downloaded if the model does not support the URL for the given media type. |
onStepFinish | StreamTextOnStepFinishCallback<TTools> | Callback function to be called after each step completes. |
onError | StreamTextOnErrorCallback | Callback that is invoked when an error occurs during streaming. You can use it to log errors. |
onFinish | StreamTextOnFinishCallback<TTools, OUTPUT> | Callback that is called when the LLM response and all request tool executions
(for tools that have an execute function) are finished. |
onAbort | StreamTextOnAbortCallback<TTools> | Callback that is called when the operation is aborted. |
prepareStep | PrepareStepCallback<TTools> | Callback function called before each step in the agent loop. Use this to modify settings, manage context, or inject messages dynamically. |
maxOutputTokens | number | Maximum number of tokens to generate. |
temperature | number | Temperature setting. The range depends on the provider and model.
It is recommended to set either temperature or topP, but not both. |
topP | number | Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either temperature or topP, but not both. |
topK | number | Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. |
presencePenalty | number | Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
frequencyPenalty | number | Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
stopSequences | string[] | Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences. |
seed | number | The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. |
maxRetries | number | Maximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism. |
abortSignal | AbortSignal | Abort signal for cancelling the operation. |
headers | Record<string, string | undefined> | Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. |
providerOptions | SharedV2ProviderOptions | Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. |
DurableAgentStreamResult
The result returned from the stream() method:
| Name | Type | Description |
|---|---|---|
messages | ModelMessage[] | The final messages including all tool calls and results. |
steps | StepResult<TTools>[] | Details for all steps. |
experimental_output | OUTPUT | The generated structured output. It uses the experimental_output specification.
Only available when experimental_output is specified. |
GenerationSettings
Settings that control model generation behavior. These can be set on the constructor or overridden per-stream call:
| Name | Type | Description |
|---|---|---|
maxOutputTokens | number | Maximum number of tokens to generate. |
temperature | number | Temperature setting. The range depends on the provider and model.
It is recommended to set either temperature or topP, but not both. |
topP | number | Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either temperature or topP, but not both. |
topK | number | Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. |
presencePenalty | number | Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
frequencyPenalty | number | Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
stopSequences | string[] | Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences. |
seed | number | The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. |
maxRetries | number | Maximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism. |
abortSignal | AbortSignal | Abort signal for cancelling the operation. |
headers | Record<string, string | undefined> | Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. |
providerOptions | SharedV2ProviderOptions | Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. |
PrepareStepInfo
Information passed to the prepareStep callback:
| Name | Type | Description |
|---|---|---|
model | string | (() => Promise<LanguageModelV2>) | The current model configuration (string or function). |
stepNumber | number | The current step number (0-indexed). |
steps | StepResult<TTools>[] | All previous steps with their results. |
messages | LanguageModelV2Prompt | The messages that will be sent to the model. This is the LanguageModelV2Prompt format used internally. |
experimental_context | unknown | The context passed via the experimental_context setting (experimental). |
PrepareStepResult
Return type from the prepareStep callback:
| Name | Type | Description |
|---|---|---|
model | string | (() => Promise<LanguageModelV2>) | Override the model for this step. |
system | string | Override the system message for this step. |
messages | LanguageModelV2Prompt | Override the messages for this step. Use this for context management or message injection. |
toolChoice | ToolChoice<ToolSet> | Override the tool choice for this step. |
activeTools | string[] | Override the active tools for this step. Limits the tools that are available for the model to call. |
experimental_context | unknown | Context that is passed into tool execution. Experimental. Changing the context will affect the context in this step and all subsequent steps. |
maxOutputTokens | number | Maximum number of tokens to generate. |
temperature | number | Temperature setting. The range depends on the provider and model.
It is recommended to set either temperature or topP, but not both. |
topP | number | Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either temperature or topP, but not both. |
topK | number | Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. |
presencePenalty | number | Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
frequencyPenalty | number | Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty. |
stopSequences | string[] | Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences. |
seed | number | The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. |
maxRetries | number | Maximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism. |
abortSignal | AbortSignal | Abort signal for cancelling the operation. |
headers | Record<string, string | undefined> | Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers. |
providerOptions | SharedV2ProviderOptions | Additional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider. |
TelemetrySettings
Configuration for observability and telemetry:
| Name | Type | Description |
|---|---|---|
isEnabled | boolean | Enable or disable telemetry. Defaults to true. |
functionId | string | Identifier for this function. Used to group telemetry data by function. |
metadata | Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined> | Additional information to include in the telemetry data. |
tracer | unknown | Custom tracer for the telemetry. |
Callbacks
StreamTextOnFinishCallback
Called when streaming completes:
| Name | Type | Description |
|---|---|---|
event | { readonly steps: StepResult<TTools>[]; readonly messages: ModelMessage[]; readonly experimental_context: unknown; readonly experimental_output: OUTPUT; } |
void | PromiseLike<void>StreamTextOnErrorCallback
Called when an error occurs:
| Name | Type | Description |
|---|---|---|
event | { error: unknown; } |
void | PromiseLike<void>StreamTextOnAbortCallback
Called when the operation is aborted:
| Name | Type | Description |
|---|---|---|
event | { readonly steps: StepResult<TTools>[]; } |
void | PromiseLike<void>Advanced Types
ToolCallRepairFunction
Function to repair malformed tool calls:
| Name | Type | Description |
|---|---|---|
options | { toolCall: LanguageModelV2ToolCall; tools: TTools; error: unknown; messages: LanguageModelV2Prompt; } |
LanguageModelV2ToolCall | Promise<LanguageModelV2ToolCall | null> | nullStreamTextTransform
Transform applied to the stream:
| Name | Type | Description |
|---|---|---|
options | { tools: TTools; stopStream: () => void; } |
TransformStream<LanguageModelV2StreamPart, LanguageModelV2StreamPart>OutputSpecification
Specification for structured output parsing:
| Name | Type | Description |
|---|---|---|
type | "object" | "text" | |
responseFormat | { type: "text"; } | { type: "json"; schema?: JSONSchema7; name?: string; description?: string; } | undefined | |
parsePartial | (options: { text: string; }) => Promise<{ partial: PARTIAL; } | undefined> | |
parseOutput | (options: { text: string; }, context: { response: LanguageModelResponseMetadata; usage: LanguageModelV2Usage; finishReason: LanguageModelV2FinishReason; }) => Promise<...> |
Key Features
- Durable Execution: Agents can be interrupted and resumed without losing state
- Flexible Tool Implementation: Tools can be implemented as workflow steps for automatic retries, or as regular workflow-level logic
- Stream Processing: Handles streaming responses and tool calls in a structured way
- Workflow Native: Fully integrated with Workflow DevKit for production-grade reliability
- AI SDK Parity: Supports the same options as AI SDK's
streamTextincluding generation settings, callbacks, and structured output
Good to Know
- Tools can be implemented as workflow steps (using
"use step"for automatic retries), or as regular workflow-level logic - Tools can use core library features like
sleep()and Hooks within theirexecutefunctions - The agent processes tool calls iteratively until completion or
maxStepsis reached - Default
maxStepsis unlimited - set a value to limit the number of LLM calls - The
stream()method returns{ messages, steps, experimental_output }containing the full conversation history, step details, and optional structured output - The
prepareStepcallback runs before each step and can modify model, messages, generation settings, tool choice, and context - Generation settings (temperature, maxOutputTokens, etc.) can be set on the constructor and overridden per-stream call
- Use
activeToolsto limit which tools are available for a specific stream call - The
onFinishcallback is called when all steps complete;onAbortis called if aborted
Examples
Basic Agent with Tools
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
async function getWeather({ location }: { location: string }) {
"use step";
// Fetch weather data
const response = await fetch(`https://api.weather.com?location=${location}`);
return response.json();
}
async function weatherAgentWorkflow(userQuery: string) {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
getWeather: {
description: "Get current weather for a location",
inputSchema: z.object({ location: z.string() }),
execute: getWeather,
},
},
system: "You are a helpful weather assistant. Always provide accurate weather information.",
});
await agent.stream({
messages: [
{
role: "user",
content: userQuery,
},
],
writable: getWritable<UIMessageChunk>(),
});
}Multiple Tools
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
async function getWeather({ location }: { location: string }) {
"use step";
return `Weather in ${location}: Sunny, 72°F`;
}
async function searchEvents({ location, category }: { location: string; category: string }) {
"use step";
return `Found 5 ${category} events in ${location}`;
}
async function multiToolAgentWorkflow(userQuery: string) {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
getWeather: {
description: "Get weather for a location",
inputSchema: z.object({ location: z.string() }),
execute: getWeather,
},
searchEvents: {
description: "Search for upcoming events in a location",
inputSchema: z.object({ location: z.string(), category: z.string() }),
execute: searchEvents,
},
},
});
await agent.stream({
messages: [
{
role: "user",
content: userQuery,
},
],
writable: getWritable<UIMessageChunk>(),
});
}Multi-turn Conversation
import { DurableAgent } from "@workflow/ai/agent";
import { z } from "zod";
async function searchProducts({ query }: { query: string }) {
"use step";
// Search product database
return `Found 3 products matching "${query}"`;
}
async function multiTurnAgentWorkflow() {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
searchProducts: {
description: "Search for products",
inputSchema: z.object({ query: z.string() }),
execute: searchProducts,
},
},
});
const writable = getWritable<UIMessageChunk>();
// First user message
// - Result is streamed to the provided `writable` stream
// - Message history is returned in `messages` for LLM context
let { messages } = await agent.stream({
messages: [
{ role: "user", content: "Find me some laptops" }
],
writable,
});
// Continue the conversation with the accumulated message history
const result = await agent.stream({
messages: [
...messages,
{ role: "user", content: "Which one has the best battery life?" }
],
writable,
});
// result.messages now contains the complete conversation history
return result.messages;
}Tools with Workflow Library Features
import { DurableAgent } from "@workflow/ai/agent";
import { sleep, defineHook, getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
// Define a reusable hook type
const approvalHook = defineHook<{ approved: boolean; reason: string }>();
async function scheduleTask({ delaySeconds }: { delaySeconds: number }) {
// Note: No "use step" for this tool call,
// since `sleep()` is a workflow level function
await sleep(`${delaySeconds}s`);
return `Slept for ${delaySeconds} seconds`;
}
async function requestApproval({ message }: { message: string }) {
// Note: No "use step" for this tool call either,
// since hooks are awaited at the workflow level
// Utilize a Hook for Human-in-the-loop approval
const hook = approvalHook.create({
metadata: { message }
});
console.log(`Approval needed - token: ${hook.token}`);
// Wait for the approval payload
const approval = await hook;
if (approval.approved) {
return `Request approved: ${approval.reason}`;
} else {
throw new Error(`Request denied: ${approval.reason}`);
}
}
async function agentWithLibraryFeaturesWorkflow(userRequest: string) {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
scheduleTask: {
description: "Pause the workflow for the specified number of seconds",
inputSchema: z.object({
delaySeconds: z.number(),
}),
execute: scheduleTask,
},
requestApproval: {
description: "Request approval for an action",
inputSchema: z.object({ message: z.string() }),
execute: requestApproval,
},
},
});
await agent.stream({
messages: [{ role: "user", content: userRequest }],
writable: getWritable<UIMessageChunk>(),
});
}Dynamic Context with prepareStep
Use prepareStep to modify settings before each step in the agent loop:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";
async function agentWithPrepareStep(userMessage: string) {
"use workflow";
const agent = new DurableAgent({
model: "openai/gpt-4.1-mini", // Default model
system: "You are a helpful assistant.",
});
await agent.stream({
messages: [{ role: "user", content: userMessage }],
writable: getWritable<UIMessageChunk>(),
prepareStep: async ({ stepNumber, messages }) => {
// Switch to a stronger model for complex reasoning after initial steps
if (stepNumber > 2 && messages.length > 10) {
return {
model: "anthropic/claude-sonnet-4.5",
};
}
// Trim context if messages grow too large
if (messages.length > 20) {
return {
messages: [
messages[0], // Keep system message
...messages.slice(-10), // Keep last 10 messages
],
};
}
return {}; // No changes
},
});
}Message Injection with prepareStep
Inject messages from external sources (like hooks) before each LLM call:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable, defineHook } from "workflow";
import type { UIMessageChunk } from "ai";
const messageHook = defineHook<{ message: string }>();
async function agentWithMessageQueue(initialMessage: string) {
"use workflow";
const messageQueue: Array<{ role: "user"; content: string }> = [];
// Listen for incoming messages via hook
const hook = messageHook.create();
hook.then(({ message }) => {
messageQueue.push({ role: "user", content: message });
});
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
system: "You are a helpful assistant.",
});
await agent.stream({
messages: [{ role: "user", content: initialMessage }],
writable: getWritable<UIMessageChunk>(),
prepareStep: ({ messages }) => {
// Inject queued messages before the next step
if (messageQueue.length > 0) {
const newMessages = messageQueue.splice(0);
return {
messages: [
...messages,
...newMessages.map(m => ({
role: m.role,
content: [{ type: "text" as const, text: m.content }],
})),
],
};
}
return {};
},
});
}Generation Settings
Configure model generation parameters at the constructor or stream level:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";
async function agentWithGenerationSettings() {
"use workflow";
// Set default generation settings in constructor
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
temperature: 0.7,
maxOutputTokens: 2000,
topP: 0.9,
});
// Override settings per-stream call
await agent.stream({
messages: [{ role: "user", content: "Write a creative story" }],
writable: getWritable<UIMessageChunk>(),
temperature: 0.9, // More creative for this call
maxSteps: 1,
});
// Use different settings for a different task
await agent.stream({
messages: [{ role: "user", content: "Summarize this document precisely" }],
writable: getWritable<UIMessageChunk>(),
temperature: 0.1, // More deterministic
maxSteps: 1,
});
}Limiting Steps with maxSteps
By default, the agent loops until completion. Use maxSteps to limit the number of LLM calls:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
async function searchWeb({ query }: { query: string }) {
"use step";
return `Results for "${query}": ...`;
}
async function analyzeResults({ data }: { data: string }) {
"use step";
return `Analysis: ${data}`;
}
async function multiStepAgent() {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
searchWeb: {
description: "Search the web for information",
inputSchema: z.object({ query: z.string() }),
execute: searchWeb,
},
analyzeResults: {
description: "Analyze search results",
inputSchema: z.object({ data: z.string() }),
execute: analyzeResults,
},
},
});
// Limit to 10 steps for safety on complex research tasks
const result = await agent.stream({
messages: [{ role: "user", content: "Research the latest AI trends and provide an analysis" }],
writable: getWritable<UIMessageChunk>(),
maxSteps: 10,
});
// Access step-by-step details
console.log(`Completed in ${result.steps.length} steps`);
}Callbacks for Monitoring
Use callbacks to monitor streaming progress, handle errors, and react to completion:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";
async function agentWithCallbacks() {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
});
await agent.stream({
messages: [{ role: "user", content: "Hello!" }],
writable: getWritable<UIMessageChunk>(),
maxSteps: 5,
// Called after each step completes
onStepFinish: async (step) => {
console.log(`Step finished: ${step.finishReason}`);
console.log(`Tokens used: ${step.usage.totalTokens}`);
},
// Called when streaming completes
onFinish: async ({ steps, messages }) => {
console.log(`Completed with ${steps.length} steps`);
console.log(`Final message count: ${messages.length}`);
},
// Called on errors
onError: async ({ error }) => {
console.error("Stream error:", error);
},
});
}Structured Output
Parse structured data from the LLM response using Output.object:
import { DurableAgent, Output } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
async function agentWithStructuredOutput() {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
});
const result = await agent.stream({
messages: [{ role: "user", content: "Analyze the sentiment of: 'I love this product!'" }],
writable: getWritable<UIMessageChunk>(),
experimental_output: Output.object({
schema: z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
confidence: z.number().min(0).max(1),
reasoning: z.string(),
}),
}),
});
// Access the parsed structured output
console.log(result.experimental_output);
// { sentiment: "positive", confidence: 0.95, reasoning: "..." }
}Tool Choice Control
Control when and which tools the model can use:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
async function agentWithToolChoice() {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
calculator: {
description: "Perform calculations",
inputSchema: z.object({ expression: z.string() }),
execute: async ({ expression }) => `Calculated: ${expression}`,
},
search: {
description: "Search for information",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `Results for: ${query}`,
},
},
toolChoice: "auto", // Default: model decides
});
// Force the model to use a tool
await agent.stream({
messages: [{ role: "user", content: "What is 2 + 2?" }],
writable: getWritable<UIMessageChunk>(),
toolChoice: "required",
maxSteps: 2,
});
// Prevent tool usage
await agent.stream({
messages: [{ role: "user", content: "Just chat with me" }],
writable: getWritable<UIMessageChunk>(),
toolChoice: "none",
});
// Force a specific tool
await agent.stream({
messages: [{ role: "user", content: "Calculate something" }],
writable: getWritable<UIMessageChunk>(),
toolChoice: { type: "tool", toolName: "calculator" },
maxSteps: 2,
});
// Limit available tools for this call
await agent.stream({
messages: [{ role: "user", content: "Just search, don't calculate" }],
writable: getWritable<UIMessageChunk>(),
activeTools: ["search"],
maxSteps: 2,
});
}Passing Context to Tools
Use experimental_context to pass shared context to tool executions:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import { z } from "zod";
import type { UIMessageChunk } from "ai";
interface UserContext {
userId: string;
permissions: string[];
}
async function agentWithContext(userId: string) {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
tools: {
getUserData: {
description: "Get user data",
inputSchema: z.object({}),
execute: async (_, { experimental_context }) => {
const ctx = experimental_context as UserContext;
return { userId: ctx.userId, permissions: ctx.permissions };
},
},
},
});
await agent.stream({
messages: [{ role: "user", content: "What are my permissions?" }],
writable: getWritable<UIMessageChunk>(),
maxSteps: 2,
experimental_context: {
userId,
permissions: ["read", "write"],
} as UserContext,
});
}Abort Signal for Cancellation
Use an abort signal to cancel long-running agent operations:
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";
async function agentWithAbort() {
"use workflow";
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
});
const controller = new AbortController();
// Set a timeout to abort after 30 seconds
setTimeout(() => controller.abort(), 30000);
await agent.stream({
messages: [{ role: "user", content: "Write a very long essay" }],
writable: getWritable<UIMessageChunk>(),
abortSignal: controller.signal,
onAbort: async ({ steps }) => {
console.log(`Aborted after ${steps.length} steps`);
},
});
}See Also
- Building Durable AI Agents - Complete guide to creating durable agents
- Queueing User Messages - Using prepareStep for message injection
- WorkflowChatTransport - Transport layer for AI SDK streams
- Workflows and Steps - Understanding workflow fundamentals
- AI SDK Loop Control - AI SDK's agent loop control patterns