new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

AI SDK · Core · all subjects

agents core

47 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

ToolLoopAgent default step limit

ToolLoopAgent stops after 20 steps by default using isStepCount(20). This default is a safety measure to prevent runaway loops that could result in excessive API calls and costs.

WorkflowAgent step limit behavior

WorkflowAgent does not apply a default step limit. It continues until the model stops calling tools or another natural termination condition is met. Configure an explicit condition such as isStepCount(20) when you need to bound its model calls.

Loop continuation and termination conditions

The ToolLoopAgent loop continues until: a finish reasoning other than tool-calls is returned, a tool that is invoked does not have an execute function, a tool call needs approval, or a stop condition is met.

stopWhen parameter behavior with tool results

When you provide stopWhen, the agent continues executing after tool calls until a stopping condition is met. When the condition is an array, execution stops when any of the conditions are met.

isStepCount stop condition example

Example: import { ToolLoopAgent, isStepCount } from 'ai'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { /* your tools */ }, stopWhen: isStepCount(50), }); const result = await agent.generate({ prompt: 'Analyze this dataset and create a summary report', });

isLoopFinished stop condition removes default step limit

Using isLoopFinished() with ToolLoopAgent removes its default step limit and lets it run until the model naturally stops making tool calls. Use with caution as the agent could potentially run indefinitely or incur significant costs if the model keeps making tool calls.

isLoopFinished example

Example: import { ToolLoopAgent, isLoopFinished } from 'ai'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { /* your tools */ }, stopWhen: isLoopFinished(), }); const result = await agent.generate({ prompt: 'Analyze this dataset and create a summary report', });

Combining multiple stop conditions

Example: import { ToolLoopAgent, isStepCount, hasToolCall } from 'ai'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { /* your tools */ }, stopWhen: [ isStepCount(20), hasToolCall('someTool', 'done'), ], }); const result = await agent.generate({ prompt: 'Research and analyze the topic', });

StopCondition custom condition signature

Custom stopping conditions receive a single parameter with { steps } property. The steps property contains all previous steps with their results. Return a boolean: true to stop, false to continue.

Custom stop condition example with ANSWER text

Example: import { ToolLoopAgent, StopCondition, ToolSet } from 'ai'; const tools = { /* your tools */ } satisfies ToolSet; const hasAnswer: StopCondition<typeof tools> = ({ steps }) => { return steps.some(step => step.text?.includes('ANSWER:')) ?? false; }; const agent = new ToolLoopAgent({ model: __MODEL__, tools, stopWhen: hasAnswer, }); const result = await agent.generate({ prompt: 'Find the answer and respond with "ANSWER: [your answer]"', });

Custom stop condition with token budget tracking

Example: const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => { const totalUsage = steps.reduce((acc, step) => ({ inputTokens: acc.inputTokens + (step.usage?.inputTokens ?? 0), outputTokens: acc.outputTokens + (step.usage?.outputTokens ?? 0), }), { inputTokens: 0, outputTokens: 0 }); const costEstimate = (totalUsage.inputTokens * 0.01 + totalUsage.outputTokens * 0.03) / 1000; return costEstimate > 0.5; };

prepareStep callback behavior

The prepareStep callback runs before each step in the loop and defaults to the initial settings if you don't return any changes. It receives messages for the current step, plus initialMessages and responseMessages when you need to distinguish the original input from assistant/tool messages accumulated in earlier steps.

prepareStep messages persistence

The messages value contains the messages that will be sent for the current step. When you return a messages override from prepareStep, that changed list becomes the base for later steps. New assistant and tool response messages are appended to it as the loop continues.

prepareStep model override example

Example: import { ToolLoopAgent } from 'ai'; const agent = new ToolLoopAgent({ model: 'openai/gpt-4o-mini', tools: { /* your tools */ }, prepareStep: async ({ stepNumber, messages }) => { if (stepNumber > 2 && messages.length > 10) { return { model: __MODEL__, }; } return {}; }, }); const result = await agent.generate({ prompt: '...', });

prepareStep can override model call settings

prepareStep can override maxOutputTokens, temperature, topP, topK, presencePenalty, frequencyPenalty, stopSequences, seed, and reasoning. These overrides apply only to the current step. When a setting is omitted or undefined, the top-level value is used for that step. Defined falsy values such as temperature: 0, seed: 0, and an empty stopSequences array are preserved.

prepareStep model call settings example

Example: import { ToolLoopAgent } from 'ai'; const agent = new ToolLoopAgent({ model: __MODEL__, temperature: 0.7, tools: { /* your tools */ }, prepareStep: async ({ stepNumber }) => { if (stepNumber === 0) { return { temperature: 0, maxOutputTokens: 300, }; } return {}; }, }); const result = await agent.generate({ prompt: '...', });

pruneMessages helper for context management

The pruneMessages helper provides a built-in way to remove selected messages. It accepts options: reasoning ('all' to remove all reasoning), toolCalls ('before-last-3-messages' to keep only last 3), and emptyMessages ('remove' to delete empty messages). Use it inside prepareStep when you want a simple compaction strategy.

pruneMessages example with token threshold

Example: import { ToolLoopAgent, pruneMessages, type ModelMessage } from 'ai'; const COMPACTION_THRESHOLD = 100_000; const estimateTokens = (messages: ModelMessage[]) => { return JSON.stringify(messages).length / 4; }; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { /* your tools */ }, prepareStep: async ({ messages }) => { if (estimateTokens(messages) > COMPACTION_THRESHOLD) { return { messages: pruneMessages({ messages, reasoning: 'all', toolCalls: 'before-last-3-messages', emptyMessages: 'remove', }), }; } }, }); const result = await agent.generate({ prompt: '...', });

prepareStep activeTools for tool selection

Control which tools are available at each step by returning activeTools array from prepareStep. This restricts the model to only use the specified tools for that step.

prepareStep activeTools example

Example: import { ToolLoopAgent } from 'ai'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { search: searchTool, analyze: analyzeTool, summarize: summarizeTool, }, prepareStep: async ({ stepNumber, steps }) => { if (stepNumber <= 2) { return { activeTools: ['search'], toolChoice: 'required', }; } if (stepNumber <= 5) { return { activeTools: ['analyze'], }; } return { activeTools: ['summarize'], toolChoice: 'required', }; }, }); const result = await agent.generate({ prompt: '...', });

prepareStep toolChoice force specific tool

Force a specific tool to be used by returning toolChoice with type 'tool' and toolName: prepareStep: async ({ stepNumber }) => { if (stepNumber === 0) { return { toolChoice: { type: 'tool', toolName: 'search' }, }; } if (stepNumber === 5) { return { toolChoice: { type: 'tool', toolName: 'summarize' }, }; } return {}; };

prepareStep message transformation example

Example: import { ToolLoopAgent } from 'ai'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { /* your tools */ }, prepareStep: async ({ messages, stepNumber }) => { const processedMessages = messages.map(msg => { if (msg.role === 'tool' && msg.content.length > 1000) { return { ...msg, content: summarizeToolResult(msg.content), }; } return msg; }); return { messages: processedMessages }; }, }); const result = await agent.generate({ prompt: '...', });

prepareStep experimental_sandbox override

An experimental sandbox returned from prepareStep only applies to tool execution in that step. Later steps use the top-level experimental_sandbox unless they return their own experimental sandbox override.

prepareStep experimental_sandbox example

Example: import { ToolLoopAgent } from 'ai'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { runCommand, }, experimental_sandbox: defaultSandbox, prepareStep: async ({ stepNumber }) => { if (stepNumber === 0) { return { experimental_sandbox: setupSandbox, }; } return {}; }, }); const result = await agent.generate({ prompt: '...', });

stopWhen and prepareStep parameter access

Both stopWhen and prepareStep receive: model (current model configuration), stepNumber (current step number, 0-indexed), steps (all previous steps with their results), and messages (messages to be sent to the model).

Access step information example

Example: prepareStep: async ({ model, stepNumber, steps, messages }) => { const previousToolCalls = steps.flatMap(step => step.toolCalls); const previousResults = steps.flatMap(step => step.toolResults); if (previousToolCalls.some(call => call.toolName === 'dataAnalysis')) { return { toolChoice: { type: 'tool', toolName: 'reportGenerator' }, }; } return {}; },

Forced tool calling with done tool

Force the agent to always use tools by combining toolChoice: 'required' with a done tool that has no execute function. This pattern ensures the agent uses tools for every step and stops only when it explicitly signals completion by calling the done tool.

Forced tool calling example

Example: import { ToolLoopAgent, tool } from 'ai'; import { z } from 'zod'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { search: searchTool, analyze: analyzeTool, done: tool({ description: 'Signal that you have finished your work', inputSchema: z.object({ answer: z.string().describe('The final answer'), }), }), }, toolChoice: 'required', }); const result = await agent.generate({ prompt: 'Research and analyze this topic, then provide your answer.', }); const toolCall = result.staticToolCalls[0]; if (toolCall?.toolName === 'done') { console.log(toolCall.input.answer); }

Manual loop control with generateText

For scenarios requiring complete control over the agent loop, use AI SDK Core functions (generateText and streamText) to implement your own loop management instead of using stopWhen and prepareStep. This approach provides maximum flexibility for complex workflows.

Manual agent loop implementation example

Example: import { generateText, ModelMessage } from 'ai'; const messages: ModelMessage[] = [{ role: 'user', content: '...' }]; let step = 0; const maxSteps = 10; while (step < maxSteps) { const result = await generateText({ model: __MODEL__, messages, tools: { /* your tools here */ }, }); messages.push(...result.responseMessages); if (result.text) { break; } step++; }

Manual loop control benefits

Manual loop control gives you complete control over: message history management, step-by-step decision making, custom stopping conditions, dynamic tool and model selection, and error handling and recovery.

ToolLoopAgent toolApproval parameter

The toolApproval parameter on ToolLoopAgent allows you to review, approve, or deny selected tool calls before they execute. It applies only to tools executed by the AI SDK; provider-executed tools run provider-side and do not use AI SDK tool approvals.

toolApproval approval statuses

Every approval rule returns one of these statuses: 'not-applicable' (execute the tool normally without approval metadata, the default), 'approved' (record an automatic approval then execute the tool), 'denied' (record an automatic denial and return a denied tool output), or 'user-approval' (emit an approval request and wait for an explicit response). For automatic approvals and denials, use object form {type: status, reason: string} to include a reason. Approval functions can also return undefined, treated as 'not-applicable'.

Per-tool toolApproval map

Use a per-tool map in toolApproval when each tool has a simple policy. Assign a string status or object with type and reason to each tool name. When the tool is called, the agent returns a tool-approval-request instead of executing the tool.

Per-tool approval function parameters

When using a per-tool approval function in toolApproval that decides based on tool input, the function receives the typed input plus toolCallId, messages, toolContext, and runtimeContext. The function can return a status string, status object with reason, or undefined.

GenericToolApprovalFunction parameter

Pass a function directly as toolApproval (called GenericToolApprovalFunction) when approval depends on the full tool call, shared state across tools, or the complete tool set. The function receives: toolCall (with toolName, toolCallId, input, and dynamic flag), tools (all available tools), toolsContext (context for all tools), messages (sent to the model for the step that produced the tool call), and runtimeContext (call's shared runtime context).

ToolLoopAgent prepareCall toolApproval configuration

The toolApproval setting can be returned from prepareCall on ToolLoopAgent. This is useful when approval policy depends on call options, tenant policy, or user permissions. Provide a callOptionsSchema and use the options within prepareCall to determine the toolApproval configuration per request.

Manual approval flow with agent.generate()

Manual approval requires: (1) Call agent.generate() or agent.stream() with toolApproval. (2) Read the tool-approval-request from the result or UI stream. (3) Ask the user or approval system for a decision. (4) Add a tool-approval-response to the messages with type, approvalId, approved boolean, and optional reason. (5) Call the agent again with the updated messages. If approved, the tool runs on the second call. If denied, the model receives the denial and can respond without the tool result.

ToolApprovalResponse structure

ToolApprovalResponse is a message role 'tool' with content containing an array of approval responses. Each response object has type: 'tool-approval-response', approvalId (matching the approval request), approved: boolean, and optional reason: string.

Tool approval security trust model

In the standard useChat pattern, the server rebuilds conversation from client-sent messages each turn without persisting state between requests. Tool approvals from this client-controlled message history are re-validated before execution: tool input is checked against schema and approval policy is re-evaluated. However, without additional protection, a client crafting a valid-looking approval for schema-conforming input can bypass the human-in-the-loop step.

experimental_toolApprovalSecret parameter

Use experimental_toolApprovalSecret to cryptographically bind approvals to the server that issued them. Configure it on ToolLoopAgent, generateText, or streamText. The server HMAC-signs each approval request at issuance and verifies the signature when the approval is replayed. A forged or tampered approval is rejected before tool execution.

experimental_toolApprovalSecret setup process

To set up experimental_toolApprovalSecret: (1) Generate a high-entropy random string of at least 32 bytes using 'openssl rand -base64 32'. (2) Store it as an environment variable accessible to all server instances. (3) Pass it to ToolLoopAgent, generateText, or streamText via experimental_toolApprovalSecret. Every serverless instance that might handle a request needs the same secret, since one instance signs the approval and a different instance may verify it on the next turn.

experimental_toolApprovalSecret signing behavior

The signature from experimental_toolApprovalSecret binds the approval to the exact tool name, tool call ID, and input arguments. Changing any of these after signing invalidates the approval. Approval requests without a valid signature are rejected (fail-closed). With no secret configured, approvals work as before (backward compatible). The secret is never sent to the client or included in the stream.

WorkflowAgent experimental_toolApprovalSecret usage

WorkflowAgent also supports experimental_toolApprovalSecret. It signs in a workflow step before writing the durable approval request. Pass an environment variable reference such as {environmentVariable: 'TOOL_APPROVAL_SECRET'} so the raw secret is read only inside signing and verification steps. Only the signature is persisted and sent to the client.

Tool approval with generateText and streamText

The toolApproval parameter can be used with generateText and streamText in addition to ToolLoopAgent.

Subagent tool approvals not supported

Subagent tools cannot use toolApproval.

HarnessAgent skills option

Pass skills to HarnessAgent with the `skills` setting as an array of skill objects. Each skill object contains `name`, `description`, `content`, and optionally `files` properties.

Give your agent this brain