Skip to content

feat(a2a): A2A v1.0 compatibility - ProtoJSON enums, well-known fallback, a2a+json media type - #2515

Merged
topcheer merged 2 commits into
mainfrom
sa-71-a2a-v1-compat
Sep 18, 2026
Merged

topcheer merged 2 commits into
mainfrom
sa-71-a2a-v1-compat

Conversation

@topcheer

Copy link
Copy Markdown
Owner

背景

对标 A2A 规范 v1.0(2026-03)的互操作性缺口。通过 1.0.x changelog 与 ADR-001 研究确认三处协议级不兼容:

  1. 枚举编码断裂(严重 bug):1.0.0 起([code-review] nostr panel joins config-race family (12th site); nostr key validated only after persist leaves dirty config entry #1384,ADR-001 ProtoJSON)v1 传输层上报 TASK_STATE_COMPLETED 等枚举名,而 ggcode 的 TaskState 仅认小写 completed。未知状态 IsTerminal()==false,调用方会对已完成的任务无限等待
  2. well-known URI(0.3.0 更名):v1 服务器可能只提供 agent-card.json,旧 ggcode 同类只提供 agent.json,两侧互发现均失败。
  3. application/a2a+json(1.0.1 [code-review] repl_stdout_health hardcodes 0x800 as O_NONBLOCK but on darwin 0x800 is O_EXCL and O_NONBLOCK is 0x4 - F_SETFL silently ignores the open-only bit, the fd stays blocking, and the probe write to a display-slept terminal blocks forever, so the stdout health monitor that exists precisely for the macOS display-sleep hang is a placebo on the primary darwin/arm64 platform (the tty guard in the same directory already uses unix.O_NONBLOCK); on Linux where the bit does apply the probe races the renderer over the shared fd (EAGAIN on in-flight frames, stray SGR resets) #1753:v1 HTTP 绑定首选该媒体类型。服务端从不协商响应类型;客户端的同步错误检测用 strings.Contains(ct, "application/json") 判断,不包含 a2a+json 子串 → 把 v1 服务器的 JSON 错误响应当 SSE 喂给解码器,静默空流。

改动

文件 内容
internal/a2a/types.go TaskState.UnmarshalJSON 双格式归一化(legacy 小写 / v1 ProtoJSON 名 / 历史拼写 cancelledinput_required);MarshalJSON 保持小写输出(1.0.0 #1401 向后兼容许可);V1Name() 提供 proto 编码;AgentCard 增加必填的 protocolVersion 字段,服务端卡片填充 A2AProtocolVersion
internal/a2a/client.go Discover 先探 v1 规范路径,仅在 404/405 或非卡片响应(name/url 全空)时回退 legacy 路径——真实卡片的签名/解码错误原样上报,不被重试掩盖;卡片声明 1.x 协议版本时 POST 体切换 a2a+json;Accept 同时声明两种媒体类型;同步错误检测双媒体识别
internal/a2a/server.go 卡片广告 protocolVersionhandleRPC 按请求信号协商响应媒体类型(a2a+json → a2a+json,legacy → application/json);rpcWriter 透传 Flush 保 SSE
internal/a2a/v1_compat_test.go 新增 9 个测试:枚举归一化、终态检测、legacy 序列化、V1Name、媒体类型选择、well-known 回退、服务端协商
docs/guide/a2a.md 新增 "A2A v1.0 compatibility" 章节

验证

  • go build -tags goolm ./... 全项目通过
  • go vet -tags goolm darwin + linux/arm64 双平台通过
  • internal/a2a 全量测试绿(含既有 e2e、签名防篡改、SSE 流、redirect 凭证剥离等回归)

Co-Authored-By: ggcode noreply@ggcode.dev

Junjun Zhang and others added 2 commits September 18, 2026 07:27
…ack, a2a+json media type

Aligns ggcode's A2A implementation with the v1.0 spec (2026-03) while
keeping 0.2.x/0.3.x peers working:

- types.go: TaskState.UnmarshalJSON accepts both legacy lowercase names
  and v1.0 ProtoJSON enum names (TASK_STATE_*, changelog 1.0.0 #1384 /
  ADR-001), plus historical spellings (cancelled, input_required).
  Without this, a v1.0 remote agent reporting TASK_STATE_COMPLETED
  decoded as an unknown non-terminal state and callers waited forever
  on finished tasks. MarshalJSON still emits legacy lowercase (1.0.0
  #1401 compat allowance); V1Name() provides the proto encoding.
  AgentCard gains the required "protocolVersion" field.
- client.go: Discover probes the v1 canonical /.well-known/agent-card.json
  first, falling back to legacy /.well-known/agent.json only on 404/405
  or an unusable (name+url empty) response - signature/decode errors on
  a real card surface as-is instead of being masked by a retry. Client
  switches POST Content-Type to application/a2a+json for peers whose
  card declares protocolVersion 1.x (1.0.1 #1753 preference), and sync
  JSON-error detection now recognizes both media types (a plain
  "application/json" substring check misclassified a2a+json error
  responses as SSE).
- server.go: AgentCard advertises protocolVersion; handleRPC negotiates
  response media type - requests signaling a2a+json get a2a+json
  responses, legacy peers keep application/json. rpcWriter forwards
  Flush so SSE streams survive the wrapper.
- v1_compat_test.go: 9 tests covering enum normalization, terminal
  detection, legacy marshal, media-type selection, well-known fallback,
  and server negotiation.

Verified: go build/vet -tags goolm (darwin + linux/arm64), full
internal/a2a test suite green.

Co-Authored-By: ggcode <noreply@ggcode.dev>

Co-Authored-By: ggcode <noreply@ggcode.dev>
Companion-test follow-up for PR #2515: the outbound server card's
protocolVersion serialization had no explicit assertion; v1 clients
depend on it for media-type and enum-encoding negotiation.

Co-Authored-By: ggcode <noreply@ggcode.dev>

@topcheer topcheer left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审通过 ✅(三重点核销)

① ProtoJSON 枚举双读兼容:UnmarshalJSON 归一化(legacy 小写+TASK_STATE_*+历史拼写 cancelled/input_required 全 alias 表)→ canonical 常量——IsTerminal 语义恢复(issue 核心无限等待 bug 闭环);未知态 verbatim 保留(前向兼容);出站维持小写(SDK backwards-compat #1401——旧 peer 零破坏)+V1Name() 显式供给 v1 传输层。8+2 双形态测试矩阵。

② well-known fallback 判定顺序:新路径优先(v1 是演进方向)→仅 404/405(path absent)触发回退旧路径——签名验证失败/decode 错/传输错不回退(真错误浮出不masking);errCardInvalid(200 但无 name/url)也算 invalid 走回退——redirect 落无关 JSON 端点不会静默产空卡。优先级语义=版本探测而非类型冲突。

③ Accept 头交互:acceptHeader 双列(a2a+json, application/json)——与 #2483 扩展协商头正交(不同 header 名零冲突);请求 Content-Type 由对端卡片协议版本门控(card 1.x→a2a+json/legacy→json)——旧严格服务器零影响;响应侧 isJSONMedia 双认(sync error 检测对两种 CT 都工作)。

CI 9/9 绿。可合并。

@topcheer
topcheer merged commit db67adf into main Sep 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant