Flue Agent Hooks API 完全指南:从渲染契约到 16 个内置 Hook 的实战解析
2026/9/16 12:27:17 网站建设 项目流程

Flue Agent Hooks API 完全指南:从渲染契约到 16 个内置 Hook 的实战解析

【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue

Flue(sandbox agent framework)把「Agent 函数」定义为一组在每次模型调用前执行的渲染(render),而useModeluseTooluseAgentStart等 16 个内置 Hook 就是在这个渲染期间声明能力、读取输入、挂载资源与感知生命周期的唯一入口。本文以仓库中的 Agent Hooks API 参考文档 为主体骨架,结合 packages/runtime/src/hooks 目录下的源码实现,系统讲解渲染契约、每个 Hook 的参数与运行时机、事件 Hook 的共享语义,以及如何用自定义 Hook 组合出可复用的 agent 逻辑,让你能独立编写结构正确、语义严谨的 Flue agent。

渲染(Render)与 Hook 规则:先理解「帧」

Flue 运行时会在每一次模型调用之前执行一次 agent 函数——这一过程被称为一次渲染(render)。无论是对话的每一轮,还是某个 delivery 加入一个进行中的 response 的瞬间,都会触发渲染。每一次渲染都从一个**全新的帧(frame)**开始,Hook 按调用顺序把声明记录到帧上;渲染结束后帧即被清空。

这个「帧」机制在源码中有非常清晰的实现:frame.ts 里用一个模块级变量currentFrame作为「当前渲染帧」的槽位,renderWithFrame在进入渲染前创建全新的RenderFrame,在finally中无论成败都会清空槽位;而requireRenderFrame在帧为空时抛出错误:

[flue] <hook>() was called outside an agent function.

这决定了 Hook 的调用点规则

  • Hook 只能在 agent 函数渲染期间同步调用——即 agent 函数体内,或它调用的自定义 Hook 中。
  • 在工具run函数、事件 Hook 回调、模块顶层等任何非渲染位置调用 Hook,都会抛出上面的错误。
  • 渲染是纯读操作。Hook 返回的写函数(usePersistentState的 setter、useDataWriter的 writer、useDispatchMessage的 dispatcher)在渲染期间调用会直接抛错;它们只应在工具run函数等 agent 响应期间的回调中调用。

渲染之间:什么可以变,什么必须不变

不同渲染之间,声明的可变性分为三类(这是整个 Hook 体系最容易踩坑的部分):

类别包含的 Hook规则
条件声明、可重排useTooluseSkilluseSubagentuseMcpConnectionusePersistentState、四个事件 Hook、useSandbox(仅「存在性」)集合变化会被运行时以resources/environment信号叙述给模型(见下文)
身份不变useDataWriter每个渲染必须用完全相同的名字无条件声明;连续渲染间的差异会抛出带有精确增删名单的错误
必须且恰好一次useModel参数可以随渲染变化,但调用绝不能消失;一次渲染内调用两次会抛错

此外,一次渲染内的重名在一切「以名字标识」的地方都会抛错:工具名、MCP 服务器名、技能名、子代理名、持久状态名、数据部件名;useModeluseSandbox调用两次也会抛错。

渲染结构的「身份不变」校验在 render.ts 的assertRenderStructureInvariance中实现:它只比较messageDataNames(数据部件名),一旦发现增删就抛出带精确差集的错误,而资源、状态、事件 Hook 与 sandbox 明确豁免。

值的作用域:submission 级 vs render 级

  • submission 级(提交级)useModel的取值(model、thinkingLevelcompaction)、useSandbox的 factory 与cwduseMcpConnection的定义,在一次 submission 开始时只读取一次。后面渲染计算出的新值只对下一次 submission生效,不会中途切换。唯一例外是useSandbox存在性——它在每个回合边界都会被重新读取。
  • render 级:资源集合(tools、skills、subagents)与指令文本,每次模型调用使用当前渲染声明的版本。

根渲染与子代理渲染

当模型通过task工具委托工作时,被委托的 agent 函数会在委托时刻以独立的subagent 帧渲染,每次任务都是全新的一次。在 subagent 帧内:

  • useTooluseSkilluseInstruction、嵌套useSubagent和自定义 Hook 照常工作。
  • 实例级、面向客户端的 Hook一律抛错useModel(委托者的模型来自useSubagent定义)、useSandbox(委托者共享父环境)、useMcpConnectionusePersistentStateuseDataWriteruseDispatchMessage,以及全部四个事件 Hook。
  • 例外:useInitialData()在 subagent 帧返回undefined(不抛错);useDelivery()返回父级的 task 提示词(作为kind: 'user'消息)。

帧的kind: 'agent' | 'subagent'字段在 frame.ts 中定义,各 Hook 实现里都能看到frame.kind === 'subagent'的分支抛错逻辑。

事件 Hook 的共享契约

四个事件 Hook(useAgentStartuseAgentFinishuseResponseStartuseResponseFinish)在 response 生命周期的固定接缝处运行回调,共享一套契约:

  • 一个response可能吸收多条已投递消息(在回合边界加入的 deliveries)。useAgentStart每条投递消息运行一次;useAgentFinish在每个「将要停止」点运行;useResponseStart/useResponseFinish每个 response 各运行一次,分别在真实开始与真实结束处。
  • 声明没有持久身份:可以条件声明、重排、跨部署增删——每个接缝运行「当前渲染声明」的内容。渲染内的身份就是声明顺序。
  • useAgentStart/useAgentFinish会被await,可以是异步的,并接收 harness;useResponseStart/useResponseFinish同步观察者——返回 Promise 会使 submission 失败。
  • 回调抛错会使 submission 失败。
  • 回调至少执行一次(at-least-once):它们的持久结果(信号追加、状态写入)在每个接缝原子提交,因此接缝中途崩溃不会留下任何持久痕迹,下次尝试会重跑全部回调。持久效果不会重复;但外部副作用(网络调用、文件写入)可能罕见地执行两次——请让它们幂等,或用持久状态做防护。

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:一次没有useModel的渲染无法启动(运行时会报[flue] ... requires a model. Call useModel('provider-id/model-id') in the agent function.)。每个渲染恰好调用一次,且只能调用一次——实现位于 use-model.ts,二次调用、subagent 帧调用、空字符串模型、未知选项字段都会抛错。

  • model— 模型标识字符串,格式为'provider-id/model-id'(如'anthropic/claude-sonnet-4-6')。无法解析的标识符会在 submission 初始化阶段直接失败。模型目录参见 Models 指南,注册 provider 参见 Provider API。
  • options.thinkingLevel— agent 级别的默认推理强度。单次harness.prompt()调用可以覆盖它。未设置时运行时默认替换为'medium'。未知取值抛错。
  • options.compaction— 阈值压缩配置(见下节CompactionConfig),传false可禁用阈值压缩;溢出恢复与显式harness.compact()仍然会在需要时压缩。
  • 未知选项字段抛错;值属于submission 级——从状态计算出的模型变更在下次 submission 生效,而不是当前运行中途。参考 Changing models mid-conversation,典型的升级模式是用持久状态在便宜的模型与强模型之间切换:
export function Reviewer() { const [escalated, setEscalated] = usePersistentState('escalated', false); useModel(escalated ? 'anthropic/claude-opus-4-6' : 'anthropic/claude-haiku-4-5'); // ... escalate_review 工具将 escalated 置为 true return 'Review the proposed change and leave actionable feedback.'; }

CompactionConfig:阈值压缩配置

interface CompactionConfig { reserveTokens?: number; keepRecentTokens?: number; model?: string; }
  • reserveTokens— 在上下文窗口中预留的 token 余量;当已用 token 超过contextWindow - reserveTokens时触发压缩。默认值是模型感知的,上限 20000 token,对输出上限较小的模型会缩小,且当余量会占用小上下文窗口一半以上时会调整。正整数。
  • keepRecentTokens— 压缩后原样保留的最近 token 数,更早的消息折叠进摘要。默认8000。更小的值压缩更激进,但会牺牲近期上下文的保真度。正整数。
  • model— 摘要调用使用的模型标识符覆盖,'provider-id/model-id'格式。默认使用会话的模型。
  • 未知字段抛错。

useSandbox():附加执行环境

function useSandbox(sandbox: SandboxFactory, options?: UseSandboxOptions): void; interface UseSandboxOptions { cwd?: string; }

useSandbox把 agent 实例的运行环境(文件系统/执行面)附加进来。factory 的createSandbox()在每个已初始化的 harness 上构建一次文件系统/执行面(adapter 以实例 id 为键持久化资源),其tools()(如果存在)会替换由 sandbox 支撑的模型面向工具集。没有调用该 Hook,agent 就没有环境:内置的文件与 shell 工具不会被添加,sandbox 支撑的操作(harness.sandbox、工作区发现)也不会发生。SandboxFactory契约见 Sandbox Adapter API;第一方 factory 包括基于 just-bash 实例的bash()和来自@flue/runtime/nodelocal()

源码层面的校验细节见 use-sandbox.ts:

  • sandbox— 直接传SandboxFactory值(factory 本身就是惰性的;昂贵的createSandbox()在初始化时只执行一次)。没有createSandbox函数(或已废弃的createSessionEnv)的值抛错;非函数的tools属性抛错。
  • options.cwd— agent 在已初始化环境内的工作目录。非空字符串。submission 开始时读取一次。未知选项字段抛错。
  • 每渲染至多一次,二次调用抛错;subagent 渲染抛错(委托者共享父环境,用 task 调用的cwd来限定工作范围)。
  • 重复渲染永远不会重建环境。
  • 调用可以是条件的。存在性在初始化和每个回合边界读取:翻转时运行时会在下一次模型调用前切换环境(attach 解析声明的 factory,detach 移除环境及其工具,什么都不带走),并以一条environment信号向模型宣告完整的当前状态(见 Dynamic resources)。
  • 只有存在性跨渲染可观察(factory 每次渲染都是新对象)。保持附加状态下把一个 factory 换成另一个,要到下次 submission 的初始化才生效。
  • 从持久状态推导的条件会持久重放:后续每次 submission 重新附加相同的声明,以实例 id 为键的 adapter 会解析回相同的持久工作区。参考 Conditional attachment 中的「调查模式」示例——工具把持久状态翻转为true后才挂载local({ cwd })

资源类 Hook:useTool()useMcpConnection()useSkill()useSubagent()

这四个 Hook 声明「模型可以使用什么」。它们的共同点是:可以条件声明,集合变化会被叙述为resources信号(见 Dynamic resources),且同名重复声明会在一个渲染内抛错。

useTool():挂载模型可调用工具

function useTool(tool: ToolDefinition): void;

接收defineTool(...)的值或内联定义对象——挂载点应用相同的校验(use-tool.ts 调用assertToolDefinition)。无论从 agent 体内还是自定义 Hook 中调用,工具都汇入该渲染的单一扁平工具集。工具的完整定义契约、ToolContextharness/durable标志见defineTool()

  • 挂载可以条件化;集合变化会叙述给模型。未挂载的工具完全无法被调用。
  • 整个渲染中重复的工具名抛ToolNameConflictError(render.ts 的assertUniqueToolNames负责执行)。
  • 无效定义在挂载时抛出与defineTool()相同的错误。

useMcpConnection():声明远程 MCP 服务器

function useMcpConnection(definition: McpConnectionDefinition): void;

声明 agent 使用的远程 MCP 服务器。接受McpConnectionDefinition——通常是defineMcpConnection(...)导出的冻结对象,或相同形状的内联对象(挂载点应用相同校验,字段白名单与传输类型校验见 use-mcp-connection.ts,支持streamable-httpsse两种传输)。运行时在submission 初始化时建立连接(在请求上下文中,所有目标上并行连接所有声明的服务器),并把服务器的工具以mcp__<server>__<tool>的命名挂进渲染的扁平工具集。定义形状与适配契约见 Agent API,用法见 MCP 指南。

  • 定义在每次 submission 初始化时读取一次。条件声明在下次 submission 生效,并叙述为resources信号。
  • 连接在实例的内存生命周期内复用;定义在首次连接时读取(auth除外——每个请求重新解析)。连接失败会在模型运行前使 submission 失败,且不会缓存——除非定义设置了optional: true,此时本次 submission 挂载零个工具并向模型宣告缺口。
  • 一个渲染内重复的服务器名抛错;subagent 渲染抛错(请在根 agent 上声明连接)。

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工具按需拉取完整指令——简报以工具结果形式到达,提示前缀永不改变。支持文件保持惰性,直到被显式读取(use-skill.ts 通过__flueSkillReference标记区分引用与内联定义)。

  • 接受SkillReference——SKILL.md导入的值(构建时自动打包,见 Skills 指南)或defineSkill(...)的结果——或内联SkillDefinition对象,挂载点校验。定义契约见 Agent API。
  • 一次渲染内挂载同名技能两次抛错。
  • 挂载可以条件化;目录变化会被叙述。
  • 始终在线的内容不需要技能:把 markdown 作为字符串导入(任何.md导入都以文本加载)并传给useInstruction()即可。

useSubagent():声明委托者

function useSubagent(subagent: SubagentDefinition): void;

声明模型可以通过框架task工具把专注工作交给的委托者。委托是声明出来的能力task工具始终在工具集中,且规范完全静态(任何一端变化都会重写序列化的工具块并使 provider 的提示缓存失效);名册位于系统提示的 "Available Agents" 部分;工具的必填agent参数只对已声明的子代理解析——名册为空时工具是惰性的。定义形状——SubagentDefinitiondefineSubagent()助手、以及空白的通用委托者GeneralSubagent——见 Agent API。

  • 一次渲染内重复的委托者名抛错。声明可以条件化;名册变化被叙述。
  • 委托者的agent函数在委托时刻渲染,拥有自己的帧,每次任务都是全新的——闭包读取当前值,两次委托给同一子代理会独立渲染(render.ts 的resolveSubagentDefinition负责此流程)。
  • 委托者与父级隔离:除共享环境以及(除非在定义中覆盖)父级的模型与推理强度外,什么都不流入。它运行一个分离的会话,只有最终文本返回父级。见 Subagents 指南 与本页的 subagent 渲染规则。

useInstruction():低级别的指令逃生舱

function useInstruction(text: string): void;

为当前渲染追加原始指令文本——刻意保留的低级别逃生舱。文本按调用顺序落在 agent 返回指令之后,以空行连接;格式完全由作者负责。没有结构、没有身份、没有逐片段的变更追踪(组合后的文档整体做摘要追踪,见instructions信号)。

  • text— 必填,去除首尾空白后必须非空,否则抛错(use-instruction.ts)。
  • 根渲染与 subagent 渲染都可调用,次数不限。

实际组合逻辑见 render.ts 的composeAgentDocument:agent 返回的指令(非空时)在前,useInstruction贡献按调用顺序在后,以'\n\n'连接。另外注意assertAgentInstruction强制 agent 函数必须是同步的——它只能返回指令字符串(或什么都不返回),异步工作要放进工具或资源 factory。

状态与输入类 Hook:usePersistentState()useInitialData()useDelivery()

这三个 Hook 构成 Flue 的三段式输入模型:useInitialData()是实例关于什么useDelivery()这条消息说什么,usePersistentState是 agent学到了什么

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——直接设置新值,或通过调用时解析的 updater 设置。读是渲染时快照;写是静默的——从不发布消息、从不唤醒 agent、从不中途重渲染。下次渲染读到最新持久值。完整语义见 use-persistent-state.ts:

  • 值是 JSON:写入经过 JSON 往返归一化,非可序列化输入抛错。设置undefined抛错——没有「取消设置」;一个名字一旦写入就永远有值。defaultValue在首次写入前填充,自身永不被持久化。
  • updater 形式是读-改-写路径previous调用时通过本次尝试的写缓冲解析,而非闭包诞生时的渲染快照——同一回合两个回调用 updater 组合不会互相丢写。任何函数参数都被当作 updater(函数从来不是合法值)。
  • 写入与当前值深相等时是 no-op,不追加记录。
  • 工具做出的写入与产生它们的工具批次原子持久:批次落定则写入持久;恢复把批次结算为中断则写入从未发生。见 Durability 指南。
  • setter 在渲染期间(渲染是纯读)以及背后没有持久运行时的裸工具/测试渲染中抛错。
  • 状态按name键定界在 agent 实例上。一次渲染内声明同名两次抛错;跨渲染条件声明合法——跳过声明的渲染就是没碰它的渲染,声明回归时重新读取已记录的值。
  • subagent 渲染抛错——持久状态是实例级的,委托者运行分离任务。把委托者需要的东西通过 task 提示词传过去。
  • 类型参数仅编译期有效;运行时不会解析持久值。需要强制时在调用点断言,或在其上组合一个做 schema 校验的自定义 Hook。

useInitialData():读取实例创建数据

function useInitialData<T = unknown>(): T;

读取实例的创建数据——调用者随本实例首次接触发送的initialData,创建时记录一次,实例整个生命周期恒定。演化的事实属于usePersistentState;逐消息的事实属于useDelivery(use-initial-data.ts)。

  • agent 上带有initialDataschema 静态时,值在创建时校验(不匹配会使创建发送失败,因此这里总是有值),Hook 返回 schema 解析后的输出。没有 schema 时,创建者发送什么就原样返回什么(未类型化)。
  • 发给既有实例的initialData被忽略;记录值无法改变。
  • 返回类型恰好是你断言的类型参数。运行时在创建未携带数据、裸工具/测试渲染、subagent 渲染(委托者没有自己的创建数据)时值undefined——遇到这些情况就在类型里说明:useInitialData<Config | undefined>()
  • 记录值是实例持久记录流的一部分,但从不提供给客户端。它仍然不是密钥通道——密钥和 token 留在环境中。

useDelivery():读取模型面前的消息

function useDelivery(): DeliveredMessage;

读取当前位于模型面前的消息,形状与每个传输接纳的已校验DeliveredMessage相同(use-delivery.ts)。该值是一个游标:从唤醒 response 的投递开始,每当新消息到达模型就前进——无论是回合边界加入进行中 response 的投递,还是事件 Hook 回调追加的信号。它让代码获得与模型相同的访问权,工具不再依赖模型把值回显进输入。

  • 传输与来源无关:直接 HTTP 提示、dispatch()调用、事件 Hook 的append在这里产生相同的形状。
  • 框架叙述信号(resources/instructions/environment)不会推进游标——关于 agent 自身声明面的记账永远不会挤掉 response 正在应答的输入。
  • 一个渲染内恒定,下个渲染刷新。渲染发生在每次模型调用前;多条消息汇集进一个 response 时,游标按模型读取它们的顺序行走。为加入的消息触发的useAgentStart回调读取的正是那条消息。
  • 崩溃安全:恢复的尝试从持久记录流推导出与实时尝试相同的游标。
  • subagent 渲染中,投递是父级的 task 提示词,为kind: 'user'消息(任务图片以attachments携带)。
  • 运行时总是存在:每个 response 都从一条已投递消息开始。背后没有投递的裸工具/测试渲染抛错。

useDispatchMessage():绑定到实例的调度器

function useDispatchMessage(): (message: DeliveredMessageInput) => Promise<DispatchReceipt>;

获取绑定到本 agent 实例的调度器——顶层dispatch()的 agent 作用域形式。返回的函数只需消息:实例已存在,因此没有initialData也没有uid条件。语义与全局动词同构(同一队列、同一接纳、同一投递,与直接 HTTP 提示共享一个被接纳的顺序)。实现见 use-dispatch-message.ts:dispatcher 从渲染帧的agentName/instanceId绑定自身,经由enqueueDispatch入队。

  • 两种消息类型都可用;裸字符串是{ kind: 'user', body }的简写。
  • 忙碌的本实例派发会在下个回合边界加入进行中的 response——持久接纳、运行自己的useAgentStart、模型在下回合读取——不打断进行中的回合。向空闲实例派发会唤醒新的 response。错过进行中 response 的投递会作为自己的 submission 从持久队列运行,永不丢失
  • 加入的投递随承载它的 response 一起结算,结果相同,在宿主 response 的持久性预算下。加入的 HTTP 提示仍会写自己的submission_settled记录,因此 SDK 的wait()解析得与单独运行完全一致。
  • 每次调用都是带自己回执的持久投递。像重试工具中的任何外部副作用一样,重跑会再次派发——请按 at-least-once 设计。
  • dispatcher 在渲染期间、裸工具/测试渲染上、以及运行时未配置时抛错。Hook 本身在 subagent 渲染抛错——委托者没有自己的实例;它把产出作为 task 结果返回即可。

useDataWriter():向客户端流式输出命名数据部件

function useDataWriter<TSchema extends v.GenericSchema>( name: string, options: { schema: TSchema }, ): (data: v.InferOutput<TSchema>) => void; function useDataWriter(name: string): (data: unknown) => void;

声明一个命名的、面向客户端的数据部件,并拿回一个流式写入它的只写函数。输出是单向、非响应式的:模型永远看不到数据部件,写入永不重跑 agent,也从不读回任何东西。每次写入被持久追加并立即流式传输给客户端,因此部件可以在工具运行中途展示实时进度。实现与校验见 use-data-writer.ts。

  • name— 部件在 response 内的身份(AI SDK 约定:response 消息部件中的data-<name>)。首次写入放置部件;后续写入就地更新。挂载本身不产生任何东西;部件在首次写入后才存在。
  • options.schema— 校验每次写入的 Valibot schema;不匹配时 writer 抛错。未知选项字段抛错。
  • 值是 JSON:写入经 JSON 往返归一化;undefined和不可序列化值抛错。
  • writer 在渲染期间和背后没有持久运行时的裸工具/测试渲染上抛错。
  • 名字每次渲染唯一,且是渲染结构身份的一部分——无条件声明useDataWriter,每个渲染完全相同;渲染间差异抛错。声明它的自定义 Hook 继承该规则。Hook 在 subagent 渲染抛错。
  • 部件作为对话消息的数据部件和AgentReply.data上线;端到端示例(含@flue/react客户端的part.type === 'data-orderCard'渲染分支)见 Streaming data to the client。

事件 Hook:useAgentStart()useAgentFinish()useResponseStart()useResponseFinish()

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; }

当 agent 开始处理一条已投递消息时运行回调——输入已持久、模型首回合之前。这是入口接缝:加载模型醒来该知道的内容、播种文件、写持久状态,并通过派发信号宣告。回调被 await,可以是异步的;抛错使 submission 失败,执行是 at-least-once 且持久结果在每个接缝原子提交(use-agent-start.ts 只是把回调压入frame.agentStarts;真正的执行在 message-output 会话层)。

  • 每条已投递消息运行一次,在模型读取它之前——包括中途加入进行中 response 的投递。非响应式:回调永不为已处理的消息重跑。一次性实例级工作用持久状态防护。
  • 一条投递的回调并发运行,顺序无保证;模型等待最慢的。绝不要依赖兄弟回调的写入——需要排序的工作组合进一个回调。追加的信号无论完成顺序如何,都按声明顺序分组到达对话。
  • 所有输出都是显式的:面向模型的信号走useDispatchMessage()dispatcher(每次都是真实投递,本身也会触发这些 Hook——用持久状态防护)、持久值走状态 setter、文件走 harness。
  • ctx.append把信号写进当前 response而不登记投递——没有自己的useAgentStart运行,没有 submission。它接受与useAgentFinishappend相同的AgentAppendMessage形状与校验,且只在回调执行窗口内合法;捕获的引用之后调用会抛错。优先派发;只有投递不对时才用append
  • ctx.harness是 harness,首次访问时惰性物化。ctx.signal是 submission 的中止信号。ctx.log把进度行发进对话流;模型永远看不到它们。
  • 压缩最终可能把信号折叠掉——把回调的实质内容留在持久状态和文件中;信号是宣告,不是存储。

典型「加载一次」模式(来自 Agent Hooks 指南):

export function AccountSupport({ id }: AgentProps) { useModel('anthropic/claude-haiku-4-5'); const [customer, setCustomer] = usePersistentState<Customer | null>('customer', null); useAgentStart(async () => { if (customer) return; // 每个对话只加载一次 setCustomer(await crm.lookupCustomer(id)); }); return customer ? `Help ${customer.name} (${customer.plan} plan) with their account.` : 'Help the customer with their account.'; }

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; }

当 agent 本来要结束响应时运行回调——模型不再有工具调用,response 即将结算。这是强制接缝:检查 response 实际做了什么,如果工作没做完,append一条信号把模型在同一 response 内送回去继续。回调被 await、可异步、接收 harness,抛错使 submission 失败(use-agent-finish.ts)。

  • 控制接缝,不是被动监听:回调在 response 结算前被 await。ctx.append把信号导入同一 response——再跑一个回合,续篇处理完后 Hook 在下个会停止点再次运行。只有当一个周期以无追加没有等待中的已投递输入完成时,response 才结算;排队中的投递在任何 finish 评估前加入,因此多条消息汇集为多次useAgentStart运行和一次最终useAgentFinish
  • append只接受kind: 'signal'消息,校验与已投递信号相同(非空type、字符串body、字符串到字符串的attributes、XML 名的tagName);kind: 'user'消息抛错——真正的新输入属于useDispatchMessage()。框架保留信号类型也抛错。它只在回调执行窗口内合法。
  • append vs dispatch:append 是 response 自我转向——没有useAgentStart运行、没有自己的 submission、计入续篇上限。本回调里的 dispatch 是真实投递——它加入同一 response,Hook 在新真实结束处再次触发,带自己的useAgentStart运行,永不计数进入上限。
  • 只在已投递 submission 上运行,按声明顺序串行;多个 Hook 共享每个周期,任一追加则 response 继续。
  • response.toolCalls聚合 response 已做出的每次工具调用——跨所有回合与重试,从持久记录推导。response.usage是迄今的聚合用量(结算总量属于useResponseFinish)。
  • 持久:续篇周期是 response 控制检查点,与其信号原子记录——恢复的 response 驱动检查点的待定续篇而非重新评估,因此绝不重跑已完成的周期或追加两次。检查点前被中断的评估在重试时整体重跑(at-least-once)。
  • 失控防护是固定的框架上限:每 response 32 个续篇周期,不可配置——无条件追加的 Hook 会响亮地使 submission 失败,而不是作为成功结算。submission 的持久性超时仍是总墙钟兜底——续篇和加入都不会延长它。

useResponseStart():观察 response 的真实开始

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; }

每个 response 一次、同步、在首次模型调用之前、在任何useAgentStart回调之前观察真实开始。返回一个普通对象以深合并到 response 消息的 metadata 上(AI SDK 约定:消息的metadata字段——客户端在内容流之外读取的信封事实)。什么都不返回则只观察不附加(use-response-start.ts)。

  • 每 response 一次:加入进行中 response 的投递会重新触发useAgentStart,但 response 只醒来一次——本 Hook 不重触发。已带持久 assistant 步骤的 response 的恢复会跳过它;从首个持久步骤之前开始的重试会重跑(at-least-once)。
  • 同步观察者:没有 append、没有 dispatch、没有 harness。返回 Promise 使 submission 失败;开始接缝的异步工作属于useAgentStart
  • ctx.metadata是本次 response 迄今积累的 metadata(更早 Hook 的贡献,按声明顺序),调用时传入——绝不是陈旧的渲染捕获。返回值深合并;后键胜出,undefined值跳过,原型污染键(__proto__constructorprototype)丢弃。非对象、数组或 Promise 返回使 submission 失败。
  • 快速失败:抛错使 submission 失败——不重试、不恢复。
  • metadata 模型不可见、非响应式;运行时不自盖任何键。它经对话流和AgentReply.metadata到达客户端。

useResponseFinish():观察 response 的真实结束

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; }

每个 response 一次、同步、在最后一个useAgentFinish周期结算每个排队输出写入都刷新之后观察真实结束。返回契约、合并规则与失败语义同useResponseStart(use-response-finish.ts)。

  • 在最终 finish 周期之后、response 真正结算时运行;它的response.usageresponse.toolCalls聚合是最终的。
  • ctx.metadata包含useResponseStartHook 附加的内容——从持久记录日志读取,因此跨重试存活——加上更早 finish Hook 的贡献。
  • 结束接缝的异步工作属于useAgentFinish

时间戳与用量统计的经典组合(两个观察者 + 深合并 metadata):

function useRunStats() { useResponseStart(() => ({ startedAt: Date.now() })); useResponseFinish(({ metadata, response }) => ({ finishedAt: Date.now(), elapsed: Date.now() - (metadata.startedAt as number), totalTokens: response.usage.totalTokens, toolCalls: response.toolCalls.length, })); }

自定义 Hook:组合的原子单位

自定义 Hook 就是一个普通函数,按惯例以use前缀命名,内部调用其他 Hook。没有注册、没有包装:因为渲染帧是环境性的,自定义 Hook 内部调用的 Hook 记录得与 agent 体直接调用完全相同、同一调用顺序(frame.ts 的注释把这个机制与 Preact 的currentComponent类比)。自定义 Hook 可以向 agent 体传参并返回值,也可以互相组合。

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 都从@flue/runtime导出,导出清单集中在 packages/runtime/src/index.ts:useAgentFinishuseAgentStartuseDataWriteruseDeliveryuseDispatchMessageuseInitialDatauseInstructiondefineMcpConnection/useMcpConnectionuseModel(含UseModelOptions类型)、usePersistentState(含StateSetter)、useResponseFinishuseResponseStartuseSandbox(含UseSandboxOptions)、useSkilldefineSubagent/GeneralSubagent/useSubagentuseTool。需要深入资源形状(defineTool/defineSkill/defineSubagent/defineMcpConnection)与 agent 周围的可编程面(dispatch()init()start()、路由、harness)时,继续阅读 Agent API;逐能力向导参见 Agent Hooks 指南、Tools、Skills、Subagents、Models、Sandboxes;Hook 与工具抛出的错误类见 Errors。

【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询