Skip to content

工具调用详情流协议

!!! note "状态" 定稿(2026-09-09,随 TFRS-401 D4 落定)。归属 TFRS-401 L1 实时过程,实施见 TFRS-420 + TFRF-142。姊妹文档:A2A 结构化进展信封。

一次工具调用期间,服务端将过程增量块持续投递到 Socket.IO,前端在该工具调用 Event 的详情区 按 tail -f 语义逐块追加渲染;对话流本体保持单节点,不逐块展开。

目标形态与边界

形态(体验原型)

  • 对话流中:工具调用仍只有一个节点(起始终态快照,同现状)。
  • 详情区(Event 详情/展开面板):随调用进行持续丰富/追加——过程块按序长成;thinking 折叠;tool_call 一行头。
  • 终值区:A2A completed 的最终回复作为权威终态显示;过程流区标记「已结束」。

与相近需求的边界

需求 维度 本协议与它
TFRS-339 / TFRF-116(Trajectory 链树 DTO 透出) 图谱:链树结构、可见性、LLM 形态字段 本协议是单工具节点内部的时间线追加流。互补:详情流是链树里一个节点「肚子里的内容」
现有 chat_event(dtos/v1/chat/event.py) 离散快照:eventId/status/eventScene/conversationId 本协议是增量追加流(新事件名,与快照并存,不合并)
TFRS-401 L2(Task.history / ListTasks) 持久化任务记录(跨进程、跨会话可查) 本协议是当次调用的实时过程流,不持久化(见 T3);L2 是粗粒度回放通道
TFRS-401 L3(异步调用) 调用模式(不阻塞) v1 不覆盖:异步调用的工具节点在返回 handle 时即终态,过程流不在该节点上展开(见 T4)

为什么归属到工具调用(而非对话流)

A2A 主叫侧一次调用 = 一次工具调用:ask_colleague BUILT_IN 工具 (robot/extensions/employee.py signal → colleague_provider.ask_colleague → A2APeerClient.send_text)。工具场景事件即 ToolEventTransitionDto (顶层 eventId,content.toolCall.{toolId, functionCall} / toolReturn)。对端 Agent 的过程消息本质是这个工具调用的执行过程,天然归属它的详情——不在对话流中追加与展开 (那也是为什么区别于 TFRS-339 的模型)。

投递面(主叫侧 → 浏览器)

通道

  • 命名空间:/chat(复用现有鉴权/S3 握手);
  • 事件名:tool_event_stream(新增;与 chat_event 快照并存);
  • 房间:conversation_id(复用 on_join_conversation 现有房间机制);
  • 生产者:主叫 worker 进程(工具执行中)经 loopback socketio client 发射——与现有 chat_event 的 loopback 机制同构(socket_namespaces/chat.py),Web 进程只做广播, worker 已持有 A2A SSE 消费句柄,天然是增量块的产生地。

载荷(主叫侧归属封装 + 信封原样透传)

{
  "v": 1,
  "callId": "caller-tool-event-id",
  "taskId": "a2a-task-id",
  "state": "running",
  "ts": 1789123456789,
  "envelope": { "v": 1, "kind": "tool_call", "streamKey": "…", "seq": 42, "status": "running", "payload": {…} }
}
字段 类型 说明
v int 本协议版本
callId str 归属键 = 主叫侧工具调用事件的 eventId(BaseEventTransitionDto.eventId,即 trajectory span_id);详情区按它聚合。获取方式见 T2
taskId str A2A task_id(区分多轮/多次调用;同 callId 下多次 task 属不同过程)
state enum running(过程块)∣ done(A2A completed 终态,唯一一次,payload.text 附终值)∣ aborted(failed/canceled 终态,payload.message 附失败摘要)
ts int 主叫侧发射时间戳(毫秒)
envelope object 姊妹文档的信封原样透传;无信封的第三方对端 → 主叫降级构造伪信封 {kind:"text", payload:{text:"<working 文本>"}}

次序:服务端保证「工具事件快照 chat_event 先发,流块后发」(worker 内工具事件落点先于消费 SSE 增量)——前端以 callId 关联即可,无需依赖到达时序。

生命周期

  1. 工具调用事件快照(现有 chat_event,tool scene)→ 详情区就位(状态「调用中…」);
  2. tool_event_stream 过程块持续到达(running);
  3. 终态:唯一一条 state=done(或 aborted)——前端收束详情(终值 + 过程结束标记);
  4. 无流块到达(对端未启用信封/纯静默):frontend 超时兜底 → 仅有快照 + 终态(终值仍可从 toolReturn/A2A 终态文本取得),流区显示「无过程详情」。

渲染语义(前端)

  • 按 callId 聚合同一详情区;区内按 envelope.seq 排序渲染(乱序容忍、同 seq 幂等);
  • tool_output_append:追加渲染(等宽/终端样式,tail -f)——v1 不会被发出(信封文档 E3), 前端可先行支持;
  • thinking:折叠段落(单行「思考中…」可展开);
  • tool_call:一行(工具名 + 开始标记);
  • 终态后:终值区固定在详情区顶部(A2A completed 文本),过程流区标记结束,不再追加。
  • 详情区字段留 mode: "live" | "replay" 占位——本期仅 live。

决策记录

# 决策点 定稿 理由
T1 事件名 tool_event_stream chat_event 语义已被快照线占用;不泛化为 event_stream,避免两线混淆
T2 callId 获取方式 v1 近似匹配(见下),内核暴露为收敛路径 内核当前不向工具暴露「进行中调用的事件 id」
T3 回放/持久化 方案 A:过程流不持久化 零存储成本;刷新后详情 = 轨迹落库快照(toolReturn 终值可见)。粗粒度回放由 L2 Task.history 承担,B(流聚合落库)视真实产品需要再立 Story
T4 限速/节流 worker 消费侧合并节流 ≤10 块/秒,超限丢过程块、不断终态 参照 TFRS-269 有界丢新先例;防前端高频重排
T5 异步调用(L3) v1 不覆盖 异步调用的工具节点在返回 handle 时即终态;异步进度走 L3 的状态工具与上下文注入

T2 细则:callId 的获取

v1(近似匹配):主叫 worker 在 send_text 消费点,从该 conversation 的 trajectory tap 中取 「最近一条未终态、且 tool_name = ask_colleague 的工具事件」的 eventId。

  • 歧义兜底:若同时存在多条符合条件的未终态工具事件 → 不带 callId,前端仅显示终值,不展开过程流。
  • 已知局限:并发同名工具调用下可能取错;工具返回前前端拿不到稳定关联 id。

收敛路径(backlog,内核通用能力):请内核向工具运行时暴露「当前进行中工具调用的事件 id」。 现状 current_tool_call_id 注入的是 LLM 的 tool_call id(drive/base.py:323,需 merge_context=True),与 trajectory span_id 不同源,且工具返回前前端拿不到(仅在 ToolReturnDto.meta.callId 里随工具返回出现)。落地后替换近似匹配,兜底逻辑保留。

关联

TFRS-401(Epic · L1 决策 T1-T5)· TFRS-420(信封生产端)· TFRF-142(本协议实施)· TFRS-246(A2A 双端)· TFRS-269(trajectory tap)· TFRS-274(ask_colleague)· TFRS-339(链树 DTO——边界见上表)· TFRF-116(前端链树渲染)· TFRF-122(聊天流渲染)。