现象
通过 ACP 协议接入的 agent调用 shell、grep、web_fetch、git_log 这类工具时,tool_call_update 携带的执行结果文本在前端 ToolCallCard 上没有展开按钮,用户看不到工具的输出。
参考截图(代表性显示):
🔧 other git log -3
🔧 other git diff 5fee96f
execute shell: cd /path && cargo test ...
每一项都是孤零零一行,没有 chevron,无法展开。对照之下,agent_thought_chunk 出来的"思考过程"卡片是有展开按钮的,可以正常查看内容。
根因
在 streaming 阶段,server/internal/api/usecase/session.go:1156-1158 的 attachSessionUpdates 对每条 update 都调用了:
runtime.OnUpdate(func(update agenttypes.Event) {
update = normalizeAgentUpdatePaths(root, update)
update = compactAgentUpdate(update)
...
if in.OnUpdate != nil {
in.OnUpdate(update) // 推送给前端
}
})
而 normalizeAgentUpdatePaths(session.go:1465-1497)和 compactAgentUpdate(session.go:1499-1507)都会经过 session.PreserveToolCallContent(server/internal/session/types.go:65-77)做过滤:
func PreserveToolCallContent(kind agenttypes.ToolKind) bool {
switch kind {
case ToolKindEdit, ToolKindDelete, ToolKindMove,
ToolKindAskUser, ToolKindTodo, ToolKindTask:
return true
default:
return false
}
}
只要 kind 不在 {edit, delete, move, ask_user, todo, task} 这个集合里,Content 就被整个置 nil。
也就是说 execute(shell)、search(grep/glob/ls)、fetch(web_fetch)、other(git_log/git_diff/MCP 工具)、read(read_file)的工具结果,在到达前端之前全部被剥光。
到了 web/src/components/stream/ToolCallCard.tsx:138-139:
const hasResult = !!result;
const hasDetails = hasContent || hasLocations || hasResult;
hasDetails 永远是 false,自然也就不渲染 chevron(第 256-271 行)。
这个行为合规吗
ACP 协议本身是通用的,官方定义的 ToolCallKind 只有 read / edit / delete / move / search / execute / think / fetch / switch_mode / other 这 10 种,且没有任何条款规定 client 可以基于 kind 丢弃 content。
task / ask_user / todo 是 mindfs 自己扩展出来的 kind,不是 ACP 规范的一部分。把"是否展示 content"这个 UI 决策耦合到 kind 上,会让所有合规的 ACP server 都需要为 mindfs 做特殊适配(目前 codes 那边已经被迫加了一个 for_mindfs_display() 把非保留 kind 强行映射成 task,这显然不是健康的协议交互方式)。
期望行为
- streaming 路径(
OnUpdate 回调推给前端的那条路径)不应当剥离 Content,无论 kind 是什么。前端应该总是能看到 agent 发过来的工具结果。
- 持久化路径上的压缩可以保留(
AddExchangeAux 已经在 manager.go:356 调用 CompactExchangeAux),那是为了减小存档体积,和实时显示是两回事。
建议修复
最小改动:把 streaming 路径上的 strip 去掉,持久化路径不变。
server/internal/api/usecase/session.go:
runtime.OnUpdate(func(update agenttypes.Event) {
update = normalizeAgentUpdatePaths(root, update)
- update = compactAgentUpdate(update)
...
})
func normalizeAgentUpdatePaths(root pathNormalizer, update agenttypes.Event) agenttypes.Event {
...
for i := range toolCall.Locations {
toolCall.Locations[i].Path = normalizeToolPath(root, toolCall.Locations[i].Path)
}
- if session.PreserveToolCallContent(toolCall.Kind) {
- for i := range toolCall.Content { ... }
- } else {
- toolCall.Content = nil
- }
+ for i := range toolCall.Content {
+ toolCall.Content[i].Path = normalizeToolPath(root, toolCall.Content[i].Path)
+ if toolCall.Content[i].Type == "text" {
+ toolCall.Content[i].Text = normalizeDiffTextPaths(root, toolCall.Content[i].Text)
+ }
+ }
...
}
compactAgentUpdate 函数可以删掉(没有其他调用方)。
持久化时的体积控制依旧由 Manager.AddExchangeAux → CompactExchangeAux → CompactToolCall 处理,行为不变。
影响面
- 修复后,所有 ACP 工具调用(shell / grep / web_fetch / read_file / git_*)的结果会和 thought_chunk 一样,实时就能在前端展开查看。
- 持久化的会话归档保持瘦身,不会因此变大。
- 兼容性:对 ACP 服务端零影响,任何 spec-compliant agent 都能直接受益。
复现步骤
- 用 mindfs 接任何按 ACP 规范发
kind=execute 的 agent。
- 让 agent 跑一条 shell,例如
git log -3。
- 观察 SessionViewer:tool call 卡片只有标题,没有展开按钮,看不到命令输出。
- 对照思考过程卡片(thought_chunk),展开按钮正常。
现象
通过 ACP 协议接入的 agent调用 shell、grep、web_fetch、git_log 这类工具时,
tool_call_update携带的执行结果文本在前端ToolCallCard上没有展开按钮,用户看不到工具的输出。参考截图(代表性显示):
每一项都是孤零零一行,没有 chevron,无法展开。对照之下,
agent_thought_chunk出来的"思考过程"卡片是有展开按钮的,可以正常查看内容。根因
在 streaming 阶段,
server/internal/api/usecase/session.go:1156-1158的attachSessionUpdates对每条 update 都调用了:而
normalizeAgentUpdatePaths(session.go:1465-1497)和compactAgentUpdate(session.go:1499-1507)都会经过session.PreserveToolCallContent(server/internal/session/types.go:65-77)做过滤:只要 kind 不在
{edit, delete, move, ask_user, todo, task}这个集合里,Content就被整个置 nil。也就是说
execute(shell)、search(grep/glob/ls)、fetch(web_fetch)、other(git_log/git_diff/MCP 工具)、read(read_file)的工具结果,在到达前端之前全部被剥光。到了
web/src/components/stream/ToolCallCard.tsx:138-139:hasDetails永远是 false,自然也就不渲染 chevron(第 256-271 行)。这个行为合规吗
ACP 协议本身是通用的,官方定义的 ToolCallKind 只有
read / edit / delete / move / search / execute / think / fetch / switch_mode / other这 10 种,且没有任何条款规定 client 可以基于 kind 丢弃 content。task / ask_user / todo是 mindfs 自己扩展出来的 kind,不是 ACP 规范的一部分。把"是否展示 content"这个 UI 决策耦合到 kind 上,会让所有合规的 ACP server 都需要为 mindfs 做特殊适配(目前 codes 那边已经被迫加了一个for_mindfs_display()把非保留 kind 强行映射成task,这显然不是健康的协议交互方式)。期望行为
OnUpdate回调推给前端的那条路径)不应当剥离Content,无论 kind 是什么。前端应该总是能看到 agent 发过来的工具结果。AddExchangeAux已经在manager.go:356调用CompactExchangeAux),那是为了减小存档体积,和实时显示是两回事。建议修复
最小改动:把 streaming 路径上的 strip 去掉,持久化路径不变。
server/internal/api/usecase/session.go:runtime.OnUpdate(func(update agenttypes.Event) { update = normalizeAgentUpdatePaths(root, update) - update = compactAgentUpdate(update) ... })func normalizeAgentUpdatePaths(root pathNormalizer, update agenttypes.Event) agenttypes.Event { ... for i := range toolCall.Locations { toolCall.Locations[i].Path = normalizeToolPath(root, toolCall.Locations[i].Path) } - if session.PreserveToolCallContent(toolCall.Kind) { - for i := range toolCall.Content { ... } - } else { - toolCall.Content = nil - } + for i := range toolCall.Content { + toolCall.Content[i].Path = normalizeToolPath(root, toolCall.Content[i].Path) + if toolCall.Content[i].Type == "text" { + toolCall.Content[i].Text = normalizeDiffTextPaths(root, toolCall.Content[i].Text) + } + } ... }compactAgentUpdate函数可以删掉(没有其他调用方)。持久化时的体积控制依旧由
Manager.AddExchangeAux → CompactExchangeAux → CompactToolCall处理,行为不变。影响面
复现步骤
kind=execute的 agent。git log -3。