Agent 工具调用与 TodoWrite 任务桥接:从 SDK tool_use 到前端任务列表的完整链路
人工智能AI 应用AI Agent交互助手MCP Clients本地部署【免费下载链接】CodePilotA multi-model AI agent desktop client — connect any AI provider, extend with MCP skills, control from your phone. Built with Electron Next.js.项目地址https://gitcode.com/gh_mirrors/co0dep/CodePilot点击查看免费下载导读本文以 CodePilotElectron Next.js 多模型 AI 桌面客户端中 SDK 代理工具Agent Tooling与本地任务系统TodoWrite之间的桥接机制为核心完整拆解TodoWrite工具从 SDK 发出、经服务端 SSE 采集、落库到前端任务列表刷新的全链路深入讲解字段映射、状态归一化、幂等同步与 tool_result 双层去重策略并给出可复现的故障排查路径。读完本文你将掌握这套“agent 自主规划 → 本地任务持久化 → UI 实时反馈”的桥接设计与实现细节。一、背景为什么需要一条“TodoWrite 桥”CodePilot 通过anthropic-ai/claude-agent-sdk运行代理Agent会话。代理在执行复杂任务时会调用内置的TodoWrite工具以结构化清单的形式输出其任务规划与执行进度如pending/in_progress/completed。这些任务清单代表“代理接下来打算做什么、已经做到哪一步”对用户而言是重要的过程透明度。然而 SDK 的任务数据只存在于流式会话内部若直接丢弃用户刷新页面或切换会话后将无法回溯代理的规划也无法与用户手动创建的任务如 TaskList.tsx 中手动勾选的任务统一管理。因此 CodePilot 在服务端建立了一条桥接链路把 SDK 发出的 TodoWrite 载荷转换为本地tasks表中的持久化记录并通过 SSE 事件与前端事件总线驱动 UI 实时刷新。该桥接的完整架构如下SDK TodoWrite tool_use → PostToolUse hook (claude-client.ts) → Emits tool_result SSE (deduped) → Emits task_update SSE { session_id, todos[] } → collectStreamResponse (route.ts) → syncSdkTasks() persists to DB → Frontend SSE consumer (useSSEStream.ts) → onTaskUpdate dispatches tasks-updated event → TaskList re-fetches from /api/tasks链路的关键设计目标有三个持久化代理的 TodoWrite 任务在刷新后依然可见去重同一tool_use_id的tool_result可能来自两个来源必须“最后写入者胜出”而非重复追加幂等同一份 TodoWrite 载荷重复同步多次数据库结果保持一致。二、TodoWrite 字段映射SDK 输入 → 本地任务表SDK 的TodoWriteInput与本地tasks表之间并非一一对应而是经过一层明确的映射。映射关系定义在 db.ts 的syncSdkTasks()中SDK TodoWriteInput 字段本地 tasks 表列说明数组下标id前缀sdk-{sessionId}-{index}使用完整 sessionId 避免跨会话碰撞contenttitle主展示文本statusstatus经mapStatus()归一化activeFormdescription进行中状态present-continuous的标签数组下标sort_order保留原始顺序本地tasks表的结构在 db.ts 与迁移逻辑 db.ts 中定义包含CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT pending CHECK(status IN (pending, in_progress, completed, failed)), description TEXT, created_at TEXT NOT NULL DEFAULT (datetime(now)), updated_at TEXT NOT NULL DEFAULT (datetime(now)), FOREIGN KEY (session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE );后续迁移补充了两个关键列source TEXT NOT NULL DEFAULT user区分任务来源user用户手动创建 /sdk代理同步sort_order INTEGER NOT NULL DEFAULT 0保持 SDK 数组中的原始顺序。状态映射Status MappingSDK 侧的状态值域与本地TaskStatus并非完全一致syncSdkTasks()内部的mapStatus()负责归一化SDK 状态本地 TaskStatuspendingpendingin_progressin_progresscompletedcompleted其他pending对应实现见 db.tscompleted、in_progress、pending三个已知状态原样映射其余未知状态一律回退为pending。这种“宽容入参、严格出参”的策略保证了本地表CHECK约束仅允许四种状态永远不会因 SDK 引入新状态值而被破坏。三、同步策略事务内 replace-all 幂等写入syncSdkTasks()采用**事务内 replace-all整批替换**策略见 db.tsDELETE FROM tasks WHERE session_id ? AND source sdk先删除该会话下所有 SDK 来源的任务将最新 TodoWrite 载荷中的全部 todos 逐条INSERT。由于每次同步都基于同一份载荷做“先清空、再全量插入”该策略天然幂等——用相同数据多次调用会产生完全相同的结果无需额外的版本号或去重键。同时source user的用户手动任务永远不会被波及用户勾选、改名、新建的任务在代理每次 TodoWrite 时保持稳定不会被代理的规划覆盖。两个实现细节值得注意排序稳定插入时使用数组下标i作为sort_order即使同一份列表在多次task_update中顺序调整sort_order也会随之更新前端列表顺序与 SDK 内数组顺序始终一致主键自足id由sdk-{sessionId}-{index}拼接而成天然包含会话维度与序号维度多会话并行时不会互相污染。任务同步还受会话锁lockId所有权的保护在 chat-collect-stream-response.ts 中task_update事件到达后若当前 turn 已不是锁的所有者例如 Stop 后新消息接管了会话则跳过syncSdkTasks()并打印stale owner (lockId superseded)警告避免被取代的旧 turn 覆盖新 turn 的任务列表。四、tool_result 双层 Last-Wins 去重SDK 的PostToolUsehook 与 user 消息处理器可能各自发出同一份tool_resultPostToolUse先触发用于即时 UI 反馈user 处理器的结果可能更完整、更权威canonical。CodePilot 的处理策略是两个来源都自由发出消费层采用“last wins”后者替换、而非跳过从而始终保留最完整的结果。整个去重分布在两层Layer 1route.ts / collectStreamResponseDB 持久化层在 chat-collect-stream-response.ts 维护seenToolResultIds: Setstring当重复的tool_use_id到达时见 chat-collect-stream-response.ts不是跳过而是在contentBlocks中找到既有tool_result条目并用新结果替换它if (seenToolResultIds.has(resultData.tool_use_id)) { const idx contentBlocks.findIndex( (b) b.type tool_result tool_use_id in b b.tool_use_id resultData.tool_use_id ); if (idx 0) { contentBlocks[idx] newBlock; // 替换而非追加 } } else { seenToolResultIds.add(resultData.tool_use_id); contentBlocks.push(newBlock); }替换逻辑还会顺带处理media保存到媒体库并替换为本地路径与sources外部引用确保最终落库的 transcript 中每个tool_use_id只出现一次且内容是后到、更完整的那份。Layer 2ChatView.tsxUI 状态层前端ChatView的onToolResult回调按tool_use_id查找既有条目并替换而非追加重复项。两条消费路径汇聚到 useSSEStream.ts 统一解析使 DB 与 UI 对同一份流的处理语义保持一致相关行为有单元测试覆盖如 sse-stream.test.ts 与 codex-tool-result-media.test.ts。五、TodoWrite 触发的时机成功执行后才同步task_update并非在tool_use出现时立即发出而是延迟到该 tool 成功执行之后。在 claude-client.ts 中SDK 流被解析时遇到TodoWrite的tool_use块会把其todos输入暂存进pendingTodoWritesMap以tool_use_id为键const pendingTodoWrites new Mapstring, Array{ content: string; status: string; activeForm?: string }(); // ... if (block.name TodoWrite) { const toolInput block.input as { todos?: Array{ content: string; status: string; activeForm?: string } }; if (toolInput?.todos Array.isArray(toolInput.todos)) { pendingTodoWrites.set(block.id, toolInput.todos); } }只有当对应的tool_result到达且!block.is_error即 TodoWrite 真正成功落地时才发出task_update见 claude-client.tsif (!block.is_error pendingTodoWrites.has(block.tool_use_id)) { const todos pendingTodoWrites.get(block.tool_use_id)!; pendingTodoWrites.delete(block.tool_use_id); controller.enqueue(formatSSE({ type: task_update, data: JSON.stringify({ session_id: sessionId, todos: todos.map((t, i) ({ id: String(i), content: t.content, status: t.status, activeForm: t.activeForm || , })), }), })); }注意这里把数组下标String(i)作为每个 todo 的id传入——这正是前文sdk-{sessionId}-{index}主键中index的来源。失败is_error的 TodoWrite 不会触发同步避免把代理“想做但没做成”的规划写进任务表。六、SSE 事件类型全景桥接链路处于 CodePilot 的流式会话 SSE 生态中理解task_update需要把它放进完整的事件谱系里。以下事件均在服务端由 SDK 流映射而来被 collectStreamResponse 消费Event来源用途textstream_event流式文本增量tool_useassistant message工具调用tool_resultPostToolUse / user工具执行结果已去重tool_outputstderr callback原始工具输出tool_timeouttool_progress工具执行超时task_updatePostToolUseTodoWrite 同步触发statussystem message会话初始化、通知resultresult message最终结果含用量统计permission_requestcanUseTool需要权限审批mode_changedsystem statusSDK 模式切换errorcatch block发生错误donestream end流结束tool_result之外status写入sdk_session_id、result用量归一化与sdk_session_id更新等事件在落库时同样受会话锁所有权门控与task_update共享同一套“stale owner 不写库”的防御语义。七、前端刷新链路从 task_update 到 TaskListtask_update到达服务端被落库后还需要驱动前端界面。前端 SSE 消费在 useSSEStream.ts 中分发case task_update: { // ... callbacks.onTaskUpdate(taskData.session_id); // skip malformed task_update data }onTaskUpdate回调随后派发浏览器级tasks-updatedCustomEvent事件由 ChatView.tsx 附近触发。监听方包括TaskList.tsx收到事件后调用fetchTasks()从/api/tasks重新拉取任务列表TaskCheckpoint.tsx同样监听tasks-updated以刷新检查点视图。用户在 TaskList.tsx 中手动勾选任务时通过PATCH /api/tasks/{id}更新status该写路径与 SDK 同步写路径互不冲突——前者更新的是sourceuser或已存在的 SDK 任务行后者在每次 TodoWrite 时整批重建sourcesdk的行。八、故障排查指南1. TodoWrite 后任务未出现按链路逐段排查查看服务端控制台是否有[claude-client]相关日志确认PostToolUse/TodoWrite 解析确实触发在浏览器 DevTools 的 Network 面板确认 SSE 中出现了task_update事件可对比[db] syncSdkTasks:日志确认落库执行确认syncSdkTasks()在服务端 tee 流collectStreamResponse中被调用——注意若看到stale owner (lockId superseded)警告说明该 turn 已被新 turn 接管属于预期跳过而非故障在浏览器控制台确认tasks-updatedCustomEvent 确实触发随后TaskList应重新请求/api/tasks。2. 出现重复 tool_result查看服务端日志中seenToolResultIdsSet 的大小变化对比去重前后的 SSE 事件计数——对工具类事件而言数量应大致减半直接查询消息表验证每个tool_use_id只出现一次SELECT content FROM messages WHERE content LIKE %tool_result%;每个tool_use_id在 transcript 中应恰好出现一次若出现多次说明替换逻辑未生效例如旧版本未升级或事件在替换前已落库。3. SDK 升级相关问题当前版本v0.2.62自 v0.2.33 升级而来回滚命令npm install anthropic-ai/claude-agent-sdk0.2.33关键集成点canUseTool、PostToolUse、Notification、tool_progress、system、result——升级 SDK 后应优先回归这些 hook 与事件类型的解析逻辑对应 claude-client.ts 的流解析与 chat-collect-stream-response.ts 的消费逻辑。九、小结与延伸阅读Agent Tooling TodoWrite 桥接的核心设计可以概括为四个关键词映射归一SDK 输入字段经mapStatus()与主键前缀策略安全落库不破坏本地表约束整批幂等事务内 replace-all 让重复同步收敛到同一结果且永不触碰sourceuser任务延迟同步task_update只在 TodoWrite 成功执行后发出失败规划不会污染任务表双层去重DB 层seenToolResultIds替换与 UI 层onToolResult替换共同实现 last-wins保证 transcript 与界面都只保留最完整的结果。想深入验证上述机制的读者可以继续阅读以下仓库文件桥接核心实现syncSdkTasks 与 mapStatus、服务端流采集、SDK 流解析与 task_update 发射前端消费useSSEStream.ts、TaskList.tsx、TaskCheckpoint.tsx相关测试sse-stream.test.ts、codex-tool-result-media.test.ts、agent-loop-tool-error.test.ts赞分享人工智能AI 应用AI Agent交互助手MCP Clients本地部署【免费下载链接】CodePilotA multi-model AI agent desktop client — connect any AI provider, extend with MCP skills, control from your phone. Built with Electron Next.js.项目地址https://gitcode.com/gh_mirrors/co0dep/CodePilot点击查看免费下载相关推荐ripgrep 快速上手指南:5 个问题装好 rg,让命令行搜索只搜该搜的ripgrep 快速上手指南:5 个问题装好 rg,让命令行搜索只搜该搜的 ripgrep 终端里写作 rg 是一个递归正则搜索工具:给定一个模式,它就把当前目CLI开发工具Jellyfish 任务执行架构解析Celery 执行层、任务真相层与前端任务中心的完整链路Jellyfish 任务执行架构解析Celery 执行层、任务真相层与前端任务中心的完整链路 Jellyfish 是一个面向 AI 短剧生成的端到端生产工作台人工智能大模型AI 应用媒体生成视频后端前端任务调度Spring 定时任务源码解析从 EnableScheduling 到任务执行的完整链路Spring 定时任务源码解析从 EnableScheduling 到任务执行的完整链路 本篇技术指南以 Spring Scheduling.md http文档教程知识库上一篇ThingsBoard终极插件版本管理指南更新通知与兼容性检查全解析下一篇ILLA Builder服务端缓存策略设计TTL与LRU算法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考