Agent Hooks API 参考
本页文档记录了每个 Flue Hook —— 智能体函数在渲染期间调用的函数 —— 以及管理它们的渲染契约。除非另有说明,所有符号均从 @flue/runtime 导出。每个 Hook 的章节涵盖该 Hook 自身的契约 —— 参数、渲染规则、何时运行 —— 并将 Hook 消费的资源形状(defineTool、defineSkill、defineSubagent、defineMcpConnection)以及智能体周围的编程接口(dispatch()、init()、start()、路由、harness)参阅 Agent API。
各 Hook:
useModel()—— 声明智能体的 LLM 及其调优参数。必需。useSandbox()—— 挂载智能体的执行环境。useTool()—— 挂载一个模型可调用的工具。useMcpConnection()—— 声明一个远程 MCP 服务器,智能体使用其工具。useSkill()—— 在智能体的目录中挂载一个技能。useSubagent()—— 声明一个task工具的委托智能体。useInstruction()—— 追加原始指令文本。usePersistentState()—— 持久化的实例级状态。useInitialData()—— 读取实例创建时的数据。useDelivery()—— 读取模型当前面对的消息。useDispatchMessage()—— 绑定到此实例的派发器。useDataWriter()—— 向客户端流式传输命名数据片段。useAgentStart()—— 在消息开始处理时运行回调。useAgentFinish()—— 在响应的每个即将结束点运行回调。useResponseStart()—— 观察响应的真正开始。useResponseFinish()—— 观察响应的真正结束。
渲染与 Hook 规则
运行时会在每次模型调用之前运行智能体函数 —— 即一次渲染:在每个 turn 开始时,以及一条消息加入正在进行的响应时。每次渲染都从一个全新的帧开始;Hook 按调用顺序记录到该帧上。
调用位置规则:
- Hook 只能在智能体函数渲染期间调用:在其函数体中同步调用,或在其调用的自定义 Hook 中调用。在其他任何地方调用 —— 工具的
run函数、事件 Hook 回调、模块作用域 —— 每个 Hook 都会抛出[flue] <hook>() was called outside an agent function. - 自定义 Hook 是一个普通函数;它调用的 Hook 记录的效果与智能体函数体直接调用完全相同。
- 渲染是纯读取操作。Hook 返回的写入函数(
usePersistentState的 setter、useDataWriter的 writer、useDispatchMessage的 dispatcher)在渲染期间调用时会抛出异常;应在工具的run函数和其他在智能体响应期间运行的回调中调用它们。
渲染之间什么可以变化,什么不可以:
- 条件性和可重排序 ——
useTool、useSkill、useSubagent(运行时将集合变化作为resources信号叙述给模型;参见动态资源);useMcpConnection(也是条件性的,粒度为提交级别 —— 添加或删除的声明在下一次提交时生效,以相同方式叙述);usePersistentState(存储按键名索引,不依赖声明顺序或存在性);四个事件 Hook(无持久身份 —— 每个接缝运行当前渲染声明的内容);useSandbox的存在性(翻转时在下一个 turn 边界交换环境,作为environment信号叙述)。 - 身份不变 ——
useDataWriter的名称必须在每次渲染中完全一致地声明;它们是响应面向客户端的身份。连续渲染之间的差异会抛出,并精确报告添加/删除的名称。 - 必需,恰好一次 ——
useModel。参数可以在渲染之间变化;但调用不能消失,从未调用它的渲染无法启动([flue] ... requires a model. Call useModel('provider-id/model-id') in the agent function.)。 - 在一次渲染中,重复的名称会在所有标识某物的地方抛出:工具名称、MCP 服务器名称、技能名称、子智能体名称、状态名称、数据片段名称。
useModel和useSandbox调用两次时会抛出。
值的作用域:
useModel的值(model、thinkingLevel、compaction)、useSandbox的 factory 和cwd,以及useMcpConnection的定义是提交作用域的:在提交开始时读取一次。后续渲染计算出的不同值将在下一次提交时生效,而非在运行中途生效。唯一的例外是useSandbox的存在性,它在每个 turn 边界重新读取。- 资源集合(工具、技能、子智能体)和指令文本是每次渲染作用域的:每次模型调用使用当前渲染声明的内容。
根渲染与子智能体渲染:
- 委托智能体的智能体函数在委托时在其自身的子智能体帧中渲染,每个任务都是全新的。
useTool、useSkill、useInstruction、嵌套的useSubagent和自定义 Hook 在那里照常组合。 - 实例作用域和面向客户端的 Hook 在子智能体渲染中会抛出:
useModel(委托智能体的模型来自其定义)、useSandbox(委托智能体共享父环境)、useMcpConnection、usePersistentState、useDataWriter、useDispatchMessage以及全部四个事件 Hook。useInitialData()返回undefined而非抛出;useDelivery()返回父智能体的任务提示。
四个事件 Hook —— useAgentStart、useAgentFinish、useResponseStart、useResponseFinish —— 在响应生命周期的固定接缝处运行回调,共享以下契约:
- 一个响应可能吸收多条已交付的消息(在 turn 边界加入的交付)。
useAgentStart对每条已交付消息运行一次;useAgentFinish在每个即将结束点运行;useResponseStart和useResponseFinish对每个响应运行一次,在其真正的开始和结束处。 - 声明没有持久身份:可以条件性声明、重排序、跨部署添加或删除 —— 每个接缝运行当前渲染声明的内容。渲染内的身份由声明顺序决定。
useAgentStart/useAgentFinish是被 await 的,可以是异步的,并接收一个 harness;useResponseStart/useResponseFinish是同步观察者 —— 返回的 promise 会导致提交失败。- 回调抛出会失败提交。
- 回调至少运行一次:它们的持久结果(信号追加、状态写入)按接缝原子提交,因此接缝中途崩溃不会留下任何持久内容,并在下次尝试时重新运行回调。持久效果不会重复;外部副作用(网络调用、文件)可能偶尔发生两次 —— 使它们幂等或用持久状态保护它们。
useModel()
function useModel(model: string, options?: UseModelOptions): void;
interface UseModelOptions {
thinkingLevel?: ThinkingLevel;
compaction?: false | CompactionConfig;
}
type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
声明智能体的模型。必需:没有 useModel 调用的智能体渲染无法启动。每次渲染中恰好调用一次,在智能体函数体或自定义 Hook 中 —— 在一次渲染中第二次调用会抛出,子智能体渲染也会抛出(委托智能体在 useSubagent 定义中声明其模型)。
model—— 模型标识符字符串,'provider-id/model-id'(例如'anthropic/claude-sonnet-4-6')。无法解析的标识符会在初始化时导致提交失败。参见模型了解目录,参见 Provider API 了解如何注册 provider。options.thinkingLevel—— 智能体级别的默认推理强度。单独的harness.prompt()调用可以覆盖它。未设置时,harness 替换为'medium'。未知值会抛出。options.compaction—— 阈值压缩配置(CompactionConfig,如下),或false以禁用阈值压缩。溢出恢复和显式的harness.compact()仍然会在需要时压缩。- 未知的选项字段会抛出。
- 值是提交作用域的:运行时在提交开始时读取它们,因此从状态计算的值将在下一次提交时生效,而非在运行中途生效。参见对话中途更换模型。
CompactionConfig
interface CompactionConfig {
reserveTokens?: number;
keepRecentTokens?: number;
model?: string;
}
useModel() 的 compaction 选项接受的阈值压缩配置。
reserveTokens—— 在上下文窗口中预留的 token 余量;当已用 token 超过contextWindow - reserveTokens时触发压缩。默认值为模型感知的值,上限为 20,000 个 token,对于输出限制较小的模型会缩减,当预留量占小上下文窗口的一半或更多时也会调整。正整数。keepRecentTokens—— 压缩后原样保留的近期 token 数量;较旧的消息会被折叠到摘要中。默认8000。较低的值会以近期上下文保真度为代价进行更激进的压缩。正整数。model—— 摘要调用的模型标识符覆盖,'provider-id/model-id'。默认为会话的模型。- 未知字段会抛出。
useSandbox()
function useSandbox(sandbox: SandboxFactory, options?: UseSandboxOptions): void;
interface UseSandboxOptions {
cwd?: string;
}
挂载此智能体实例运行的环境。工厂的 createSandbox() 为每个初始化的 harness 构建一次文件系统/执行表面(适配器基于实例 ID 索引持久资源),其 tools()(如果存在)会替换沙箱支持的面向模型的工具集。没有此 Hook 时,智能体没有环境:不会添加内置的文件和 shell 工具,沙箱支持的操作(harness.sandbox、工作区发现)也不会发生。SandboxFactory 契约在沙箱适配器 API 中有文档记录;第一方工厂包括基于 just-bash 实例的 bash() 和来自 @flue/runtime/node 的 local()。
sandbox—— 一个SandboxFactory值,直接传递(工厂本身已经是惰性的;昂贵的createSandbox()调用只在初始化时发生一次)。没有createSandbox函数(或已弃用的createSessionEnv)的值会抛出;非函数的tools属性会抛出。options.cwd—— 初始化环境中智能体的工作目录。非空字符串。在提交开始时读取一次。未知的选项字段会抛出。- 每次渲染最多调用一次;第二次调用会抛出。子智能体渲染会抛出 —— 委托智能体共享父环境(改用任务调用的
cwd来限定工作范围)。 - 重新渲染不会重建环境。
- 调用可以是条件性的。存在性在初始化和每个 turn 边界读取:当它翻转时,运行时在下一次模型调用之前交换环境 —— 挂载解析声明的工厂,卸载移除环境及其工具(不会保留任何内容)—— 并作为一个
environment信号宣布变化,重述完整的当前状态。 - 只有存在性可以在渲染之间观察到(工厂每次渲染都是新对象)。在保持挂载状态的同时将一个工厂替换为另一个工厂会在下一次提交的初始化时生效,而非在运行中途生效。
- 从持久状态派生的条件会持久重放:每次后续提交都会重新附加相同的声明,基于实例 ID 索引的适配器会解析回相同的持久工作区。参见条件性挂载。
useTool()
function useTool(tool: ToolDefinition): void;
为当前渲染挂载一个模型可调用的工具。接受一个 defineTool(...) 值或一个内联定义对象 —— 在挂载点应用相同的验证。无论在智能体函数体还是自定义 Hook 中调用,该工具都会加入渲染的单一扁平工具集。工具是什么 —— 完整的定义契约、ToolContext 以及 harness/durable 标志 —— 在 Agent API 的 defineTool() 中有文档记录。
- 挂载可以是条件性的;集合变化会叙述给模型(参见动态资源)。未挂载的工具根本无法被调用。
- 整个渲染中重复的工具名称会抛出
ToolNameConflictError。 - 无效的定义在挂载时抛出,消息与
defineTool()相同。
useMcpConnection()
function useMcpConnection(definition: McpConnectionDefinition): void;
声明一个远程 MCP 服务器,智能体使用其工具。接受一个 McpConnectionDefinition —— 通常是 defineMcpConnection(...) 导出的冻结对象,或相同形状的内联写法(相同的验证,应用于挂载点)。运行时在提交初始化时连接 —— 在请求上下文中,在所有目标上,所有声明的服务器并行连接 —— 并将服务器的工具以 mcp__<server>__<tool> 的形式挂载到渲染的扁平工具集中。使用模式参见 MCP 指南;定义形状和适配契约参见 Agent API。
- 定义在每次提交初始化时读取一次。条件性声明在下一次提交时生效,作为
resources信号叙述。 - 连接在实例的内存生命周期内被重用;定义在首次连接时读取(
auth除外 —— 每次请求解析)。连接失败会在模型运行之前导致提交失败,且永远不会被缓存 —— 除非定义设置了optional: true,这会为该提交挂载零个工具,并向模型宣布该缺失(参见McpConnectionDefinition)。 - 一次渲染中重复的服务器名称会抛出。子智能体渲染会抛出 —— 在根智能体上声明连接。
useSkill()
function useSkill(skill: Skill): void;
type Skill = SkillReference | SkillDefinition;
interface SkillReference {
readonly __flueSkillReference: true;
readonly id: string;
readonly name: string;
readonly description: string;
}
在智能体的目录中挂载一个技能。技能是渐进式披露:每个挂载的技能在系统提示词中占用一行始终存在的目录行(名称 + 描述),模型通过框架的 activate_skill 工具按需拉取完整指令 —— 简报作为工具结果到达,因此提示词前缀永远不会改变。支持文件在被显式读取之前保持惰性。
- 接受一个
SkillReference——SKILL.md导入的值(由构建自动打包;参见技能)或一个defineSkill(...)结果 —— 或一个内联的SkillDefinition对象,在挂载点验证。定义契约 —— 每个字段和约束 —— 在 Agent API 中有文档记录。 - 在一次渲染中挂载相同的技能名称会抛出。
- 挂载可以是条件性的;目录变化会被叙述(参见动态资源)。
- 始终在线的内容不需要技能:将 markdown 作为字符串导入(任何
.md导入都作为文本加载)并传递给useInstruction()。
useSubagent()
function useSubagent(subagent: SubagentDefinition): void;
声明一个模型可以通过框架的 task 工具委托聚焦工作的智能体。委托是一种声明的能力:task 工具始终在工具集中,具有完全静态的规范(任何更改都会重写序列化的工具块并使 provider 的提示词缓存失效),名册位于系统提示词的”可用智能体”部分,工具的必需 agent 参数只能解析已声明的子智能体 —— 名册为空时工具是惰性的。定义形状 —— SubagentDefinition、defineSubagent() 辅助函数,以及空白的通用 GeneralSubagent —— 在 Agent API 中有文档记录。
- 一次渲染中重复的委托名称会抛出。声明可以是条件性的;名册变化会被叙述(动态资源)。
- 委托智能体的
agent函数在委托时渲染,在其自身的帧中,每个任务都是全新的 —— 闭包读取当前值,对同一子智能体的两次委托独立渲染。 - 委托智能体与父智能体隔离:除了共享环境以及(除非在定义中覆盖)父智能体的模型和推理强度外,没有任何东西流入。它运行一个分离的会话,只有其最终文本返回给父智能体。参见子智能体和子智能体渲染 Hook 规则。
useInstruction()
function useInstruction(text: string): void;
为当前渲染追加原始指令文本 —— 刻意设计的低层逃生通道。文本在智能体返回的指令之后到达,按调用顺序排列,以空行连接;作者拥有所有格式化控制权。没有结构,没有身份,没有逐片段的变更追踪(组合后的文档作为整体进行摘要追踪;参见 instructions 信号)。
text—— 必需,去除空白后非空;其他情况会抛出。- 可在根渲染和子智能体渲染中调用,任意次数。
usePersistentState()
function usePersistentState<T>(name: string, defaultValue: T): [T, StateSetter<T>];
function usePersistentState<T = unknown>(name: string): [T | undefined, StateSetter<T | undefined>];
type StateSetter<T> = (value: T | ((previous: T) => T)) => void;
持久化的智能体状态:实例记录日志上的 API。Hook 读取本次渲染时的值并返回一个 setter 来持久化新值 —— 直接设置,或通过在调用时解析的更新函数。读取是渲染时的快照;写入是静默的 —— 它们永远不会发送消息、唤醒智能体或在运行中途重新渲染。下次渲染读取最新的持久化值。
- 值是 JSON:写入通过 JSON 往返规范化,对不可序列化的输入会抛出。设置
undefined会抛出 —— 没有取消设置;一旦写入名称,就始终有值。defaultValue在首次写入之前填充,本身永远不会被持久化。 - 更新函数形式(
set((previous) => next))是读取 - 修改 - 写入路径:previous在调用时通过尝试的写入缓冲区解析,而非闭包诞生时的渲染快照 —— 同一 turn 中两个使用更新函数的回调不会丢失彼此的写入。任何函数参数都被视为更新函数(函数从来不是合法值)。 - 写入与当前值深度相等的值是空操作;不会追加记录。
- 工具进行的写入与产生它们的工具批次原子地变为持久:如果批次已结算,写入就是持久的;如果恢复将批次结算为中断,写入就从未发生。参见持久性。
- setter 在渲染期间抛出(渲染是纯读取),在没有持久运行时支持的纯工具/测试渲染中也会抛出。
- 状态作用域为智能体实例,按键名
name索引。在一次渲染中声明相同的名称会抛出;跨渲染条件性地声明名称是合法的 —— 跳过声明的渲染就是未触及它的渲染,当声明返回时会再次读取记录的值。 - 子智能体渲染会抛出 —— 持久状态是实例作用域的,委托智能体运行分离的任务。通过任务提示词传递委托智能体所需的内容。
- 类型参数仅在编译时有效;运行时不会解析持久化值。如需强制执行,在调用点断言或在此之上组合一个模式检查自定义 Hook。
useInitialData()
function useInitialData<T = unknown>(): T;
读取实例的创建数据 —— 调用者在此实例首次接触时发送的 initialData,在创建时精确记录一次,在实例的整个生命周期内保持不变。不断变化的事实属于 usePersistentState;每条消息的事实属于 useDelivery。
类型参数仅在编译时有效;运行时对创建数据不执行解析或验证。当创建未携带数据时、在纯工具/测试渲染中、以及在子智能体渲染中(委托智能体没有自己的创建数据),运行时的值是 undefined。当这些情况适用时,在类型中说明:useInitialData<Config | undefined>()。
记录的值是实例持久记录流的一部分,但永远不会提供给客户端。它仍然不是密钥通道 —— 密钥和令牌保存在环境中。
useDelivery()
function useDelivery(): DeliveredMessage;
读取当前在模型面前的消息,作为每种传输都承认的相同已验证 DeliveredMessage 形状。该值是一个游标:它从唤醒响应的交付开始,每当新消息到达模型时前进 —— 在 turn 边界加入实时响应的交付,或事件 Hook 回调追加的信号。它使代码拥有与模型相同的访问权限,因此工具无需依赖模型将值回显到其输入中。
- Transport- and origin-agnostic: a direct HTTP prompt, a
dispatch()call, and an event hook’sappendproduce the same shapes here. - Framework narration signals (
resources/instructions/environment) do not advance the cursor — bookkeeping about the agent’s own declared surface never displaces the input the response is answering. - Constant within one render; fresh at the next. Renders happen before every model call, and when several messages collect into one response the cursor walks them in the order the model read them. A
useAgentStartcallback firing for a joined message reads that message. - Crash-safe: a resumed attempt derives the same cursor from the durable record stream the live attempt saw.
- In a subagent render, the delivery is the parent’s task prompt as a
kind: 'user'message (task images and documents ride asattachments). - Always present in the runtime: every response starts from a delivered message. A bare tooling/test render with no delivery behind it throws.
useDispatchMessage()
function useDispatchMessage(): (message: DeliveredMessageInput) => Promise<DispatchReceipt>;
获取绑定到此智能体实例的派发器 —— 顶层 dispatch() 的智能体作用域形式。返回的函数只接受消息:实例已经存在,因此没有 initialData 和没有 uid 条件。语义与全局动词通过构造相同 —— 相同的队列、相同的准入、相同的交付,与直接 HTTP 提示共享一个已接受的顺序。
- 两种消息类型都可以使用;纯字符串是
{ kind: 'user', body }的简写。 - 向忙碌的自身实例派发会在下一个 turn 边界加入实时响应 —— 持久准入,其自身的
useAgentStart运行,模型在下一个 turn 读取 —— 不会中断正在进行的 turn。向空闲实例派发会唤醒新的响应。错过实时响应的交付会从持久队列作为自己的提交运行;它永远不会丢失。 - 加入的交付随携带它的响应一起结算,具有相同的结果,在宿主响应的持久性预算下。加入的 HTTP 提示仍然会写入自己的
submission_settled记录,因此 SDK 的wait()解析结果与单独运行完全相同。 - 每次调用都是带有自己收据的持久交付。与重新尝试的工具中的任何外部副作用一样,重新运行会再次派发 —— 设计为至少一次。
- 派发器在渲染期间、在纯工具/测试渲染中、以及在配置运行时之前调用时会抛出。Hook 本身在子智能体渲染中会抛出 —— 委托智能体没有自己的实例;它将其产生的内容作为任务结果返回。
useDataWriter()
function useDataWriter<TSchema extends v.GenericSchema>(
name: string,
options: { schema: TSchema },
): (data: v.InferOutput<TSchema>) => void;
function useDataWriter(name: string): (data: unknown) => void;
声明一个命名的、面向客户端的数据片段,并返回一个只写的流式函数。输出是单向且非响应式的:模型永远看不到数据片段,写入永远不会重新运行智能体,也没有任何东西被读回。每次写入都被持久追加并立即流式传输到客户端,因此片段可以在工具运行中途显示实时进度。
name—— 片段在响应中的身份(AI SDK 约定:响应消息的片段中为data-<name>)。首次写入放置片段;后续写入原地更新。挂载时不发出任何内容;片段仅在首次写入后才存在。options.schema—— 验证每次写入的 Valibot 模式;写入器在不匹配时抛出。未知选项字段会抛出。- 值是 JSON:写入通过 JSON 往返规范化;
undefined和不可序列化的值会抛出。 - 写入器在渲染期间以及没有持久运行时支持的纯工具/测试渲染中会抛出。
- 名称在每次渲染中唯一且是渲染结构身份的一部分 —— 无条件地声明
useDataWriter调用,在每次渲染中完全一致;渲染之间的差异会抛出。声明它的自定义 Hook 继承该规则。Hook 在子智能体渲染中会抛出。 - 片段作为对话消息的数据片段到达传输线路上,也到达
AgentReply.data;参见向客户端流式传输数据。
useAgentStart()
function useAgentStart(run: (ctx: AgentStartContext) => void | Promise<void>): void;
interface AgentStartContext {
readonly append: (message: AgentAppendMessage) => void;
readonly harness: FlueHarness;
readonly log: FlueLogger;
readonly signal: AbortSignal;
}
在智能体开始处理已交付消息时运行回调 —— 在输入持久化之后,在模型的第一个 turn 之前。接入接缝:加载模型唤醒时应知道的内容、种子文件、写入持久状态,并通过派发信号来宣布。回调被 await 且可以是异步的;抛出会失败提交,执行至少一次,持久结果按接缝原子提交 —— 共享事件 Hook 契约。
- 对每条已交付消息运行一次,在模型读取它之前 —— 包括在运行中途加入实时响应的交付。非响应式:回调永远不会为已处理的消息重新运行。对于每实例一次的工作,用持久状态保护。
- 一条交付的回调并发运行,无保证顺序;模型等待最慢的。永远不要依赖兄弟回调的写入 —— 需要顺序的工作组合到一个回调中。追加的信号按声明顺序(无论完成顺序)分组到达对话。
- 所有输出都是显式的:通过
useDispatchMessage()派发器的面向模型信号(每个都是触发这些 Hook 本身的真实交付 —— 用持久状态保护)、通过状态 setter 的持久值、通过 harness 的文件。 ctx.append将信号写入此响应而不注册交付 —— 没有自己的useAgentStart运行,没有提交。它接受与useAgentFinish的append相同的AgentAppendMessage形状和验证,仅在回调的执行窗口期间合法;捕获的引用之后会抛出。优先使用派发;只有在交付不合适时才使用append。ctx.harness是 harness,首次访问时惰性实例化。ctx.signal是提交的中止信号。ctx.log将进度行发送到对话流中;模型永远看不到它们。- 压缩最终可能会折叠信号 —— 将回调的实质内容保存在持久状态和文件中;信号是公告,而非存储。
useAgentFinish()
function useAgentFinish(run: (ctx: AgentFinishContext) => void | Promise<void>): void;
interface AgentFinishContext {
readonly response: {
readonly toolCalls: readonly AgentResponseToolCall[];
readonly usage: PromptUsage;
};
readonly append: (message: AgentAppendMessage) => void;
readonly harness: FlueHarness;
readonly log: FlueLogger;
readonly signal: AbortSignal;
}
interface AgentResponseToolCall {
tool: string;
isError: boolean;
}
interface AgentAppendMessage {
kind: 'signal';
type: string;
body: string;
attributes?: Record<string, string>;
tagName?: string;
}
在智能体即将结束响应时运行回调 —— 模型不再有工具调用,响应即将结算。执行接缝:检查响应实际做了什么,如果工作未完成,append 一个信号将模型送回去在同一响应内继续工作。回调被 await 且可以是异步的,接收一个 harness,抛出会失败提交 —— 共享事件 Hook 契约。
- 这是一个控制接缝,而非被动观察点:回调在响应结算之前被 await。
ctx.append将信号引导到同一响应中 —— 再运行一个 turn,一旦该延续被处理,Hook 在下一个即将结束点再次运行。响应仅在一个循环以无 append 完成且没有已交付输入等待时才结算;排队的交付在任何结束评估之前加入,因此多条消息收集到多次useAgentStart运行和一次最终的useAgentFinish中。 append仅接受kind: 'signal'消息,具有与已交付信号相同的验证(非空type、字符串body、字符串到字符串的attributes、XML 名称tagName);kind: 'user'消息会抛出 —— 真正的新输入属于useDispatchMessage()。框架保留的信号类型也会抛出。仅在回调的执行窗口期间合法。- append 与派发的区别:append 是响应自我引导 —— 没有自己的
useAgentStart运行,没有自己的提交,计入延续上限。从此回调派发是真正的交付 —— 它加入同一响应,Hook 在新的真正结束点再次触发,有自己的useAgentStart运行,永远不计入上限。 - 仅在已交付的提交上运行,按声明顺序,顺序执行;多个 Hook 共享每个循环,如果其中任何一个 append 了,响应就会继续。
response.toolCalls聚合响应做出的每次工具调用 —— 跨所有 turn 和重新尝试,从持久记录派生。response.usage是到目前为止的聚合使用量(最终的总使用量属于useResponseFinish)。- 持久性:延续循环是响应控制检查点,与其信号原子记录 —— 恢复的响应驱动检查点的待处理延续而非重新评估,因此永远不会重新运行已完成的循环或两次 append。在检查点之前被中断的评估会在重新尝试时整体重新运行(至少一次)。
- 失控保护是每个响应 32 次延续循环的固定框架上限,不可配置:无条件 append 的 Hook 会大声失败提交而非以成功结算。提交的持久性超时仍然是总挂钟后备 —— 延续和加入都不会延长它。
useResponseStart()
function useResponseStart(run: ResponseMetadataCallback<ResponseStartContext>): void;
type ResponseMetadataCallback<TCtx> = (ctx: TCtx) => Record<string, unknown> | void;
interface ResponseStartContext {
readonly metadata: Record<string, unknown>;
readonly log: FlueLogger;
}
观察响应的真正开始 —— 每个响应一次,同步,在第一个模型调用之前且在任何 useAgentStart 回调之前。返回一个纯对象以深度合并到响应消息的元数据中(AI SDK 约定:消息的 metadata 字段 —— 客户端在内容流之外读取的信封事实)。不返回任何内容以观察而不附加。
- 每个响应一次:加入实时响应的交付会重新触发
useAgentStart,但响应只唤醒一次 —— 此 Hook 不会重新触发。已有持久助手步骤的恢复会跳过它;从第一个持久步骤之前的重新尝试会重新运行它(至少一次)。 - 同步观察者:无 append、无 dispatch、无 harness。返回的 promise 会导致提交失败;异步开始接缝的工作属于
useAgentStart。 ctx.metadata是本次响应到目前为止累积的元数据(先前 Hook 的贡献,按声明顺序),在调用时传入 —— 永远不是过时的渲染捕获。返回值通过深度合并组合;后出现的键优先,undefined值被跳过,原型污染键(__proto__、constructor、prototype)被丢弃。非对象、数组或 promise 返回会导致提交失败。- 快速失败:抛出会失败提交 —— 无重试、无恢复。
- 元数据对模型不可见且非响应式;运行时不标记自己的任何键。它在对话流上和
AgentReply.metadata上到达客户端。
useResponseFinish()
function useResponseFinish(run: ResponseMetadataCallback<ResponseFinishContext>): void;
interface ResponseFinishContext {
readonly metadata: Record<string, unknown>;
readonly response: {
readonly usage: PromptUsage;
readonly toolCalls: readonly AgentResponseToolCall[];
};
readonly log: FlueLogger;
}
观察响应的真正结束 —— 每个响应一次,同步,在最后一个 useAgentFinish 循环结算且每个排队的输出写入刷新之后。与 useResponseStart() 相同的返回契约、合并规则和失败语义。
- 在最终完成循环之后运行,当响应实际正在结算时;其
response.usage和response.toolCalls聚合是最终的。 ctx.metadata包含useResponseStartHook 附加的内容 —— 从持久记录日志读取,因此它在重新尝试中存活 —— 加上早期完成 Hook 的贡献。- 异步完成接缝的工作属于
useAgentFinish。
自定义 Hook
自定义 Hook 是一个普通函数,按照约定以 use 前缀命名,调用其他 Hook。没有注册,没有包装器:因为渲染帧是环境性的,其中调用的 Hook 记录的效果与智能体函数体直接调用完全相同,按相同的调用顺序。自定义 Hook 可以接受参数并向智能体函数体返回值,它们可以组合 —— 自定义 Hook 可以调用其他自定义 Hook。
function useRetention(active: () => boolean) {
useTool({
...offerCredit,
run: (ctx) => (active() ? offerCredit.run(ctx) : 'Refused: no churn risk on record.'),
});
useInstruction(
'Only while the customer is weighing cancellation: you may offer retention incentives.',
);
}
所有 Hook 规则通过自定义 Hook 不变地应用 —— 在渲染之外调用的自定义 Hook 在其第一个内部 Hook 调用时抛出,每次渲染的唯一性(一个 useModel、一个 useSandbox、唯一名称)计算通过任何深度的自定义 Hook 调用的 Hook。