工具组摘要
对话记录里,一串连续的工具调用折叠成一行。本页规定这一行写什么:用一句短话说明这些调用做了什么,由客户端根据记录里已有的工具调用生成;如果这串调用改了文件,就带一个 +N −M 徽标。本页也评估是否值得为整轮加一行摘要。
这一行从哪来
assistant-bubble.tsx 把一轮的有序 blocks 切成段:每个文本块单独成段,每串连续的思考和工具块合成一个 ExecutionStrip(execution-strip.tsx),标题由 summarizeToolGroup() 生成。没有有序 blocks 的旧记录走 LegacyRuntimeTrace,它把调用交给同一个函数。折叠行是一行 28px,见聊天轮次视觉规范。
每个调用带的数据
| 字段 | 来源 | 用途 |
|---|---|---|
tool | block | 通过 tool-presentation.ts 的 presentTool() 归类,和时间线行用同一套分类。 |
input | block,调用参数的完整 JSON | 路径(file_path、path)、命令、搜索模式、URL。edit 带 old_string/new_string,write 带 content,apply_patch 带整个补丁。 |
result、is_error、outcome | block | 成功或失败。edit 的结果写 (N replacements);apply_patch 的结果写 Added、Updated 或 Deleted。 |
| 运行中 | msg.tools[].status | 哪个调用正在跑。 |
turnFiles | 助手消息,结束时从变更日志写入 | 整轮权威的逐文件 op(create/modify/delete)和 added/removed。按路径聚合,没有工具调用 id,只在本轮结束后才有。 |
所以一轮里两个组改了同一个文件时,不能直接从 turnFiles 读每组的统计。统计从参数推导,缺的部分用 turnFiles 补。
摘要语法
短语
组内调用按类型分桶,每桶一个短语,放在该桶第一次出现的位置,读起来就是事情发生的顺序。短语之间用","连接(英文用 ", ",首字母大写)。思考只在组里没有别的内容时写"思考",否则省略,因为每轮都会思考。
| 类型(工具) | 一个 | 多个 | 进行中 |
|---|---|---|---|
命令(bash、process、terminal_use、execute_code) | 运行了 1 条命令 | 运行了 3 条命令 | 正在运行命令… |
新建文件(write 新路径、apply_patch Add File) | 创建了 pill-shadow.html | 创建了 2 个文件 | 正在创建 pill-shadow.html… |
修改文件(edit、覆盖已有文件的 write、apply_patch Update) | 修改了 app.tsx | 修改了 4 个文件 | 正在修改 app.tsx… |
删除文件(apply_patch Delete File) | 删除了 old.css | 删除了 2 个文件 | 正在删除 old.css… |
读取(read、pdf) | 读取了 package.json | 读取了 3 个文件 | 正在读取 package.json… |
列目录(list) | 列出了 1 个目录 | 列出了 2 个目录 | 正在列出目录… |
代码搜索(grep、glob、semble_*、lsp_*) | 搜索了"tl-toggle" | 搜索了 3 次 | 正在搜索"tl-toggle"… |
网页搜索(web_search) | 搜索了网页 | 搜索了 2 次网页 | 正在搜索网页… |
抓取(web_fetch) | 抓取了 docs.anthropic.com | 抓取了 3 个网页 | 正在抓取 docs.anthropic.com… |
浏览器(web_use、agent_browser、playwright_browser) | 操作了浏览器 | 正在操作浏览器… | |
子代理(agent、task、send_message) | 运行了子代理 Reviewer | 运行了 2 个子代理 | 正在运行子代理 Reviewer… |
图片(image_generate / image_analyze) | 生成了 1 张图片 | 生成了 2 张图片 | 正在生成图片… |
待办(todo_*) | 更新了待办 | 正在更新待办… | |
提问(ask_user_question) | 提了 1 个问题 | 正在提问… | |
记忆(memory_*) | 读写了记忆 | 正在读写记忆… | |
| 程序 / 技能 | 运行了程序 deploy、使用了技能 pdf | 运行了 2 个程序 | 正在运行程序 deploy… |
| 其他(MCP、自定义) | 使用了 linear_search(两次时加" 2 次") | 使用了 linear_search 和 jira_create;使用了 3 个工具 | 正在使用 linear_search… |
命名:只有一个文件时写文件名(basename)。命令从不写进标题,因为命令很长,展开后的行里已经有。搜索模式加引号,截到 24 个字符。抓取只写主机名。一个桶只在它恰好有一个目标时才写出目标名,所以出现第二个文件后,"读取了 a.ts"就变成"读取了 2 个文件"。同一文件的重复调用只算一次。
数字:英文单数用 "a"/"an",两个及以上用数字。中文一律用数字加量词(1 条命令、3 个文件、2 次)。
失败:组里任何调用失败时,标题后面跟一个淡红色的"· 1 个失败"。失败的调用仍计入所属的桶;写文件失败不计入徽标。
进行中
当这个组是流式中这一轮的最新部分时,已完成的桶保持过去时,正在执行的调用所属的桶改成进行时并以"…"结尾,并移到行尾(最新一步在眼睛预期的位置),例如"读取了 2 个文件,正在运行命令…"。扫光效果保留。调用完成后,短语回到过去时,放回它第一次出现的位置。组里目前只有思考时,标题是"思考中…"。
截断
屏幕上标题只占一行:标题 span 设 min-width:0 和 CSS 省略号,失败提示、徽标和箭头设 flex:none,永远不会被挤掉。不设字符上限,同一个标题在窄侧栏和宽窗口都能正确显示。这一行不带任何悬停提示:它是对话内容,不是需要解释用途的按钮,展开后就能看到每一次调用。
改动徽标
组里至少有一个写文件调用成功,并且所有成功的写文件调用统计都已知时,才显示徽标。徽标把这些统计加起来,0 也显示(+16 −0),复用本轮文件卡片的 .turn-files-diff 样式。每个调用的统计来源:
apply_patch:数补丁正文里的+行和-行,结果精确。edit:对old_string和new_string做行级 diff,再乘以结果里的替换次数。任一侧超过 2,000 行时退化为直接比较行数。write:turnFiles把该路径标为create时,统计是+行数(content) −0;标为modify且本轮对该路径的所有写入都在这个组内时,取turnFiles的数值;其余情况统计未知,徽标先不显示。流式期间这通常意味着徽标在本轮结束时出现。
write 算"创建"还是"修改"也按这个规则:turnFiles 出现前,本轮之前没读过、没改过该路径的 write 才算"创建";turnFiles 标为 modify 后改成"修改"。
文案
所有短语都走 useTranslation() 现有的 text(en, zh)。每个桶一条:过去时和进行时两种形式,每种又分单目标和计数两种写法。中文过去时用"了",进行时用"正在":"运行了 1 条命令,创建了 pill-shadow.html"、"读取了 2 个文件,正在运行命令…"。
在哪生成
全部由客户端根据结构化工具数据生成,是 tool-presentation.ts 旁边的一个纯函数。不需要模型调用,也不需要改后端。可选的后端改进有一项:在每条变更日志记录上写入工具调用 id,并随工具结果下发该调用的 {path, op, added, removed}。这样流式期间 write 的统计也精确,两个组改同一文件时每组的统计也精确。
预览
每组对比同一批调用的按类别计数标题和本设计的标题。都是 15px 的折叠行,和对话记录一致。
整轮摘要
评估过在一轮顶部加一行,例如"读取了 9 个文件,修改了 3 个文件,运行了 6 条命令 · +40 −12"。
- 确定性版本没有额外成本:用同一个函数遍历整轮的工具块即可。但每轮结尾已经有文件卡片的整轮统计,每个组也已经有自己的标题,回复正文又是模型自己对这一轮的说明。顶部一行会和这两者重复。
- 模型生成版本("修好了胶囊阴影并验证了构建")每轮要多一次调用,走会话自动命名用的标题模型。它要花 token 和时间,只能在本轮结束后出现,还要把本轮内容再发给模型一次。它最可能说的也就是回复里已有的话。
不做整轮摘要行。别处需要把一轮压成一行时(消息导航、悬停预览、父轮次里的子代理行),由那个界面调用确定性的汇总函数。
实现
- tool-group-summary.ts 是纯函数模块。
summarizeToolGroup(blocks, {text, active, runningIds, spawnNames, turnFiles, turnBlocks})返回{label, failed, added, removed};没有徽标时added和removed都是null。SPAWNING_TOOL_NAMES定义在这里;没有专门分桶的工具沿用presentTool()的分类;同时导出lineDiff()(去掉首尾相同行后做行级 LCS,上限LINE_DIFF_CAP= 2,000 行)和parsePatch()。 ExecutionStrip接收label: string | ToolGroupSummary。工作流运行卡片和调试面板传的纯字符串照旧渲染,不带失败提示和徽标。这一行渲染带省略号的标题、红色失败提示、徽标(本轮文件卡片的.turn-files-diff/.turn-files-stat胶囊)、箭头和悬停提示。AssistantBubble给每一段传入该段的 blocks、整轮 blocks、msg.turnFiles、运行中调用 id,以及该段是否是正在进行的末段。- 旧记录走同一个函数。
LegacyRuntimeTrace(没有有序 blocks 的 agent 调用)把每个调用转成以函数名命名的工具块,标题读作"使用了 extract_tables"而不是"调用 1 个函数",调用进行中也会显示扫光。没有工具块的旧子代理条只传子代理名称。 app/styles/chat/execution-strip.css:.tl-label省略号、.tl-fail,以及.tl-diff和.tl-chev的flex:none。- 测试:
apps/web/tests/chat/tool-group-summary.test.mjs覆盖每个桶的语法、中英文复数、顺序、进行时、失败、徽标来源和未知统计、apply_patch 解析和带上限的行级 diff。
实现状态
已按本文实现。可选的后端改进(变更日志记录带工具调用 id)没有做,所以覆盖已有文件的 write 要等本轮结束后才显示徽标。