Skip to content

A2A 结构化进展信封

!!! note "状态" 定稿(2026-09-09,随 TFRS-401 D4 落定)。归属 TFRS-401 L1 实时过程,实施见 TFRS-420。 姊妹文档:工具调用详情流协议。

A2A 被叫端在 message/stream 的中间 working 状态更新中,用结构化信封(Block)携带本轮 turn 的过程信息(工具调用、思考、流式输出),供主叫端转发到前端「工具调用详情」区持续追加渲染。

现状与动机

  • 现状链路:被叫 executor(protocols/a2a/executor.py::_emit_trajectory)把 trajectory 增量转成 纯文本 working 更新(new_text_message(text=...))——只有文本、无结构,且被主叫端 send_text 全部丢弃(client.py "working 增量仅进度不作回复")。
  • 动机:A2A 对端是另一个 Agent,其过程(尤其长耗时工具调用、tail -f 类流式输出)对主叫侧 用户是黑盒。需要把过程信息结构化带出来,让主叫侧前端在工具调用详情区实时丰富。
  • 纪律不变:终值仍单源自 completed 终态(及 failed/canceled)。信封只是 best-effort 进度, 可断、可丢、可被终态覆盖。

载体选型(实测 a2a-sdk ≥1.1 约束)

a2a.types.Part 为单一联合类型,且 metadata 独立于 content oneof:

字段 类型 说明
text / raw / url / data oneof content(互斥) 一个 Part 只能放其中之一;data = google.protobuf.Value(任意 JSON)
metadata google.protobuf.Struct(独立字段,任意 JSON) 可与 text/data 并存

承担载体:中间 working 更新消息仍为 TaskStatusUpdateEvent(status=TaskStatus(state=working, message=Message(parts=[Part(...)]))),不新增 A2A 方法或事件类型。

传输格式:方案 A — text 回退 + metadata 信封

每个 working Part 同时携带:

  • text:人类可读回退文本(现状 _trajectory_text 产物不变);
  • metadata = {"tfrs": "<envelope JSON 字符串>"}:结构化信封。

取舍:

方案 描述 优点 缺点
A(定稿) text + metadata.tfrs 信封 第三方/未升级主叫看到纯文本(完全向后兼容,行为=现状);升级主叫读信封 metadata 内信封 JSON 字符串需自行截断管控
B 仅 Part(data=<envelope>) 纯结构化 第三方对端/旧客户端 get_message_text 得空——破坏互操作

方案 A 的 metadata 是 Struct,可直接放嵌套结构;为兼容「逐字段 map 语义」起见统一序列化为单个 JSON 字符串放在固定键 tfrs,读取方 parse 失败即视为无信封(降级文本)。

信封 Schema(v1)

{
  "v": 1,
  "kind": "tool_call",
  "streamKey": "callee-tool-event-id",
  "seq": 42,
  "status": "running",
  "payload": {
    "tool": "ask_colleague",
    "tool_call_id": "optional-callee-id",
    "chunk": "分片输出……"
  }
}
字段 类型 必填 说明
v int ✅ 信封版本。读取方不认识(v > 1)→ 丢弃信封、用 text 回退
kind enum ✅ thinking ∣ tool_call ∣ tool_output_append ∣ text ∣ error
streamKey str ❌ 被叫侧聚合键(优先 tool_call_id;无则省略=单一聚合流)
seq int ✅ 被叫 turn 内单调递增;允许跳号(trajectory 有界队列丢新,TFRS-269)
status enum ✅ running(过程块)∣ done(对应该 streamKey 的工具执行完成)∣ aborted
payload dict ✅ per-kind 见下

per-kind payload:

kind payload 语义 v1 启用
thinking {text} 思考段(折叠渲染) ✅
tool_call {tool, tool_call_id?} 工具开始 ✅
tool_output_append {tool, chunk, tool_call_id?} 增量输出分片(tail -f 场景逐片;seq 即顺序) ❌ 见 E3
text {text} 普通进度文本 ✅
error {message} 被叫侧过程错误(不替代终态判据) ✅

约束规则

  1. 一条 working 更新最多一个信封;无信封 = 纯文本 working(升级前的形式,继续合法)。
  2. text 回退必须与信封同 Part 并存(方案 A 铁律:任何消费方都能当升级前消息处理)。
  3. best-effort:信封可断、可丢、可乱序到达(乱序容忍:前端按 (streamKey, seq) 排序合并,重复到达幂等——同 (streamKey, seq) 只吃一次);任何中途状态都可被终态覆盖。
  4. seq 域 = 单次 turn:一次 send 请求内严格递增(起点不约束,见 E2);不同 turn 由 task_id 区分(属 A2A 事件上下文属性,主叫侧可读,不放进信封)。
  5. 体量:payload.chunk 单条软上限 8KB、硬上限 64KB,超限丢弃该块并以 text 回退告知;长输出优先按行切分,由 seq 保证顺序。
  6. 不得含敏感数据:peers 为同组织机器人,但信封仍按「可被日志留痕」约束——不放密钥、凭据、用户隐私文本。
  7. 协议纯增量:不新增方法/事件类型,enable_v0_3_compat 双线(message/send / message/stream)不变。
  8. 终态纪律:completed 终态权威携带最终回复;failed/canceled 终态收束;信封里任何 error/status 都不推翻终态判据。

决策记录

# 决策点 定稿 理由
E1 metadata 键名 tfrs 短、无歧义;版本由信封内 v 承担
E2 seq 起点 不约束(允许非 0) 消费方按 (streamKey, seq) 排序合并,不依赖起点值
E3 tool_output_append 本期是否启用 v1 不启用,kind 保留定义 被叫内核当前无工具输出增量事件源;先发 tool_call/thinking/text。待内核提供流式输出事件后启用,消费侧无需改协议
E4 payload.chunk 上限 软 8KB / 硬 64KB,优先按行切分 tail -f 长行场景;超限丢块并回退 text
E5 与 L3 异步的关系 信封与调用模式解耦:异步调用没有随请求返回的流,同一信封经 SubscribeToTask 投递 不因异步引入第二种信封格式
E6 与 L2 历史的关系 本协议不负责持久化;Task.history 会累积 working 的 status.message(TaskManager 语义),构成粗粒度回放通道 精确过程流回放不在 v1 范围(见姊妹文档回放决策)

与异步调用(L3)的交互

return_immediately=true 的调用没有随 SendMessage 返回的 SSE 流,但信封机制不变:

  • 主叫侧对已派发任务开 SubscribeToTask(tasks/{id}:subscribe)订阅 → 收到的仍是同一 TaskStatusUpdateEvent 结构 → 信封解析逻辑零改动复用;
  • 未订阅时,进度只能经 GetTask 轮询取到 Task.history 中的过程消息(粒度粗,且受 history_length 截断)。

生产端改造点

被叫 executor.py:

  • _trajectory_text(event_dto) → _trajectory_blocks(event_dto) -> list[dict](event_dto camelCase: eventScene/status/content),按 scene 映射:
  • tool scene 且 content 为 ToolEventContentDto(toolCall.toolId/toolId/functionCall)→ tool_call(开始);
  • 其它 scene → thinking/text;
  • tool_output_append 留空(E3,待内核增量源);
  • executor 维护 turn 内 seq 计数器;_emit_trajectory 改为发「text 回退 + metadata 信封」Part;
  • 输出阶段:构造 done 块(tagging 该 streamKey 的工具调用结束)。

主叫 client.py::send_text:

  • 消费 SSE 时不再丢弃 working:解析 metadata.tfrs(失败→降级伪信封 {kind:"text"}), 转发给前端详情流(见姊妹文档);终态消费逻辑(COMPLETED/FAILED/CANCELED)不变。

关联

TFRS-401(Epic · L1 决策 E1-E6)· TFRS-420(本协议实施)· TFRF-142(前端消费)· TFRS-246(A2A 双端)· TFRS-269(trajectory tap)· TFRS-281(contextId 解析)· TFRS-339(Trajectory 链树 DTO——另一维度:本协议是过程流,不是树/可见性语义)· TFRS-384(结对 contextId)· TFRF-116(前端树型渲染)。