首页 / 资讯中心 / 文章详情

Gemini cli 源码分析之工具篇:Write-Todos 工具的任务拆解与状态流转

Gemini cli 源码分析之工具篇:Write-Todos 工具的任务拆解与状态流转 ★ FEATURED ARTICLE
1. Write-Todos 工具到底在解决什么问题从一次多步任务跑偏说起如果你用 Gemini cli 跑过稍微复杂一点的任务比如「把 src 下所有 fetch 调用改成统一封装的 request 方法并补上错误处理」大概率遇到过这种情况模型前两步改得挺好第三步突然开始重复改同一个文件或者干脆忘了自己已经改过哪些文件。这不是模型能力问题而是 CLI 工具层缺少一个显式的任务状态载体。Write-Todos 就是 Gemini cli 里专门用来解决这个问题的工具它让模型把任务拆成清单、标记进度、并在终端 UI 上实时展示从而把「多步执行」从模型脑内记忆变成可观测的外部状态。这个工具适合谁读源码一类是想给自家 Agent 框架加任务清单能力的开发者一类是想搞清楚「声明式工具 状态流转 UI 联动」这套组合拳怎么落地的人。我试过把它的实现拆开看发现真正有价值的不是那个 Todo 数组本身而是三件事参数校验里强制「同时只能有一个 in_progress」的业务规则、工具执行结果如何进入 history 并被 UI 倒序检索、以及测试文件如何用参数化测试覆盖两种显示模式。这三块拼起来才是一个能用的任务管理工具而不是一个花架子。本篇的阅读路径是这样安排的先定位源码文件与关键函数再讲清楚数据结构与状态机然后给出可复制的配置片段让你在本地触发 Write-Todos接着用实际请求验证行为最后把常见报错对照源码逐条排查。全程围绕packages/core/src/tools/write-todos.ts和packages/cli/src/ui/components/messages/Todo.tsx两个文件展开你可以边读边在编辑器里跳转对照。需要先说明一个前提Write-Todos 在 Gemini cli 里默认是关闭的由配置项useWriteTodos控制必须显式开启才会进入模型的工具列表。很多人读源码时纳闷「为什么我跑起来模型从不调用它」答案就在这个开关上。后面第 3 节会给出完整的开启方式。另外提醒一点读这类工具层源码别一上来就通读全文。正确姿势是先看类型定义TodoStatus、Todo、TodoList再看工具类的validateToolParamValues最后看execute的返回结构。因为声明式工具的核心契约就藏在这三处输入长什么样、什么输入算非法、输出长什么样。把这三者吃透剩下的 UI 部分只是消费者。2. 源码阅读路径与关键函数定位Write-Todos 工具层结构拆解先给一张定位清单你可以直接按文件路径跳转。核心实现集中在packages/core/src/tools/write-todos.ts约 229 行类型定义在packages/core/src/tools/tools.ts的 605 到 610 行附近UI 组件在packages/cli/src/ui/components/messages/Todo.tsx测试在write-todos.test.ts约 109 行和Todo.test.tsx。这四个文件构成一个完整的闭环定义、实现、展示、验证。数据结构非常克制就三个类型。TodoStatus是四个字面量联合类型pending、in_progress、completed、cancelled。Todo只有两个字段description字符串和status。TodoList包一层todos数组。这种极简设计的好处是 JSON Schema 好写、模型好生成、UI 好渲染代价是无法表达优先级、截止时间、子任务嵌套。如果你要扩展建议在Todo上加可选字段而不是改状态枚举否则状态机逻辑会失控。状态流转的约束是这套设计的灵魂。四种状态里pending是初始态in_progress是执行态completed和cancelled是终态。关键业务规则写在validateToolParamValues里统计status in_progress的数量大于 1 就直接抛错错误信息是Invalid parameters: Only one task can be in_progress at a time.。这条规则的意义在于强制模型线性推进任务避免它同时声称多个任务在进行中却一个都没做完。从状态机角度看合法迁移是pending → in_progress → completed/cancelled且任意时刻in_progress集合大小不超过 1。工具类本身继承自BaseDeclarativeTool这是理解调用链路的关键。声明式工具意味着参数校验不靠手写 if-else 堆砌而是用 JSON Schema 描述参数形状框架层负责基础类型校验业务规则再在validateToolParamValues里补充。WriteTodosToolInvocation类约 98 到 140 行负责实际执行它的execute方法返回两部分人类可读的格式化文本以及结构化的resultDisplay就是TodoList。这个双返回结构很重要前者给模型看后者给 UI 看职责分离得很干净。调用链路可以这样串起来用户请求进入 → 模型判断任务复杂度 → 决定调用write_todos_list→ 框架用 JSON Schema 校验参数 →validateToolParamValues检查单一 in_progress 规则 →execute生成结果 → 结果写入uiState.history的tool_group项 →TodoTray组件用useMemo倒序遍历 history 找到最新的resultDisplay→ 渲染状态图标。这条链路上任何一环出问题表现都是「清单不显示」或「状态不对」第 5 节会按环节给排查方法。UI 侧的状态图标系统值得单独说。TodoStatusDisplay组件用 switch 映射四种状态completed是绿色对勾in_progress是蓝色箭头»pending是灰色方框cancelled是红色叉号。颜色取自主题的status.success、text.accent、text.secondary、status.error。这种映射看似琐碎但它决定了用户能否一眼扫出进度。TodoTray还有两个显示模式showFullTodos为 true 时展开全部条目为 false 时只显示标题和当前进行中的任务用 CtrlT 切换。无活跃任务时面板自动隐藏这个逻辑避免了空清单占屏。测试文件是很好的使用说明。write-todos.test.ts里有一条测试专门验证「两个 in_progress 必须抛错」参数构造和断言错误信息都写得很直白。Todo.test.tsx用describe.each([true, false])参数化跑两种显示模式还用toMatchSnapshot()做视觉回归。读测试比读实现快因为测试直接告诉你「什么输入对应什么输出」这是源码分析里性价比最高的部分。3. 可复制配置在本地开启 Write-Todos 并接入模型服务要让 Write-Todos 真正跑起来需要两步开启配置开关以及给 Gemini cli 配一个可用的模型服务端点。第二步涉及 Base URL、API Key、Model ID 三件套这里用 TaoToken 的兼容端点做示例它的 API 地址是https://taotoken.net/api模型对话入口在 deep link 里可以找到。注意配置里只写 API 根地址不要带多余路径。先看配置开关。Gemini cli 的配置文件通常在用户目录下的 settings 文件里useWriteTodos默认 false改成 true 即可。下面是一个可复制的 JSON 片段路径按你本地实际位置调整{ useWriteTodos: true, model: { name: gemini-2.5-pro, baseUrl: https://taotoken.net/api, apiKey: sk-你的密钥 } }如果你用的是环境变量方式注入密钥把apiKey那行换成引用即可比如apiKey: ${TAOTOKEN_API_KEY}然后在 shell 里 export。这样做的好处是配置文件可以进版本库而不泄露密钥。Model ID 要写服务端实际支持的名称写错了会在请求阶段报模型不存在而不是在配置加载阶段报错这点要留意。开启之后怎么确认工具真的进了模型可见列表最直接的办法是启动 CLI 后问它「你现在有哪些工具可用」或者直接给一个多步任务看它是否主动调用write_todos_list。如果模型不调用先检查useWriteTodos是否生效再检查模型是否支持工具调用function calling。有些轻量模型不支持工具调用表现就是永远只回文本。关于 API Key 的获取走 TaoToken 的 API Keys 页面生成即可deep link 是https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。生成后立刻复制保存页面通常只展示一次。如果你打算长期跑编码类任务Coding Plan 会比按量计费更划算入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。配置写完后建议先用一个最小请求验证端点连通性再启动 CLI。最小请求可以用 curl 打模型对话接口确认返回 200 且有内容再去跑 CLI。这样能把「网络/密钥问题」和「CLI 配置问题」分开排障时少走弯路。验证请求的具体写法放在下一节。还有一个容易忽略的点Write-Todos 的启用是全局的一旦开启模型在所有会话里都可能调用它。如果你只想在特定项目里用可以检查 CLI 是否支持项目级配置覆盖。源码里配置读取走的是统一配置系统项目级 settings 优先级高于用户级具体文件名以你本地版本为准改完重启 CLI 生效。4. 验证请求与成功结果触发 Write-Todos 并观察状态流转验证分两层先验证模型服务端点通再验证工具被调用且状态正确流转。第一层用 curl 打一个带工具定义的请求第二层在 CLI 里给多步任务观察输出。两层都过了说明配置和源码行为一致。先看端点验证。下面这个请求体是最小化的模型对话调用注意tools字段里放了write_todos_list的 JSON Schema 简化版实际 Schema 以源码为准。这个请求的目的是确认服务端接受工具定义并返回tool_callscurl -s https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: gemini-2.5-pro, messages: [ {role: user, content: 把重构任务拆成三步并写入待办清单} ], tools: [{ type: function, function: { name: write_todos_list, description: 写入或更新待办事项清单, parameters: { type: object, properties: { todos: { type: array, items: { type: object, properties: { description: {type: string}, status: {type: string, enum: [pending, in_progress, completed, cancelled]} }, required: [description, status] } } }, required: [todos] } } }] }成功返回里应该能看到choices[0].message.tool_calls其中function.name是write_todos_listarguments是一个 JSON 字符串解析后包含todos数组。如果返回的是纯文本而没有tool_calls说明模型没选择调用工具可以换更强的模型或把用户指令写得更明确。如果返回 401是密钥问题如果返回 404多半是 baseUrl 多写了路径。第二层验证在 CLI 里做。启动 Gemini cli输入一个明确的多步任务比如「读取 package.json列出所有 dependencies然后逐个检查是否有更新版本最后汇总成表格」。观察终端是否出现待办面板以及面板里的条目状态是否从 pending 变成 in_progress 再变成 completed。按 CtrlT 切换完整/简化视图确认两种模式都正常。任务结束后面板应自动隐藏。判断「成功」的标准有三条模型确实调用了write_todos_list可以在 CLI 的调试输出或 history 里看到tool_group项清单里任意时刻最多一个in_progress最终所有条目要么 completed 要么 cancelled没有卡在 pending 的。第三条经常被忽略如果任务结束时还有 pending 条目说明模型没走完流程这时候可以追问「还有未完成的任务吗」让它继续。如果你想更细地观察状态流转可以在 CLI 里让它「先只写清单不要执行」看它生成的初始清单是否全是 pending然后说「开始执行第一项」看它是否把第一项改成 in_progress。这种分步指令能把状态机逼出来比一次性跑完更容易看清行为。5. 常见报错对照排查从 401 到清单不显示逐条定位排障的核心思路是「按调用链路分段」。链路是配置加载 → 请求发出 → 参数校验 → 执行返回 → history 写入 → UI 渲染。每一段有各自的典型报错对照源码能快速定位。第一类401 Unauthorized或invalid api key。这发生在请求发出阶段和 Write-Todos 源码无关是密钥问题。检查三处环境变量是否真的 export 了用echo $TAOTOKEN_API_KEY确认非空、配置文件里的 key 是否有多余空格或引号、密钥是否已过期或被删除。注意不要把密钥写进会提交的文件里。第二类local proxy failed或连接超时。这类报错说明请求没到达服务端常见原因是 baseUrl 写错比如多写了/v1导致路径重复、本机网络策略拦截、或者端口被占用。先 curl 直连https://taotoken.net/api看是否通通了再查 CLI 配置。如果 curl 通而 CLI 不通检查 CLI 是否读取了另一份配置文件。第三类reading choices或Cannot read properties of undefined。这是响应解析阶段的报错通常意味着服务端返回的结构和客户端预期不一致比如返回了错误对象但客户端仍按成功结构解析。先看原始响应体curl 复现确认是 200 带choices还是 4xx 带error。如果是后者按错误信息处理如果是前者但字段缺失检查模型名是否被服务端识别。第四类Invalid parameters: Only one task can be in_progress at a time.。这条直接来自validateToolParamValues是业务规则拦截。出现它说明模型一次生成了多个 in_progress 条目。这不是 bug是设计如此。处理方式是让模型重试或者在提示里明确「一次只推进一个任务」。如果你在写自己的工具可以借鉴这个思路把业务不变量写成显式校验并给出可读错误信息比让脏数据流进 UI 强。第五类清单完全不显示。这发生在 UI 渲染阶段排查顺序是先确认useWriteTodos为 true再确认 history 里是否有tool_group项且resultDisplay含todos字段最后确认TodoTray的useMemo是否找到了最新条目。源码里它是从 history 末尾倒序找第一个含todos的resultDisplay如果历史记录被截断或结构变了就找不到。可以在 CLI 的调试模式里打印 history 验证。第六类OAuth相关报错。如果你用的是需要 OAuth 的登录方式而非 API Keytoken 过期会报这类错。处理方式是重新登录或改用 API Key 方式。源码里认证走的是统一客户端工具层不感知认证细节所以这类错一定在更外层。第七类模型不调用工具。没有报错但清单永远不出现。检查模型是否支持 function calling、工具定义是否真的传给了模型有些配置项会过滤工具列表、以及用户指令是否足够明确。把「帮我重构」改成「请先用 write_todos_list 拆解任务再执行」命中率会明显提升。6. 把源码读成可复用的设计下一步怎么动手读到这里你应该已经能把 Write-Todos 的四个文件串起来了。如果要把这套设计搬到自己的项目里建议按这个顺序动手先抄数据结构三个类型别加字段再抄校验规则单一 in_progress 这条最值得抄然后抄双返回结构文本给模型、结构化数据给 UI最后抄 UI 的倒序检索逻辑。这四步里校验规则是最容易被忽略但收益最高的因为它把「模型可能生成脏数据」这个不确定性挡在了执行之前。想继续深入源码下一步可以看BaseDeclarativeTool的实现理解 JSON Schema 校验是怎么和业务校验串起来的再看uiState.history的写入时机搞清楚工具结果是在哪一层被塞进历史的。这两个点吃透你就能自己加一个新工具并让它出现在 UI 上。实际动手时建议先用模型对话接口把工具定义和请求跑通确认服务端行为符合预期再改 CLI 源码。模型对话入口在https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite遇到参数细节以文档为准。密钥管理和用量查看在控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。如果你打算长期跑 Agent 类任务Coding Plan 的额度模型更适合高频调用场景。最后一个实操建议改完源码后先跑write-todos.test.ts确认没破坏原有校验再跑Todo.test.tsx的快照测试。快照测试失败不一定是 bug可能是你改了图标或布局这时候人工确认差异再更新快照。测试通过后再启动 CLI 做端到端验证顺序别反否则你会分不清是源码问题还是配置问题。
阅读完成 · 觉得有帮助?
咨询建站