ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cloudflare Agents Agent Tools:用 runAgentTool 与 agentTool 把子 Agent 变成可重放、可下钻的工具

Cloudflare Agents Agent Tools:用 runAgentTool 与 agentTool 把子 Agent 变成可重放、可下钻的工具 Cloudflare Agents Agent Tools用 runAgentTool 与 agentTool 把子 Agent 变成可重放、可下钻的工具【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agentsAgent Tools 是 Cloudflare Agents 框架中让父 Agent 将可聊天的子 Agent作为工具运行起来的编排层。读完本文你能理解它的双表持久化模型父侧cf_agent_tool_runs注册表 子侧运行映射、runAgentTool/agentTool两个核心 API 的调用方式与幂等语义、agent-tool-event事件协议以及applyAgentToolEvent/useAgentToolEvents这套 headless React 前端如何从字节流重建子 Agent 的完整时间线。设计文档 design/agent-tools.md 描述的是已接受的 V1 方向完整 RFC 见 design/rfc-helper-sub-agent-orchestration.md而 packages/agents 中的源码是它当前的落地形态——本文以设计文档为骨架用仓库源码逐条印证。一、心智模型Agent 工具就是普通的聊天子 Agent从设计文档的表述看agent tool 不是一个新的类或基类而是一种编排角色任何一个能跑程序化聊天回合Think 或 AIChatAgent 子类的普通子 Agent被父 Agent 通过runAgentTool/agentTool调度后就成为 agent tool。RFC 给出的关系是Browser ──ws──▶ Parent chat agent │ ├─ normal parent chat response │ └─ agent-tool-event frames under parent tool calls │ ├─ Agent tool chat sub-agent A ├─ Agent tool chat sub-agent B └─ Agent tool chat sub-agent C浏览器始终只连着父 Agent。每个被调度的子 Agent 拥有自己的消息、工具、模型、可恢复流resumable stream和独立 SQLite底层是既有的subAgent(Cls, name)facet 原语见 packages/agents/src/index.ts父 Agent 只保存一张轻量运行注册表用于重放、排序与访问门禁。这一分层的关键收益是状态存在拥有它的 DO上。子 Agent 的完整聊天记录和流块由子 facet 自己持久化父侧只记录谁、为什么、按什么顺序发起了这次运行。二、持久化模型父子两张表如何配合设计文档明确了两侧的分工父侧cf_agent_tool_runs按runId记录每次逻辑运行的父工具调用 idparentToolCallId、子 Agent 类、安全输入预览inputPreview、展示顺序displayOrder、状态、摘要与终态错误元数据。子侧Think 子 Agent 用cf_agent_tool_child_runs把runId映射到底层 Think 请求 id 与流 idAIChatAgent 子 Agent 用cf_ai_chat_agent_tool_runs把runId映射到saveMessages()请求。runId是唯一的公开 id贯穿重放、下钻drill-in、取消、清理与日志而requestId聊天回合/abort 注册表 id与streamId可恢复流持久化 id留在子侧内部。三个 id 刻意分离避免把一次 agent tool 运行 一个聊天回合焊死在 schema 里。父侧表的实际建表语句在 packages/agents/src/index.ts与设计文档中的概念 schema 一致并带有idx_agent_tool_runs_parent_tool_call_id按父工具调用 id 展示顺序查询索引CREATE TABLE IF NOT EXISTS cf_agent_tool_runs ( run_id TEXT PRIMARY KEY, parent_tool_call_id TEXT, agent_type TEXT NOT NULL, input_preview TEXT, input_redacted INTEGER NOT NULL DEFAULT 1, status TEXT NOT NULL, summary TEXT, output_json TEXT, error_message TEXT, interrupted_reason TEXT, child_still_running INTEGER, display_metadata TEXT, display_order INTEGER NOT NULL DEFAULT 0, started_at INTEGER NOT NULL, completed_at INTEGER );从源码结构看当前实现相比设计文档还通过addColumnIfNotExists追加了interrupted_reason、child_still_running等列见 建表迁移用于把机器可读的中断原因持久化下来保证断线重连时客户端重放到的字段与实时客户端看到的一致。一个值得注意的安全细节注册表默认存的是输入预览而非原始输入以避免把编排表变成第二份 prompt/凭证存储。源码里默认预览逻辑是截断到 500 字符见_defaultAgentToolPreview调用方也可以用inputPreview选项显式控制。三、命令式 APIrunAgentToolrunAgentTool(Cls, options)是基础 API设计文档给出的调用形态如下const result await this.runAgentTool(Researcher, { input: { query: Compare HTTP/3 and gRPC }, parentToolCallId, displayOrder: 0, signal: abortSignal }); result.runId; result.summary; result.status; // completed | error | interrupted | aborted它是确定性多阶段工作流、callable/HTTP 触发的报告、以及需要Promise.allSettled做 fan-out/fan-in 的代码的首选接口。V1 不要求运行时 schemaTypeScript 泛型即可type ResearchInput { query: string }; type ResearchOutput { summary: string }; const result await this.runAgentTool typeof Researcher, ResearchInput, ResearchOutput (Researcher, { input: { query } });结果基线是summary?: string聊天类 Agent 天然产出助手文本结构化的output?: Output仅在子 Agent 有明确的结构化输出契约时出现。执行流程源码印证Agent.runAgentTool 实现 完整呈现了设计文档描述的时序生成/复用 runIdoptions.runId ?? nanoid(12)先查父侧行若该runId已有行——硬终态completed/error/aborted直接返回既有结果不重复执行非终态或软终态interrupted则尝试通过tailAgentToolRun重新附着到仍在运行的子 Agent 并 tail 到终态失败才回退到重放已存 chunk 标记 interrupted并发护栏_activeAgentToolRunCount() this.maxConcurrentAgentTools时快速失败并插入一行statuserror的注册表行使失败对 UI 与重放可见见 超限分支先插行、后唤醒子 Agent随后通过子 Agent 适配器启动回合、转发 chunk、记录终态。这个按runId幂等的设计让runAgentTool可以安全地从重试路径、alarm 或重连恢复中调用不会意外重复烧 LLM 调用。结果与失败信封设计文档 V1 约定error/reason字段为字符串当前源码已演进为更结构化的失败信封AgentToolFailureagent-tool-types.tsstatus镜像底层终态error/aborted/interruptedretryable仅对临时中断为true——子 Agent 因部署替换/父侧恢复而中断且未产生逻辑结果重新派发是正确动作真正的error或主动aborted均为falsereason是机器可读的中断原因枚举AgentToolInterruptedReasonno-progress、window-exceeded、not-tailable、inspect-timeout、inspect-failed、recovery-deadline、budget-exceeded免去调用方去解析人类可读的error文案childStillRunning区分父放弃等待时子 facet 仍在跑与子已被拆毁帮助调用方在重新派发与重连观察之间决策。失败信封的单测见 packages/agents/src/tests/agent-tools-failure.test.ts。四、模型选择式派发agentTool 工具工厂agentTool(Cls, options)是架在runAgentTool之上的 AI SDK 工具工厂用于由父 LLM 决定何时调度的常见场景实现见 packages/agents/src/agent-tools.tsgetTools() { return { research: agentTool(Researcher, { description: Research one topic in depth., displayName: Researcher, inputSchema: z.object({ query: z.string().min(3) }), outputSchema: z.object({ summary: z.string() }) }), plan: agentTool(Planner, { description: Write an implementation plan., inputSchema: z.object({ description: z.string().min(5) }) }) }; }工厂生成的工具在执行时做几件关键事情源码逐条可见稳定 runId 派生优先用agent-tool:${toolCallId}作为runId。工具调用 id 会被父侧聊天转录保留因此父回合在部署/驱逐后由聊天恢复重跑时同一agentTool()调用解析到同一runId转为重复请求并附着到仍在运行的子 Agent而不是新开一个子 Agent 把已完成的工作重跑一遍见 runId 派生注释把toolCallId作为parentToolCallId传给runAgentTool子 Agent 事件即挂到该工具调用名下转发给浏览器把abortSignal透传父侧取消 → 按runId显式取消子 Agent 回合结果路由completed且设置了outputSchema时做运行时校验校验失败返回失败信封无outputSchema时返回文本summary非 completed 一律返回结构化失败信封保证 LLM 永远不会看到非 completed 的静默空结果从而避免父模型幻觉出一个并不存在的成功摘要。注意agentTool只能在 Agent 回合内运行它通过AsyncLocalStorage上下文拿到当前父 AgentcurrentAgentToolRunner在回合外调用会抛出明确错误提示。另外 V1 规定agentTool(...)的inputSchema必填父 LLM 需要运行时 schema 做工具选择与校验outputSchema可选框架不会从散文里自动抽取结构化输出——需要结构化结果的场景应把它写进子 Agent 自身的 prompt/工具契约。五、agent-tool-event 事件协议父 Agent 把子 Agent 的UIMessageChunk转发给自己的客户端帧结构由 AgentToolEventMessage / AgentToolEvent 定义与设计文档一致type AgentToolEvent | { kind: started; runId: string; agentType: string; inputPreview?: unknown; order: number; display?: { name?: string; icon?: string } } | { kind: chunk; runId: string; body: string } // body 为 JSON 编码的 UIMessageChunk | { kind: finished; runId: string; summary: string } | { kind: error; runId: string; error: string } | { kind: aborted; runId: string; reason?: string } | { kind: interrupted; runId: string; error: string; reason?: AgentToolInterruptedReason; childStillRunning?: boolean }; type AgentToolEventMessage { type: agent-tool-event; parentToolCallId?: string; sequence: number; replay?: true; event: AgentToolEvent; };协议有几个刻意的设计点chunk.body是不透明的 JSONUIMessageChunk。框架不发明第二套文本/推理/工具调用的词汇表客户端用与聊天响应相同的applyChunkToParts原语就能重建子 Agent 的消息 partssequence按 run 单调递增客户端去重键必须是(parentToolCallId, runId, sequence)——因为一个父工具调用下并行的多个子 Agent 都合法地从 0 开始命令式运行无parentToolCallId用(null, runId, sequence)终态事件彼此独立error子 Agent/工具失败、aborted显式取消、interrupted父侧恢复受限或观察者丢失UI 可据此差异化渲染。传输层面设计文档说明 V1 中跨 Durable Object RPC 的子 Agent 实时 chunk 以字节编码的换行分隔记录传递父侧解码后广播agent-tool-event帧取消不跨 RPC 序列化AbortSignal而是通过父侧取消回调桥接到子 Agent。六、状态生命周期interrupted 是父侧专属状态RFC 中的状态表在源码中被原样保留为AgentToolRunStatus联合类型agent-tool-types.ts状态是否终态可观测位置含义starting否父注册表、子适配器行已插入子 Agent 尚未确认启动running否父注册表、子适配器子 Agent 已开始聊天回合completed是全层 runAgentTool子 Agent 到达正常终态error是全层 runAgentTool子 Agent 抛错、流错误或结果合成失败aborted是全层 runAgentTool被父侧 abort 信号显式取消interrupted是父注册表、观察者、runAgentTool父侧对无法安全恢复的非终态运行做了对账两条不变式很重要interrupted是父侧专属子 Agent 不会自我声明中断只有丢失观察者、无法 live-tail 的父 Agent 才能记录该状态终态权威且不可回写晚到的 cancel 不得把completed/error改写成aborted晚到的对账不得把aborted改写成interrupted。源码里取消与清理路径都以status IN (starting,running)作为前置条件如_activeAgentToolRunCount与clearAgentToolRuns与该不变式一致。设计文档还明确了一个边界V1 的持久化运行状态只描述执行不描述观察——观察者掉线浏览器断连、父重启、重放失败只应解除观察不构成取消执行的请求只有父侧活跃操作的显式 abort 才取消运行。七、恢复与重放父重启时会发生什么设计文档对父 Agent 在运行非终态时重启的处理是 V1 的核心诚实性保证重放已存 chunk 并把父侧行标记为interruptedlive-tail 重附着被推迟。完整的恢复决策树来自 RFC Recovery semantics重启后对每个starting/running行做对账无对应子 Agent 或子运行 →interrupted默认不盲目重跑任意 agent 工作子 Agent 报completed→ 重放已存 chunk、读取最终输出、父行标completed子 Agent 报error/aborted→ 重放 chunk、父行标对应终态子 Agent 报running→ V1 重放已存 chunk 并标interrupted附当前运行时不支持 live-tail 重附着的精确错误信息。子侧映射表是恢复的权威来源即使父在子已启动但父尚未记下requestId/streamId之间崩溃父也能凭runId向子查询恢复映射。当前源码已在该边界上向前演进runAgentTool的既有行处理路径源码会优先尝试tailAgentToolRun重附着到终态重附着成功即修复父行并返回真实结果只有在没有 tail 适配器或超出有界预算agentToolReattachNoProgressTimeoutMs/agentToolReattachMaxWindowMs时才回退到重放 interrupted并且父放弃重附着后会拆毁子 facet避免其继续空耗 fiber/keep-alive。也就是说设计文档的推迟项在当前实现中已有条件地兑现且以机器可读的reason枚举暴露给调用方。最难的边界仍是父聊天本身的恢复如果agentTool(...)调用是父 LLM 回合的一部分仅恢复子运行不足以续跑父回合除非父聊天恢复机制也能从工具结果处继续在那之前命令式runAgentTool(...)的恢复故事更干净因为应用代码可以事后检查运行而无需重建进行中的 LLM 回合。八、headless React 层applyAgentToolEvent 与 useAgentToolEvents设计文档有意让 React 面保持 headlessapplyAgentToolEvent是不依赖 UI 的纯 reducer从opaque chunk body 重建子 Agent 的UIMessage.parts并按父工具调用 id 分组运行useAgentToolEvents订阅既有的父连接useAgent处理重放/实时的竞态去重。布局、面板与下钻 UI 由应用自己拥有。分层对应仓库中的三个位置协议类型 纯 reducerpackages/agents/src/chat/agent-tools.tsapplyAgentToolEvent钩子packages/agents/src/react.tsx 中的useAgentToolEvents测试packages/agents/src/chat/tests/agent-tools.test.tsreducer 单测含重放/实时竞态去重、packages/agents/src/react-tests/useAgentToolEvents.test.tsx 与 agent-tool-replay.test.tsx浏览器端钩子与重放行为。reducer 维护的状态形状AgentToolEventState见 agent-tool-types.tstype AgentToolEventState { runsById: Recordstring, AgentToolRunState; runsByToolCallId: Recordstring, AgentToolRunState[]; unboundRuns: AgentToolRunState[]; // 无 parentToolCallId 的命令式运行 };钩子负责过滤agent-tool-event消息、按(parentToolCallId, runId, sequence)去重重放/实时竞态、用applyChunkToParts应用 JSON chunk、按parentToolCallId分组、按order排序兄弟运行、把协议状态映射为running | completed | error | aborted | interrupted并暴露subAgent: { agent: agentType, name: runId }供下钻接线。它不负责面板 UI、渲染选择、自动打开下钻连接或服务端清理策略。与useAgentChat的预期集成方式RFC 中的草图const agent useAgent({ agent: Assistant, name: userId }); const { messages } useAgentChat({ agent }); const agentTools useAgentToolEvents({ agent }); return messages.map((message) ( Message message{message} {message.parts.map((part) part.type tool-call ? ( ToolCallPart part{part} runs{agentTools.getRunsForToolCall(part.toolCallId)} / ) : ( NormalPart part{part} / ) )} /Message ));只做命令式运行的应用直接渲染unboundRuns无需触碰useAgentChat。agents 包 README 也把useAgentToolEvents({ agent })列为渲染保留与重放子时间线的标准入口。九、Think 完成语义执行、观察与结果合成三分离设计文档中一段常被忽略但很关键的约定Think 子 Agent 的完成不绑定于助手文本。助手文本只是聊天型 helper 的默认摘要来源工作流型 Think 子 Agent 可以没有任何文本 chunk 就完成并通过getAgentToolOutput()暴露持久化结构化输出可选覆写getAgentToolSummary()。这使三个关注点分离回合结束决定终态、子 chunk 保留供 UI 观察、output/summary 钩子决定父侧收到什么。由于 output 钩子在子回合解析后立即求值工作流型子 Agent 应在回合结束前提交持久化输出并让摘要保持可展示的尺寸。十、权衡、清理 API 与访问控制设计文档的 Tradeoffs 一节应原样带入实践判断运行与 facet 默认保留刷新/下钻/调试在完成后仍可用应用清理聊天历史或实施保留策略时必须调用clearAgentToolRuns()父注册表只存输入预览避免产生第二份 prompt 存储AIChatAgent 的 agent-tool 回合是 headless 的服务端工具正常工作但浏览器提供的客户端工具不可用除非应用把交互建模为服务端状态或父中介的工作流。清理 API 的源码实现见clearAgentToolRuns语义与设计文档一致await this.clearAgentToolRuns(); await this.clearAgentToolRuns({ olderThan: Date.now() - 7 * DAY }); await this.clearAgentToolRuns({ status: [completed, error, aborted, interrupted] });实现上它先按started_at取出待删行对starting/running的运行先取消再删除跳过取消步骤会留下无观察者、结果无处呈现的孤儿 LLM 工作随后删除子 facet 与父注册表行清理全程幂等。前端侧的配套是resetLocalState()先服务端删运行、再清本地状态、最后清聊天历史。下钻drill-in通过既有的子 Agent 路由寻址runId本身不是能力凭证——下钻 URL 必须经由父 Agent 的既有身份进入并且onBeforeSubAgent门禁应确保只有父侧存在对应cf_agent_tool_runs行的(agentType, runId)才能触达子 facet防止 URL 猜测凭空孵化 facet。并发与成本方面父级属性maxConcurrentAgentTools默认Infinity见 源码限制非终态运行数超限快速失败onAgentToolStart/onAgentToolFinish生命周期钩子为日志、计量与审计提供接入点runId是把父注册表、子转录、日志与 trace 关联起来的 join key。十一、V1 边界与延伸阅读按设计文档与 RFC 的 Non-goalsV1 明确不做替代 Workflowsagent tool 是聊天形——可流式、带转录、可下钻、MCP 桥接、多回合 agent tool一次runId对一个子回合schema 已为cf_agent_tool_child_turns留了演化空间、对失败/中断运行的自动重试恢复是只读的、跨租户/跨账号调度、以及带样式的 UI 组件headless reducer/hook 是唯一 React 面。V1 支持 Think 子 Agent 与 AIChatAgent 子 Agent混合 Think/AIChatAgent 配对是设计目标而非第一阶段要求。实现阶段的建议顺序来自 RFC Suggested phasingThink 服务端面 → headless React 面 → AIChatAgent 对等 → 人体工学如defineAgentTool→ live-tail 重附着与detached观察者状态。经验证的端到端原型在 examples/agents-as-tools其 e2e 用例覆盖了刷新重放、下钻门禁与清理等场景如 refresh-replay.e2e.ts、planner-drill-in.e2e.ts、clear.e2e.ts可作为行为基线参考后续 RFC如 design/rfc-detached-agent-tools.md在此基础上扩展了 detached后台运行、进度与里程碑信号等能力本文以 V1 设计文档所述范围为准。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表