Think 拥有 streamText 调用,并在聊天轮次各阶段提供钩子。无论入口路径——WebSocket 聊天、子 agent chat()、saveMessages()、持久 submitMessages() 执行、continueLastTurn() 或工具结果后的自动续聊——钩子都会在每个轮次触发。
| Hook | 触发时机 | 返回值 | 异步 |
|---|---|---|---|
configureSession(session) |
onStart 期间一次 |
Session |
是 |
beforeTurn(ctx) |
streamText 之前 |
TurnConfig 或 void |
是 |
beforeStep(ctx) |
每个模型步骤之前 | StepConfig 或 void |
是 |
beforeToolCall(ctx) |
服务端工具执行之前 | ToolCallDecision 或 void |
是 |
afterToolCall(ctx) |
工具结果已知之后 | void | 是 |
onStepFinish(ctx) |
每个步骤完成之后 | void | 是 |
onChunk(ctx) |
每个流式分块 | void | 是 |
onChatResponse(result) |
轮次完成且消息已持久化后 | void | 是 |
onChatError(error, ctx?) |
轮次期间出错 | 要传播的错误 | 否 |
classifyChatError(error, ctx?) |
轮次出错且启用 contextOverflow.reactive 时 |
ChatErrorClassification 或 void |
否 |
对于包含两次工具调用的轮次:
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"]
在 streamText 之前调用。接收完整组装的上下文——系统提示词、转换后的消息、合并的工具与模型。返回 TurnConfig 可覆盖任意部分,返回 void 则接受默认值。
beforeTurn(ctx: TurnContext): TurnConfig | void | Promise<TurnConfig | void>| 字段 | 类型 | 描述 |
|---|---|---|
system |
string |
组装的系统提示词(来自上下文块或 getSystemPrompt()) |
messages |
ModelMessage[] |
组装的模型消息(已截断、修剪) |
tools |
ToolSet |
合并的工具集(工作区 + getTools + 会话 + 扩展 + MCP + 客户端) |
model |
LanguageModel |
getModel() 返回的模型 |
continuation |
boolean |
是否为续聊轮次(工具结果后自动续聊) |
body |
Record<string, unknown> |
客户端请求中的自定义 body 字段 |
所有字段均为可选。仅返回需要修改的部分。
| 字段 | 类型 | 描述 |
|---|---|---|
model |
LanguageModel |
覆盖本轮次的模型 |
system |
string |
覆盖系统提示词 |
messages |
ModelMessage[] |
覆盖组装的消息 |
tools |
ToolSet |
额外合并的工具(叠加) |
activeTools |
string[] |
限制模型可调用的工具 |
toolChoice |
ToolChoice |
强制调用特定工具 |
maxSteps |
number |
覆盖本轮次的 maxSteps |
sendReasoning |
boolean |
本轮发送推理片段 |
chatStreamStallTimeoutMs |
number |
覆盖本轮次的流停滞看门狗(0 禁用);轮次结束后自动重置。适用于已知慢工具的轮次——参阅 持久恢复 |
output |
Output |
本轮次请求结构化输出 |
providerOptions |
Record<string, unknown> |
提供商特定选项 |
experimental_telemetry |
object |
本轮次的 AI SDK 遥测设置 |
experimental_transform |
StreamTextTransform | StreamTextTransform[] |
本轮次的 AI SDK 流转换——检查或重写流部分(例如从工具结果派生 source 部分)。按顺序应用。 |
续聊轮次切换到更便宜的模型:
beforeTurn(ctx: TurnContext) {
if (ctx.continuation) {
return { model: this.cheapModel };
}
}限制模型可调用的工具:
beforeTurn(ctx: TurnContext) {
return { activeTools: ["read", "write", "getWeather"] };
}从客户端 body 添加每轮次上下文:
beforeTurn(ctx: TurnContext) {
if (ctx.body?.selectedFile) {
return {
system: ctx.system + `\n\nUser is editing: ${ctx.body.selectedFile}`,
};
}
}内部续聊轮次隐藏推理:
beforeTurn(ctx: TurnContext) {
if (ctx.continuation) {
return { sendReasoning: false };
}
}为轮次强制结构化输出:
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 仅为轮次级设置。AI SDK 的 prepareStep 不接受 output 覆盖,因此 beforeStep 无法在单个步骤切换结构化输出。
在 agentic 循环中每个 AI SDK 步骤之前调用。Think 将此钩子转发给 streamText 的 prepareStep,因此接收 AI SDK 完整的 prepare-step 上下文,并可返回每步覆盖。轮次级组装用 beforeTurn,当决策依赖步骤编号或前一步结果时用 beforeStep。
beforeStep(ctx: PrepareStepContext): StepConfig | void {
if (ctx.stepNumber > 0) {
return { activeTools: [] };
}
}在服务端工具的 execute 运行之前调用。Think 包装每个服务端工具,使钩子可在模型收到工具结果前允许、修改、阻止或替换调用。
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 };
}
}| 字段 | 类型 | 描述 |
|---|---|---|
toolName |
string |
被调用工具的名称 |
input |
unknown |
模型提供的输入 |
toolCallId |
string |
本次工具调用的 ID |
messages |
ModelMessage[] |
工具执行时可见的消息 |
abortSignal |
AbortSignal | undefined |
轮次取消时中止的信号 |
返回 ToolCallDecision 控制执行:
| Decision | 行为 |
|---|---|
void 或 { action: "allow" } |
用原始输入运行原始工具 |
{ action: "allow", input } |
用修改后的输入运行原始工具 |
{ action: "block", reason } |
跳过原始工具,将 reason 作为工具结果返回 |
{ action: "substitute", output } |
跳过原始工具,将 output 作为工具结果返回 |
若包装工具为初步工具结果返回 AsyncIterable,Think 会在 beforeToolCall 之后将 iterable 折叠为最终 yield 值。若需要该工具的真正初步流式输出,避免用 beforeToolCall 拦截。
在工具结果已知后调用,包括真实执行、被阻止、被替换以及工具抛错。
afterToolCall(ctx: ToolCallResultContext) {
if (!ctx.success) return;
this.env.ANALYTICS.writeDataPoint({
blobs: [ctx.toolName],
doubles: [JSON.stringify(ctx.output).length],
});
}| 字段 | 类型 | 描述 |
|---|---|---|
toolName |
string |
被调用工具的名称 |
input |
unknown |
模型提供的输入 |
toolCallId |
string |
本次工具调用的 ID |
messages |
ModelMessage[] |
工具执行时可见的消息 |
durationMs |
number |
工具执行耗时(毫秒) |
success |
boolean |
模型是否收到成功的工具结果 |
output |
unknown |
success 为 true 时存在 |
error |
unknown |
success 为 false 时存在 |
对于被阻止和替换的工具调用,success 为 true,因为模型收到有效工具结果。仅原始工具执行抛错时 success 为 false。
在 agentic 循环中每个步骤完成后调用。StepContext 是 AI SDK 的 step-finish 事件,包含完整步骤记录:生成文本、推理、文件、来源、类型化工具调用与结果、用量、警告、请求/响应元数据及提供商元数据。
onStepFinish(ctx: StepContext) {
console.log(
`Step ${ctx.stepNumber} (${ctx.finishReason}): ` +
`${ctx.usage.inputTokens}in/${ctx.usage.outputTokens}out`,
);
}| 字段 | 描述 |
|---|---|
stepNumber |
步骤的从零开始索引 |
text |
本步骤生成的文本 |
reasoning |
模型发出的推理部分 |
files |
步骤期间生成的文件 |
sources |
模型使用的引用或来源 |
toolCalls |
本步骤的类型化工具调用 |
toolResults |
本步骤收到的类型化工具结果 |
finishReason |
步骤结束原因 |
usage |
token 用量,含缓存与推理 token |
providerMetadata |
提供商特定元数据 |
每个流式分块调用。高频——每个 token 触发。用于流式分析、进度指示或 token 计数。仅观察,不可修改。
聊天轮次生成并持久化 assistant 消息之后调用。轮次锁在此钩子运行前释放,因此可在内部安全调用 saveMessages 或其他方法。
对所有持久化 assistant 消息的轮次路径触发:WebSocket、子 agent RPC、saveMessages 与自动续聊。若轮次在产生任何 assistant 部分前失败,由 onChatError 处理。
onChatResponse(result: ChatResponseResult) {
if (result.status === "completed") {
console.log(`Turn ${result.requestId}: ${result.message.parts.length} parts`);
}
}| 字段 | 类型 | 描述 |
|---|---|---|
message |
UIMessage |
已持久化的 assistant 消息 |
requestId |
string |
本轮次的唯一 ID |
continuation |
boolean |
是否为续聊轮次 |
status |
"completed" | "error" | "aborted" |
轮次如何结束 |
error |
string? |
错误消息(status 为 "error" 时) |
聊天轮次期间出错时调用。返回错误以传播,或返回不同错误。可选上下文描述失败位置及用户消息是否已持久化。部分 assistant 消息(若有)在此钩子前持久化。
onChatError(error: unknown, ctx?: ChatErrorContext): unknownChatErrorContext 包含:
| 字段 | 类型 | 描述 |
|---|---|---|
requestId |
string | undefined |
聊天请求 ID(可用时) |
stage |
"parse" | "persist" | "turn" | "stream" | "recovery" | "transcript" |
失败阶段 |
messagesPersisted |
boolean |
入站用户消息是否已存储 |
classification |
ChatErrorClassification | undefined |
上下文溢出无法恢复时,终端 onChatError 设为 "context_overflow"(参阅 classifyChatError);否则为 undefined |
Think 还会在 agents:chat 可观测性通道发出 chat:request:failed,含相同阶段与持久化信息。
onChatError(error: unknown, ctx?: ChatErrorContext) {
console.error("Chat turn failed:", ctx?.stage, error);
if (ctx?.classification === "context_overflow") {
return new Error("This conversation is too long to continue. Please start a new one.");
}
return new Error("Something went wrong. Please try again.");
}轮次期间出错时、在 onChatError 之前调用。将原始提供商错误映射为与提供商无关的类别,使 Think 无需在框架中硬编码提供商字符串即可响应——与传给 compactAfter() 的 tokenCounter 分工相同。应用负责映射,因为它知道使用的提供商与模型。
classifyChatError(error: unknown, ctx?: ChatErrorContext): ChatErrorClassification | voidChatErrorClassification 为 "context_overflow" | "rate_limit" | "transient" | "fatal" | "unknown"。目前此钩子仅驱动上下文溢出恢复。轮次出错且启用 contextOverflow.reactive 时 Think 会调用;reactive 关闭则不调用。
返回 "context_overflow" 会运行压缩并重试兜底(参阅上下文窗口溢出恢复)。若恢复无法挽救轮次,该分类通过 ChatErrorContext.classification 浮现到终端 onChatError。
其他类别保留供将来使用。目前返回其中之一为无操作,不会转发到 onChatError。返回 void(默认)保持现有终端行为。
参数可能是 Error、AI SDK APICallError(含 statusCode/responseBody),或——对流内提供商错误以流错误部分而非抛出出现时——错误消息字符串。请相应收窄类型。提供商上下文溢出错误以流内错误部分到达,因此此钩子收到字符串形式,而非抛出的异常。
第二参数为 ChatErrorContext。溢出恢复期间为 { stage: "stream", requestId },分类器可将错误与进行中的轮次关联——例如调用 cancelChat(requestId) 并退出恢复。
常见做法是赋值内置 defaultContextOverflowClassifier,匹配 Anthropic、OpenAI、Google、Bedrock 等的上下文溢出错误:
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think {
classifyChatError = defaultContextOverflowClassifier;
}import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think<Env> {
override classifyChatError = defaultContextOverflowClassifier;
}或编写自定义分类器,可选委托给内置分类器:
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think {
classifyChatError(error) {
if (error instanceof Error && /rate.?limit/i.test(error.message)) {
return "rate_limit";
}
return defaultContextOverflowClassifier(error);
}
}import type { ChatErrorClassification } from "@cloudflare/think";
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";
export class MyAgent extends Think<Env> {
override classifyChatError(error: unknown): ChatErrorClassification | void {
if (error instanceof Error && /rate.?limit/i.test(error.message)) {
return "rate_limit";
}
return defaultContextOverflowClassifier(error);
}
}