Skip to content

Lifecycle hooks

Think owns the streamText call and provides hooks at each stage of the chat turn. Hooks fire on every turn regardless of entry path — WebSocket chat, sub-agent chat(), saveMessages(), durable submitMessages() execution, continueLastTurn(), and auto-continuation after tool results.

Hook summary

HookWhen it firesReturnAsync
configureSession(session)Once during onStartSessionyes
beforeTurn(ctx)Before streamTextTurnConfig or voidyes
beforeStep(ctx)Before each model stepStepConfig or voidyes
beforeToolCall(ctx)Before a server-side tool executesToolCallDecision or voidyes
afterToolCall(ctx)After a tool outcome is knownvoidyes
onStepFinish(ctx)After each step completesvoidyes
onChunk(ctx)Per streaming chunkvoidyes
onChatResponse(result)After turn completes and message is persistedvoidyes
onChatError(error, ctx?)On error during a turnerror to propagateno

Execution order

For a turn with two tool calls:

flowchart TD
    cfg["configureSession() — once at startup, not per-turn"] --> bt["beforeTurn() — inspect context, override model/tools/prompt"]
    bt --> bs

    subgraph loop ["streamText (repeats per step)"]
        bs["beforeStep()"] --> chunk["onChunk() — per streaming chunk"]
        chunk --> btc["beforeToolCall()"]
        btc --> exec["tool executes"]
        exec --> atc["afterToolCall()"]
        atc --> sf["onStepFinish()"]
        sf -->|"more steps"| bs
    end

    sf -->|"turn complete"| ocr["onChatResponse() — message persisted, turn lock released"]

beforeTurn

Called before streamText. Receives the fully assembled context — system prompt, converted messages, merged tools, and model. Return a TurnConfig to override any part, or void to accept defaults.

TypeScript
beforeTurn(ctx: TurnContext): TurnConfig | void | Promise<TurnConfig | void>

TurnContext

FieldTypeDescription
systemstringAssembled system prompt (from context blocks or getSystemPrompt())
messagesModelMessage[]Assembled model messages (truncated, pruned)
toolsToolSetMerged tool set (workspace + getTools + session + extensions + MCP + client)
modelLanguageModelThe model from getModel()
continuationbooleanWhether this is a continuation turn (auto-continue after tool result)
bodyRecord<string, unknown>Custom body fields from the client request

TurnConfig

All fields are optional. Return only what you want to change.

FieldTypeDescription
modelLanguageModelOverride the model for this turn
systemstringOverride the system prompt
messagesModelMessage[]Override the assembled messages
toolsToolSetExtra tools to merge (additive)
activeToolsstring[]Limit which tools the model can call
toolChoiceToolChoiceForce a specific tool call
maxStepsnumberOverride maxSteps for this turn
sendReasoningbooleanSend reasoning chunks for this turn
chatStreamStallTimeoutMsnumberOverride the stream-stall watchdog for this turn (0 disables it); auto-resets after the turn. Useful for a turn with a known-slow tool — refer to Durable recovery
outputOutputRequest structured output for this turn
providerOptionsRecord<string, unknown>Provider-specific options
experimental_telemetryobjectAI SDK telemetry settings for this turn

Examples

Switch to a cheaper model for continuation turns:

TypeScript
beforeTurn(ctx: TurnContext) {
if (ctx.continuation) {
return { model: this.cheapModel };
}
}

Restrict which tools the model can call:

TypeScript
beforeTurn(ctx: TurnContext) {
return { activeTools: ["read", "write", "getWeather"] };
}

Add per-turn context from the client body:

TypeScript
beforeTurn(ctx: TurnContext) {
if (ctx.body?.selectedFile) {
return {
system: ctx.system + `\n\nUser is editing: ${ctx.body.selectedFile}`,
};
}
}

Hide reasoning for internal continuation turns:

TypeScript
beforeTurn(ctx: TurnContext) {
if (ctx.continuation) {
return { sendReasoning: false };
}
}

Force structured output for a turn:

TypeScript
import { Output } from "ai";
import { z } from "zod";
const ResultSchema = z.object({ severity: z.enum(["low", "high"]) });
beforeTurn(ctx: TurnContext) {
if (ctx.body?.mode === "structured-answer") {
return {
output: Output.object({ schema: ResultSchema }),
activeTools: [],
};
}
}

output is a turn-level setting only. The AI SDK's prepareStep does not accept an output override, so beforeStep cannot toggle structured output on a single step.

beforeStep

Called before each AI SDK step in the agentic loop. Think forwards this hook to streamText as prepareStep, so it receives the AI SDK's full prepare-step context and can return per-step overrides. Use beforeTurn for turn-wide assembly and beforeStep when the decision depends on the step number or previous step results.

TypeScript
beforeStep(ctx: PrepareStepContext): StepConfig | void {
if (ctx.stepNumber > 0) {
return { activeTools: [] };
}
}

beforeToolCall

Called before a server-side tool's execute function runs. Think wraps each server-side tool so the hook can allow, modify, block, or substitute the call before the model receives the tool result.

TypeScript
beforeToolCall(ctx: ToolCallContext): ToolCallDecision | void {
if (ctx.toolName === "delete" && this.isReadOnlyMode) {
return { action: "block", reason: "delete is disabled in read-only mode" };
}
if (ctx.toolName === "weather") {
const cached = this.weatherCache.get(JSON.stringify(ctx.input));
if (cached) return { action: "substitute", output: cached };
}
}
FieldTypeDescription
toolNamestringName of the tool being called
inputunknownInput the model provided
toolCallIdstringID for this tool call
messagesModelMessage[]Messages visible at tool execution time
abortSignalAbortSignal | undefinedSignal that aborts if the turn is canceled

Return a ToolCallDecision to control execution:

DecisionBehavior
void or { action: "allow" }Run the original tool with the original input
{ action: "allow", input }Run the original tool with modified input
{ action: "block", reason }Skip the original tool and return reason as the tool result
{ action: "substitute", output }Skip the original tool and return output as the tool result

If a wrapped tool returns an AsyncIterable for preliminary tool results, Think collapses the iterable to its final yielded value after beforeToolCall runs. If you need true preliminary streaming from that tool, avoid intercepting it with beforeToolCall.

afterToolCall

Called after a tool outcome is known. This includes real executions, blocked calls, substituted calls, and thrown tool errors.

TypeScript
afterToolCall(ctx: ToolCallResultContext) {
if (!ctx.success) return;
this.env.ANALYTICS.writeDataPoint({
blobs: [ctx.toolName],
doubles: [JSON.stringify(ctx.output).length],
});
}
FieldTypeDescription
toolNamestringName of the tool that was called
inputunknownInput the model provided
toolCallIdstringID for this tool call
messagesModelMessage[]Messages visible at tool execution time
durationMsnumberTool execution duration in milliseconds
successbooleanWhether the model received a successful tool outcome
outputunknownPresent when success is true
errorunknownPresent when success is false

For blocked and substituted tool calls, success is true because the model receives a valid tool result. Only thrown errors from the original tool execution surface as success: false.

onStepFinish

Called after each step completes in the agentic loop. StepContext is the AI SDK's step-finish event, so it includes the full step record: generated text, reasoning, files, sources, typed tool calls and results, usage, warnings, request and response metadata, and provider metadata.

TypeScript
onStepFinish(ctx: StepContext) {
console.log(
`Step ${ctx.stepNumber} (${ctx.finishReason}): ` +
`${ctx.usage.inputTokens}in/${ctx.usage.outputTokens}out`,
);
}
FieldDescription
stepNumberZero-based index of the step
textText generated in this step
reasoningReasoning parts emitted by the model
filesFiles generated during the step
sourcesCitations or sources used by the model
toolCallsTyped tool calls made in this step
toolResultsTyped tool results received in this step
finishReasonWhy the step ended
usageToken usage, including cache and reasoning tokens
providerMetadataProvider-specific metadata

onChunk

Called for each streaming chunk. High-frequency — fires per token. Use for streaming analytics, progress indicators, or token counting. Observational only.

onChatResponse

Called after a chat turn produces and persists an assistant message. The turn lock is released before this hook runs, so it is safe to call saveMessages or other methods from inside.

Fires for all turn paths that persist an assistant message: WebSocket, sub-agent RPC, saveMessages, and auto-continuation. If a turn fails before producing any assistant parts, onChatError handles the error instead.

TypeScript
onChatResponse(result: ChatResponseResult) {
if (result.status === "completed") {
console.log(`Turn ${result.requestId}: ${result.message.parts.length} parts`);
}
}
FieldTypeDescription
messageUIMessageThe persisted assistant message
requestIdstringUnique ID for this turn
continuationbooleanWhether this was a continuation turn
status"completed" | "error" | "aborted"How the turn ended
errorstring?Error message (when status is "error")

onChatError

Called when an error occurs during a chat turn. Return the error to propagate it, or return a different error. The optional context describes where the failure happened and whether user messages were already persisted. The partial assistant message (if any) is persisted before this hook fires.

TypeScript
onChatError(error: unknown, ctx?: ChatErrorContext): unknown

ChatErrorContext includes:

FieldTypeDescription
requestIdstring | undefinedChat request ID, when available
stage"parse" | "persist" | "turn" | "stream" | "recovery" | "transcript"Failure stage
messagesPersistedbooleanWhether incoming user messages were already stored

Think also emits chat:request:failed on the agents:chat observability channel with the same stage and persistence information.

TypeScript
onChatError(error: unknown, ctx?: ChatErrorContext) {
console.error("Chat turn failed:", ctx?.stage, error);
return new Error("Something went wrong. Please try again.");
}