Skip to content

工具调用结果在前端无法展开:streaming 路径上 content 被 kind 过滤剥离 #26

@KingingWang

Description

@KingingWang

现象

通过 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-1158attachSessionUpdates 对每条 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 都能直接受益。

复现步骤

  1. 用 mindfs 接任何按 ACP 规范发 kind=execute 的 agent。
  2. 让 agent 跑一条 shell,例如 git log -3
  3. 观察 SessionViewer:tool call 卡片只有标题,没有展开按钮,看不到命令输出。
  4. 对照思考过程卡片(thought_chunk),展开按钮正常。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions