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