Skip to content

feat: 升级a2a版本到1.0及以上 - #297

Closed
bochencwx wants to merge 1 commit into
trpc-group:mainfrom
bochencwx:feature/update_a2a_version_v2
Closed

feat: 升级a2a版本到1.0及以上#297
bochencwx wants to merge 1 commit into
trpc-group:mainfrom
bochencwx:feature/update_a2a_version_v2

Conversation

@bochencwx

Copy link
Copy Markdown
Contributor

No description provided.

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from c83eed3 to 7f9bcd7 Compare August 14, 2026 10:04
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

发现的问题

🚨 Critical

未发现必须修复的阻塞性问题。

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:134-135(及 :250 的 card 拉取):httpx 客户端以 timeout=httpx.Timeout(timeout=None) 创建,无任何超时。新增的 _resolve_legacy_card 在该客户端上发起 GET /.well-known/agent-card.json,若远端 TCP 建连成功但 HTTP 响应挂起(如服务端僵死),initialize() 会永久阻塞,进而拖住整个 Runner 调用。建议为该客户端或本次请求设置合理的 connect/read 超时。

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:248-252_resolve_legacy_card 仅从 /.well-known/agent-card.json 拉取卡片。但文档(docs/mkdocs/zh/a2a.md 升级章节)声称"0.3 和 1.0 的 Agent Card 都发布在 /.well-known/agent-card.json"——a2a 0.3 协议规范实际使用的是 /.well-known/agent.json。若目标确为纯 0.3 老服务端(只在 agent.json 发卡),此处 fetch 会失败返回 None,随后靠 CompatJsonRpcTransport 直连 agent_base_url 仍可工作,但"自动协商卡片"的路径实际不会命中,且文档结论与实现不一致。建议补一条 agent.json 的回退 fetch,或修正文档措辞避免误导。

  • examples/a2a/test_a2a.py:157-167:示例 docstring 声称 A2A_V03_COMPAT=1 演示"1.0 client -> v0.3 server",但同一环境变量同时让 run_server.pyenable_v0_3_compat=True 启动一个 1.0 服务端(并非纯 0.3 服务端),实际跑出的是"1.0 客户端(compat) <-> 1.0 服务端(compat)"。该示例无法真正覆盖"纯 0.3 服务端"组合,注释与运行行为不符,容易让使用者误判兼容性已验证。建议修正注释,或提供独立的老版 0.3 服务端以覆盖该路径。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/_application.py:80-90_ensure_card_has_url_ensure_v0_3_interfacea2a_svc.agent_card 各遍历一次 supported_interfaces,且 v0.3 接口追加后又会影响"是否有 url"的判定顺序。可读性上可合并为单次遍历计算 url 并据此决定追加/告警,避免后续维护者因两次遍历的先后依赖引入回归。

总结

本次为 a2a-sdk 0.3→1.0 的协议升级适配,核心转换(Part oneof、Struct metadata、Task-first 流式、compat 协商)逻辑严密并配有较完整测试,未发现明确阻塞问题。主要风险集中在 httpx 无超时导致的潜在挂起、v0.3 卡片路径/文档一致性,以及示例对兼容组合的描述与实际行为不符,建议在合入前处理以上 Warning。

测试建议

  • 补充 _resolve_legacy_cardagent-card.json 返回 404/连接挂起的超时与回退路径测试(确认不会无限阻塞 initialize)。
  • 补充一个真实"纯 v0.3 服务端"场景的端到端测试(或 mock 其仅在 agent.json 发卡),验证 enable_v0_3_compat 客户端的发现与调用链,以对齐文档声称的三种协议组合。

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.96386% with 30 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@1bf8510). Learn more about missing BASE report.

Files with missing lines Patch % Lines
trpc_agent_sdk/server/a2a/_remote_a2a_agent.py 89.89899% 10 Missing ⚠️
trpc_agent_sdk/server/a2a/logs/_log_utils.py 85.96491% 8 Missing ⚠️
...gent_sdk/server/a2a/converters/_event_converter.py 90.00000% 4 Missing ⚠️
...agent_sdk/server/a2a/converters/_part_converter.py 93.10345% 4 Missing ⚠️
...ent_sdk/server/a2a/executor/_a2a_agent_executor.py 75.00000% 3 Missing ⚠️
...nt_sdk/server/a2a/converters/_request_converter.py 80.00000% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #297   +/-   ##
==========================================
  Coverage        ?   88.53654%           
==========================================
  Files           ?         495           
  Lines           ?       47176           
  Branches        ?           0           
==========================================
  Hits            ?       41768           
  Misses          ?        5408           
  Partials        ?           0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经掌握了所有需要的信息,正在撰写审查结论。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:248:兼容模式只从 /.well-known/agent-card.json 拉取卡片,而纯 v0.3 服务端通常在 /.well-known/agent.json 发布卡片(a2a 0.3 规范路径)。文档(docs/mkdocs/en/a2a.md:142docs/mkdocs/zh/a2a.md:141)又声称“卡片路径不变,0.3 和 1.0 都发布在 agent-card.json”,二者对纯 v0.3 服务端不一致。结果是“1.0 客户端 → 0.3 服务端”场景下卡片拉取必然 404、返回 None,退化为直连 agent_base_url 的 v0.3 wire,丢失卡片中的 capabilities 协商。建议确认 a2a 0.3 的实际 well-known 路径并对 agent.json 做一次回退拉取,或修正文档表述。

    response = await self._httpx_client.get(f"{self.agent_base_url.rstrip('/')}/.well-known/agent-card.json")
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:343-345:出站消息 metadata 合并存在语义脆弱点。request_meta.update(a2a_message.metadata) 把 protobuf Struct 当 mapping 合并到普通 dict,数值类值会经 protobuf Value 包装(整数读回为 float);随后 a2a_message.metadata.update(request_meta) 又把含任意 Python 对象的 dict 写回 Struct。一旦 build_request_message_metadata 或既有 metadata 含非 protobuf 可序列化值(如自定义对象、None 嵌套),Struct.update 会抛异常导致整条请求失败。建议显式经 ParseDict/MessageToDict 规范化后再合并,或沿用旧版“以 request_meta 为底、dict 合并后整体赋值”的写法。

    if a2a_message.metadata:
        request_meta.update(a2a_message.metadata)
    a2a_message.metadata.update(request_meta)
  • trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py:369-395:错误路径下会重复下发 FAILED 事件。convert_event_to_a2a_eventsevent.error_code 时已把 FAILED TaskStatusUpdateEvent 入队(_event_converter.py:711-712),aggregator 记为 FAILED;循环结束后 else 分支又调用 create_final_status_event(state=FAILED, ...) 再入队一次。在 a2a 1.x task 模式下,首个 FAILED 即终止流,第二个终止事件可能抛流已关闭错误或造成下游重复错误事件。建议终态已达成时跳过 else 分支的二次下发。

💡 Suggestion

总结

整体是一次从 a2a 0.3 到 1.x 的协议迁移,结构清晰、测试覆盖较全,无明确阻塞性安全或逻辑错误。主要风险集中在 v0.3 兼容路径的卡片拉取与文档表述不一致、metadata 合并的类型健壮性,以及错误路径下重复 FAILED 事件,建议合并前修复或验证。

测试建议

  • 补充“1.0 客户端 → 纯 v0.3 服务端”端到端用例:mock 服务端在 /.well-known/agent.json(而非 agent-card.json)返回 0.3 卡片,验证 _resolve_legacy_card 能否解析及 agent_base_url 直连 wire 是否可达。
  • 补充 metadata 合并用例:当既有 a2a_message.metadata 含非字符串/嵌套/数值类型(含 None)时,验证 _remote_a2a_agent.py:343-345 不抛异常且 round-trip 正确。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from 7f9bcd7 to 65d292c Compare August 14, 2026 10:16
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

现在我已经有了全面的了解。让我来确定最终的问题。

让我重新考虑一下 task_id 捕获问题,这可能是最重要的真正 Bug。在 _run_async_impl 第 398 行:

if task_id is None and hasattr(result, "task_id") and result.task_id:
    task_id = result.task_id

在 a2a-sdk 1.x 中,Task protobuf 消息使用的是 id 而不是 task_id。所以,对于服务器首先入队的初始 Task 事件(现在是第一个事件,而不是 TaskStatusUpdateEvent),task_id 将无法被捕获。但是后续的 TaskArtifactUpdateEvent / TaskStatusUpdateEvent 会携带 task_id,所以取消操作仍然有效。但有一个细微的情况:如果远程 Agent 只返回一个 Task 然后没有任何内容(立即完成),取消操作就无法捕获 task_id。这是一种边缘情况。让我看看 a2a 的 Task 是否公开了 task_id。考虑到 protobuf 会生成对应字段名的属性,Task 包含的是 id,而不是 task_id。所以 hasattr(result, "task_id") 对于 Task 来说是 False。

实际上,这是一个真正的退化风险。以前,第一个入队的事件是 submitted 状态的 TaskStatusUpdateEvent,它确实有 task_idtask_id 被立即捕获。现在第一个事件是 Task,它没有 task_id → 在第一个更新/制品(update/artifact)事件之前,task_id 保持为 None。在实践中,制品/更新事件会携带 task_id,所以取消操作依然有效。只有在罕见的只返回 Task 的情况下才会有风险。我会将其标记为警告。

让我再核实一下:Task 上是否有 context_id?它确实有。好的。

现在让我最终确定答案。让我再确认一下 test_task_result 测试是否检查了 task_id 的捕获——并没有,它只检查了事件数量。所以这个行为在测试中没被覆盖到。

再考虑一点:在 _resolve_legacy_card 中,如果原始请求成功,但既不是 v0.3 也不是 v1.0,那么会进入回退到 A2ACardResolver 的逻辑。但 A2ACardResolver.get_agent_card() 会获取 /.well-known/agent-card.json —— 这是第三次获取原始卡片(原始获取 + 解析器获取)。等等,原始获取获取的是同一个 URL,然后解析器再次获取它。所以总共是 2 次获取。测试 test_falls_back_to_v10_resolver_when_card_is_v10_layout 证实了解析器会重新获取。浪费但不是 Bug。

让我写出审查结果。我也要指出 set_metadata 的 Struct 更新可能无法正确处理嵌套的 dict 值 —— 文档字符串说是通过 ParseDict,但代码使用的是 metadata.update({key: value}),这是 Struct 的 Mapping 更新。Struct.update 可以通过 _struct_helpers 处理嵌套的 dict/list。在 protobuf 5.x 中这应该可以工作。测试 test_marks_matching_tool_ids 设置了一个 bool 类型的值 True 并且断言它能取回 True → 可以工作。所以没问题。

现在最终确认。让我重新考虑严重程度。该 PR 是对 a2a-sdk 0.3 → 1.0 的重大升级。根据可测试性,最具体的实际问题是:

  1. 取消时的 task_id 捕获:初始的 Task 没有 task_id 属性 → 第一个事件的 task_id 捕获退化(警告)。
  2. create_jsonrpc_routes(rpc_url=path):路径与 URL 语义 —— 没有集成测试验证挂载是否实际匹配(警告)。
  3. _resolve_legacy_card 中的双重卡片获取:浪费网络请求(建议)。
  4. create_a2a_application 在未初始化时崩溃:如果 agent_card 为 None → _jsonrpc_path_from_card(None) 崩溃且提示晦涩(建议/警告)。
  5. _compat 客户端的 CompatJsonRpcTransport 签名:未经测试的构造函数参数顺序 —— 但测试断言了 .url,所以部分验证了。跳过。

让我再重新考虑一下是否真的存在严重(Critical)问题。让我重新检查一下 convert_event_to_a2a_events 错误路径中的一个正确性问题。对于错误事件,现在它追加的是 error_event (TaskStatusUpdateEvent) 而不是 Message。但请注意:_create_error_status_event 同时设置了 metadata=event_metadata(一个普通字典)和 status.message=_status_message(...)。在执行器的 _handle_request 中,这个错误事件被追加并加入队列,同时 on_event=aggregator.process_event 也被通知了。聚合器将状态设置为 FAILED。然后在该循环之后,由于 aggregator.task_state == TASK_STATE_FAILED(而不是 WORKING),它会进入 else 分支:create_final_status_event(state=FAILED, message=aggregator.task_status_message)。所以另一个 FAILED 的最终事件被加入队列了。这是预期的吗?所以对于错误事件,你会得到:(1) 来自 convert_event_to_a2a_events 的错误 TaskStatusUpdateEvent (FAILED),然后 (2) 来自循环后处理的最终 FAILED 事件。有两个 FAILED 事件。这是多余的,但可能没问题(聚合器去重了状态)。以前代码也会发出消息 + 最终状态。不是退化,不是 Bug。

其实等等——这是在 for trpc_event in runner.run_async(...) 循环内部。错误事件是作为 trpc_event 出现的吗?带有 error_code 的事件是通过 run_async 产生的。所以是的。发出两个 FAILED 状态是可以接受的。OK。

让我最终确定。让我重新检查 #4 的严重程度 —— 如果 a2a_svc.agent_card is Nonecreate_a2a_application 会崩溃。文档说要先初始化。所有示例都调用了 initialize()。所以这是一个被记录的前提条件;对普通用户来说不是真正的 Bug。我会将其作为建议合并。

让我写出最终的审查报告。基于我对 pr.diff 的审查(这是对 a2a-sdk 0.3 → 1.0 的重大升级),以下是我的发现。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:398:首个 Task 事件无法捕获 task_id,取消能力退化

    • 改造后服务端首个事件由 submitted 状态事件改为 Taskexecutor/_a2a_agent_executor.py:229),但客户端仍用 hasattr(result, "task_id") and result.task_id 捕取取消用 id。a2a-sdk 1.x 的 Task proto 字段是 id 而非 task_idhasattr(Task, "task_id") 为 False,首轮 Task 不会贡献 task_id。正常流里后续 TaskArtifactUpdateEvent/TaskStatusUpdateEvent 携带 task_id 仍可兜底,但若远端只回一个 Task(如立即完成),cancel 时 task_id 仍为 None 无法取消。建议改为 result.task_id or getattr(result, "id", None),并补一个覆盖首轮 Task 捕获 task_id 的用例(当前 test_task_result 未断言此行为)。
    if task_id is None and hasattr(result, "task_id") and result.task_id:
        task_id = result.task_id
  • trpc_agent_sdk/server/a2a/_application.py:226-231create_jsonrpc_routesrpc_url 实参语义与挂载正确性缺少集成验证

    • 这里把由卡片 url 推导出的路径//a2a)作为 rpc_url=... 传入 create_jsonrpc_routes,而参数名为 rpc_url,存在 SDK 期望完整 url 而非路径的风险。现有测试只验证了 _jsonrpc_path_from_card 的返回值与 DefaultRequestHandler 的构造参数,没有真正装配 Starlette 并断言 JSON-RPC 路由挂载到了与卡片声明一致的路径。建议补一个用 TestClient/a2a/ 的端到端挂载路径断言,避免出现“卡片声明 A、实际挂 B”的静默错配。
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:228-261_resolve_legacy_card 在 v0.3 解析失败时会对同一卡片端点发起第二次 HTTP 请求

    • httpx.get("/.well-known/agent-card.json") 拿原始 JSON,解析 0.3 失败后又调 A2ACardResolver.get_agent_card() 再次请求同一 URL,纯 v0.3 卡片场景下多一次网络往返且失败时仍走 compat 回退。建议复用已获取的 raw 走 1.x pydantic 解析(或直接用 A2ACardResolver 一次),减少请求与超时风险。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/_application.py:220-232create_a2a_applicationa2a_svc.agent_card is None(未 initialize())时会崩溃。if a2a_svc.agent_card is not None 只保护了 _ensure_* 调用,而 create_agent_card_routes(None)_jsonrpc_path_from_card(None) 仍无条件执行,报错信息晦涩。建议显式校验并给出明确报错(“请先调用 a2a_svc.initialize()”)。

总结

整体是一次结构清晰的 0.3→1.0 协议升级,核心转换与兼容开关均有单测覆盖,未发现阻断性安全/数据错误。主要风险集中在取消链路的 task_id 捕获退化与 JSON-RPC 挂载路径缺少端到端验证两处,建议合并前补强。

测试建议

  • 补充客户端首轮收到 Task 事件时 task_id 被正确捕获(或显式从 Task.id 取值)的用例。
  • 补充 create_a2a_application 装配后用 TestClient 验证 JSON-RPC 路由挂载路径(//a2a)与卡片 supported_interfaces[].url 一致的端到端用例。

@bochencwx bochencwx closed this Aug 16, 2026
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.

2 participants