Skip to content

feat: 升级a2a协议版本到1.0 - #299

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

feat: 升级a2a协议版本到1.0#299
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 022fbfe to e7aba7b Compare August 16, 2026 08:28
@bochencwx bochencwx changed the title feat: 升级a2a协议到1.0 feat: 升级a2a协议版本到1.0 Aug 16, 2026
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我来检查一下 CompatJsonRpcTransport 的签名 —— 新代码调用了 CompatJsonRpcTransport(self._httpx_client, self._agent_card, url)。测试断言 agent._a2a_client._transport.url == "http://remote:8080"。所以 url 是一个属性。我需要验证构造函数是否按 (httpx_client, card, url) 的顺序接收位置参数。测试通过了这一假设(断言传输类型为 CompatJsonRpcTransporturl 匹配)。由于测试是在同一个 PR 中编写的,并且断言了 url,我姑且相信测试反映了 SDK 的 API。此外,BaseClient(card=..., config=..., transport=..., interceptors=[]) —— 关于构造函数参数,同样如此。

我来看一下 test_force_v0_3_uses_compat_wire 中关于 CompatJsonRpcTransport(self._httpx_client, self._agent_card, url) 顺序的测试断言 —— 该测试断言 URL 解析为 card 的 url (http://remote:8080)。因此在 _build_v0_3_a2a_client 中,url = self._first_jsonrpc_url() or self.agent_base_url。card 的 url 是 http://remote:8080,所以使用了该 url。很好。

现在有一个微妙的问题:在 _build_v0_3_a2a_client 中,BaseClient(card=self._agent_card or AgentCard(), ...) —— 如果在 force_v0_3 路径中 card 为 None。但 force_v0_3 要求 card 必须存在(发现失败则返回 False)。因此 card 不为 None。没问题。

我现在掌握的信息足够了。我还要验证一下 _first_jsonrpc_url —— 在填充 url 后,它返回的是第一个 JSONRPC url。在 _build_v0_3_a2a_client 中,url = self._first_jsonrpc_url() or self.agent_base_url。但 _fill_empty_jsonrpc_urls 已经在 _build_a2a_client 中被调用过了。很好。

还有一点:_fill_empty_jsonrpc_urls 会原地修改 self._agent_card.supported_interfaces —— 修改发现的 card(一个共享对象)。没问题。

现在我来定稿审查意见。我也要确认一下关于在 application 中 agent_card 为 None 的顾虑是否值得注意 —— 我会将其标记为 Warning(健壮性/文档契约)。并且 _capabilities_with_trpc_extension 的原地修改也标记为 Warning。

我再重新考虑一下针对 Struct 执行 request_meta.update(a2a_message.metadata) 的顾虑。鉴于日志测试确认 Struct 暴露了类似 mapping 的 .update(dict)、迭代和包含检查,proto-plus Struct 很可能也暴露了 keys(),从而使 dict.update(struct) 能够正常工作。所以我不会标记它。

现在我来撰写审查意见。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_application.py:90-96a2a_svc.agent_cardNone 时未保护,导致装配崩溃

    • if a2a_svc.agent_card is not None 仅保护了 _ensure_card_has_url/_ensure_v0_3_interface(第85行),但 create_agent_card_routes(a2a_svc.agent_card)_jsonrpc_path_from_card(...) 以及第80-83行构造 DefaultRequestHandler(agent_card=...) 都在无保护下直接使用 agent_card。若调用方未先 svc.initialize()(卡片尚未构建),这些调用会在装配阶段抛异常而非给出清晰错误。建议在函数入口对 a2a_svc.agent_card is None 显式校验并抛出带提示的异常,或在文档/类型上强制要求已初始化。
  • trpc_agent_sdk/server/a2a/_agent_card_builder.py:117-126_capabilities_with_trpc_extension 改为就地修改入参,破坏原非破坏性契约

    • 旧实现用 model_copy(update=...) 不修改调用方传入的 capabilities;新实现直接 base.extensions.append(...),会修改调用方传入的 AgentCapabilities 实例。当前内部路径 (_agent_service.py:110) 总是新建 capabilities,影响有限;但 capabilitiesAgentCardBuilder 的公开入参,外部复用同一实例多次构建时会累积重复扩展或污染调用方对象。建议恢复拷贝语义(base.model_copy() 后再 append,或重新走 model_copy(update=...))。测试 (test_agent_card_builder.py:165-171) 只校验结果,未断言入参未被修改,故该回归未被覆盖。
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:343:从 Task 响应中无法捕获 task_id,影响取消

    • 取消依赖 hasattr(result, "task_id") and result.task_id,但 a2a 1.x 的 Task 字段是 id(见 _a2a_agent_executor.py:231 构造 Task(id=...)),没有 task_id,故初始 Task 事件无法捕获 task_id。1.x task 模式要求首个事件为 Task,后续若没有携带 task_idTaskStatusUpdateEvent/TaskArtifactUpdateEvent,则取消请求无法发送(落入第370行 "No task_id captured" 分支)。建议对 Task 结果用 result.id 兜底捕获 task_id

💡 Suggestion

  • tests/server/a2a/executor/test_a2a_agent_executor.py:153-165_get_user_session_from_task_metadata 的测试用 dict 而非真实 Struct
    • 生产路径 current_task.metadata 在 1.x 是 protobuf Struct,新代码经 _metadata_to_dict 转换;但测试直接喂 plain dict,只覆盖了 _metadata_to_dict 的 dict 分支,未验证 MessageToDict(Struct) 分支。建议改用 struct_pb2.Struct 构造,覆盖真实序列化路径。

总结

整体风险中等:1.x 协议升级改动覆盖较完整、测试较充分,但存在 create_a2a_application 对未初始化 service 缺少保护、_capabilities_with_trpc_extension 就地修改入参破坏原有契约、以及 Task 响应无法捕获 task_id 三个值得修复的问题;均为非阻塞性但建议在合并前处理。

测试建议

  • 补充 create_a2a_applicationagent_card is None(未 initialize)场景下的行为测试,明确是抛清晰异常还是延迟到首次请求。
  • 补充 _run_async_impl 流式路径下、远端只返回单个 Task(无后续 status/artifact 事件)时 task_id 捕获与取消请求的测试。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

基于对完整 diff 和周边代码的审查,我的发现如下。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:339:流式首事件为 Task 时无法捕获 task_id,导致早期取消失效

    • 1.x 服务端按规范首个事件必须是 Task(见 _a2a_agent_executor.py 改动),而取消路径仍用 hasattr(result, "task_id") and result.task_id 捕取 id;Task 消息只有 id 字段、没有 task_id,因此首个 Task 响应不会设置 task_id。若客户端在收到后续 TaskStatusUpdateEvent/TaskArtifactUpdateEvent 之前就取消(这两个事件才带 task_id),task_id 仍为 None,取消分支被跳过,远端任务泄漏。0.3 时首事件是带 task_id 的 submitted status event,不存在该问题——属本次迁移引入的回归。建议在 _response_payload 后对 Task 单独取 result.id 作为 task_id
  • pyproject.toml:80pyproject.toml:159requirements-test.txt:48a2a-sdk 版本约束由 <1.0.0,>=0.3.22 改为 >=1.0.0 且无上界

    • 0.3→1.0 是架构重写(类型从 pydantic 改为 protobuf、A2AStarletteApplication 删除、Part oneof 化等),本 PR 大量代码强依赖 1.x 的具体 API(HasFieldStreamResponse oneof、CompatJsonRpcTransportcreate_client)。不加上界会使未来任意 1.x/2.x 升级直接打破这些内部调用点。建议收紧为 >=1.0.0,<2.0.0(并固定到一个已验证的 1.x 次版本更稳)。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/converters/_event_converter.py:497-565create_cancellation_event / create_exception_status_event / create_submitted_status_event 等仍保留 final 形参但完全不再序列化
    • 1.x 的 TaskStatusUpdateEvent 已无 final 字段,参数仅为向后兼容保留并被静默丢弃;当前仓库内已无调用方传入 final。可在注释中明确"已废弃、不生效",或后续清理移除,避免调用方误以为仍能控制流结束语义。

总结

整体迁移到 a2a-sdk 1.x 的改动结构清晰、测试覆盖较全(application 装配、v0.3 兼容、part/event 转换、aggregator 不变性等均有针对性用例)。存在一处由协议语义变化引入的取消时序回归(首事件 Task 不带 task_id),以及依赖上界缺失;前者建议修复,后者建议加约束。

测试建议

  • 补充一个用例:服务端先发 Task(仅 id)再发 update 事件前,客户端触发取消,验证 cancel_taskTask.id 被正确调用(当前会在 task_id is None 分支跳过)。
  • 补充 rpc_url 带路径(如 https://x/a2a)时 create_a2a_application 挂载路径与卡片 supported_interfaces[].url 一致性的端到端断言(单元层已有 _jsonrpc_path_from_card 覆盖,但未验证实际路由挂载点)。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from e7aba7b to 8b05c56 Compare August 16, 2026 09:16
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.99085% with 35 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
...ent_sdk/server/a2a/executor/_a2a_agent_executor.py 61.70213% 18 Missing ⚠️
trpc_agent_sdk/server/a2a/logs/_log_utils.py 84.61538% 14 Missing ⚠️
...gent_sdk/server/a2a/converters/_event_converter.py 95.45455% 2 Missing ⚠️
trpc_agent_sdk/server/a2a/_application.py 97.87234% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #299   +/-   ##
==========================================
  Coverage        ?   88.70572%           
==========================================
  Files           ?         495           
  Lines           ?       47254           
  Branches        ?           0           
==========================================
  Hits            ?       41917           
  Misses          ?        5337           
  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.

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from 8b05c56 to 9db8d72 Compare August 16, 2026 09:20
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

现在我来整理评审结论。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/logs/_log_utils.py:197-201:错误响应日志分支被删除,错误响应被记为 SUCCESS

    • 旧代码通过 hasattr(resp.root, "error") 单独处理 JSON-RPC 错误响应并打印 error code/message/data;新代码只判断 task/message,对错误响应 resultNone,最终输出固定 Type: SUCCESS + No result,错误码与错误信息完全丢失,会误导排障。建议补充 error 分支(按 a2a-sdk 1.x 的 SendMessageResponse oneof 字段判断),并补一条错误响应测试。
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:205-214_fill_empty_jsonrpc_urls 直接就地修改传入的 agent_card

    • 该方法遍历 self._agent_card.supported_interfaces 并直接写回 interface.url,而 _agent_card 是构造时传入的对象引用。若调用方将同一个 AgentCard 复用给多个 TrpcRemoteA2aAgent 或在其他地方持有该卡片,填充会污染共享对象、且不同实例的 agent_base_url 会互相覆盖。建议在填充前对卡片/接口做拷贝,或仅在客户端内部维护一份派生 url 而不改写原卡。
  • tests/server/a2a/test_utils.py:1-120_utilsStruct 分支无测试覆盖

    • set_metadata/get_metadata/metadata_is_true 的 docstring 明确声明支持 google.protobuf.Struct(这是本 PR 1.x 化的核心),但测试只覆盖 dict 路径,Struct 路径(Struct.updatekey in structstruct[key] 的 round-trip 行为)完全未验证。建议补 Struct 路径用例,避免 1.x 元数据读写回归未被发现。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/_application.py:152-156_jsonrpc_path_from_card 对仅含 path 以外成分(如带 query/fragment)的 url 直接取 urlparse(...).path,未处理 path 为空但 url 非空(如 http://host?q=1)的退化情况,当前会回退到 /。实际业务影响小,但可显式归一化以避免歧义。

总结

本次 PR 主要是 a2a-sdk 0.3→1.0 的协议升级与 create_a2a_application 便利层引入,核心转换/执行器/远程客户端逻辑均有对应测试覆盖,未发现明确的阻断性正确性或安全问题。存在 2 处建议修复项(错误响应日志丢失、共享 AgentCard 被就地修改)和 1 处测试覆盖缺口,均不影响主路径启动但可能在排障/多实例复用场景下产生隐患。

测试建议

  • build_a2a_response_log 补充错误响应(JSON-RPC error)用例,验证错误码/错误信息仍能被记录。
  • _utilsset_metadata/get_metadata/metadata_is_true 补充 google.protobuf.Struct 路径用例,覆盖写入与读取 round-trip。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from 9db8d72 to 6518d67 Compare August 16, 2026 10:10
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

_get_user_session_from_task_metadata 使用了 _metadata_to_dict,能够正确处理 Struct 类型。很好。

现在让我重新考虑一个真正的问题:在执行器的 execute 方法中,入队的 Task 使用了 history=[context.message]。但在 a2a-sdk 1.x 中,Task 随后接收 TaskStatusUpdateEvent。然而,问题在于 第一次 入队的是一个带有 TASK_STATE_SUBMITTEDTask,然后 _handle_request 会入队一个 working 事件。但是工作事件包含了 working_meta(app_name/user_id/session_id)。在 1.x 中,DefaultRequestHandler 会持久化任务。让我检查一下这些元数据是否存储在任务上。实际上,这位于 SDK 内部,不在本次 diff 范围内。

现在我已经完成了全面评估。这次迁移看起来很稳健,测试覆盖率也很全面。让我确认一下是否还有遗漏的严重问题——特别是检查 convert_a2a_part_to_genai_part 中对原始字节的处理:data=a2a_part.raw。之前代码是 base64.b64decode(part.file.bytes)。现在 a2a_part.raw 是原始字节(_genai_inline_file_to_a2a 中的 part.inline_data.data 是字节,而不是 base64)。所以往返过程是一致的:写入原始字节,读取原始字节。很好——没有 base64 不匹配的问题。

再确认一下:_genai_inline_file_to_a2a 设置了 raw=part.inline_data.data(字节)。而 convert_a2a_part_to_genai_part 读取 a2a_part.raw 作为 data=a2a_part.raw(字节)存入 Blob.data。是一致的。很好。

现在让我核实一下 _part_converter 中的 _a2a_data_to_dict 是否处理了 data 为 Value 包装的列表/标量的情况——MessageToDict(Value) 可能会返回非字典类型。在 _convert_a2a_data_part 中,data = _a2a_data_to_dict(...) 然后是 converter(data)。转换器期望的是一个字典(例如 _function_call_data_for_a2a 会读取 raw.get(...))。如果 data 是标量 ValueMessageToDict 将返回该标量,此时 converter(scalar) 会失败。但 data-part 的元数据类型(function_call 等)始终是结构化的(字典)。对于未知类型,会执行 json.dumps(data) 回退逻辑。没问题。

我已经完成了审查。现在开始撰写评审意见。

发现的问题

🚨 Critical

未发现 Critical 级别问题。

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_application.py:96-97_ensure_v0_3_interfacea2a_svc.agent_card原地修改(向 supported_interfaces 追加 0.3 接口),会永久改变服务持有的 card。若同一 TrpcA2aAgentService 被多次 create_a2a_application 或 card 在别处被复用,0.3 接口会残留;当前虽有去重 guard 保证幂等,但语义上不应让“装配应用”这一动作反向修改业务对象。建议对 card 做一份 CopyFrom 副本后再追加接口,或显式说明此副作用。

  • trpc_agent_sdk/server/a2a/_application.py:100-105create_jsonrpc_routesrpc_url 传入的是从 card url 解析出的路径(如 /a2a)。当 deployer 把 rpc_url 配成带路径的地址时,JSON-RPC 端点挂载到该子路径,而 /.well-known/agent-card.json 仍挂载在根;这本身正确,但若 rpc_url 路径带尾斜杠(/a2a/),路由会以 /a2a/ 挂载,客户端 POST /a2a(无尾斜杠)会 404,存在路径匹配不一致风险。建议规范化掉尾斜杠或在文档/校验中提示。

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:292-295request_meta.update(a2a_message.metadata) 用 protobuf Struct 去更新一个普通 dict,会把 Value 包装对象当作 value 存进 dict,再经 a2a_message.metadata.update(request_meta) 合并回 Struct。当前测试 (test_merges_existing_message_metadata) 验证了简单字符串值可往返,但该写法依赖 Struct 的 mapping 协议对嵌套/非标量值的隐式转换,属于脆弱实现。建议显式用 MessageToDict/set_metadata 归一后再合并,避免复杂 metadata 类型下的数据畸变。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/converters/_event_converter.py:483-492create_*_status_event 系列仍保留 final 形参但完全忽略,全仓已无调用方传 final=(见全局搜索)。保留形参仅为兼容但会让调用方误以为 final 仍生效,建议在参数上加 deprecate 注释或直接移除以减少误导。

总结

整体是从 a2a-sdk 0.3 到 1.x(protobuf 化)的协议升级迁移,核心路径(Part oneof、Struct metadata、Task-first 流式、v0.3 兼容)均有对应单测覆盖且逻辑自洽,未发现会导致核心功能失败的 Critical 问题。主要风险集中在 _application.py 对 card 的原地修改、JSON-RPC 挂载路径与尾斜杠一致性,以及 remote agent 中 dict/Struct 混合 update 的脆弱性,建议合入前处理上述 Warning。

测试建议

  • 补充 create_a2a_applicationrpc_url 带尾斜杠路径(如 http://h/a2a/)时,客户端以无尾斜杠路径 POST 的路由匹配用例,确认是否 404。
  • 补充 _run_async_impla2a_message.metadata 含嵌套结构(list/dict)时 request_meta.update(...) 的往返断言,验证非标量 metadata 不被畸变。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from 6518d67 to d074129 Compare August 16, 2026 10:29
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经完成了全面审查,让我来总结一下发现的问题。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:537-544:空 StreamResponse 会产生伪造的“未知响应类型”事件

    • _response_payload 在 oneof 未设置时 return response(返回 StreamResponse 本身),随后 _events_from_response 落入 else 分支,向下游 yield 一个内容为 Received unknown response type: StreamResponse 的 Event。a2a-sdk 1.x 流中若出现未选 oneof 的帧(如协议 keepalive 或 SDK 边界异常),会被当成正常内容透传给业务,污染 Runner 事件流。建议在 _response_payload 返回 None 时让调用方跳过该帧(continue)而不是当作未知类型生成事件。
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:293-295:合并已有 metadata 时 MessageToDict 可能丢失 None/默认值键

    • request_meta.update(MessageToDict(a2a_message.metadata)) 用默认参数序列化 protobuf StructNullValue 等值在不同 MessageToDict 版本下可能被省略,导致调用方原本设置的 metadata 键在合并后被丢掉、无法透传到对端。建议显式 MessageToDict(..., preserving_proto_field_name=True) 或直接用 Struct 原地 update(如 _build_messagemessage.metadata.update(msg_meta) 的写法),避免 dict 往返丢字段。

💡 Suggestion

总结

整体是一次质量较高、测试覆盖充分的 a2a-sdk 0.3→1.0 协议迁移,核心转换/装配/兼容路径均有对应单测。未发现必须修复的阻塞问题;两处 Warning 分别是流式空帧被误当作业务内容、以及 metadata 合并时 protobuf 序列化可能丢键的边界风险,建议在合入前处理。

测试建议

  • 补充一个 StreamResponse()(oneof 未设置)流经 _run_async_impl 的用例,断言其不产生任何业务 Event(覆盖上面第一条 Warning 的修复)。
  • 补充一个含 None 值 metadata 的 Message_build_outgoing_message 合并后,request.message.metadata 仍保留该键的用例。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from d074129 to fb15c45 Compare August 16, 2026 13:55
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

test_cancel_uses_id_from_initial_task 中,流产生一个 Task(状态为 SUBMITTED,无消息,无历史记录)。然后 _events_from_response(task)convert_a2a_task_to_event:无制品,状态无消息,无历史记录 → 返回一个空的 Event(author=...)。因此它产生了一个空事件(而非用户消息回显)。在该测试中,任务没有历史记录。

但是生产环境中的执行器将 Task 入队时使用了 history=[context.message]。因此,当远程客户端收到该 Task 时,convert_a2a_task_to_event 会回退到 history[-1](用户消息)并发出一个用户内容的事件。这个事件具有 author=self.name(远程代理名称),但内容是用户的消息。这可能会导致远程代理向会话回显用户的输入。

然而,这是一个与服务端发往客户端的 Task 事件相关的行为。服务端包含历史记录是刻意为之吗?历史记录字段用于携带上下文。convert_a2a_task_to_event 使用历史记录的最后一项作为消息。如果历史记录=[用户消息],它会发出用户消息。这似乎是预期的行为,即在初始 Task 中回显用户消息——但通常远程代理不应该重新发出用户消息。嗯。

但是等等——这在 0.3 版本中可能已经是行为了(之前提交的状态事件包含了消息)。我无法轻易确认这是否是一个回归问题。鉴于测试 test_cancel_uses_id_from_initial_task 产生了一个空事件(无历史记录的 Task),且生产环境执行器的 Task 包含历史记录,存在潜在的不一致。但这很微妙,可能是有意的(历史记录往返)。我将其标记为低置信度的警告——但指令要求不要猜测。由于我无法确认这是一个 Bug(可能是设计使然,且之前可能也是这样),我将跳过它。

让我重新考虑实质性问题。我相当有信心的一个具体问题是:_fill_empty_jsonrpc_urls 即便在没有 JSONRPC 接口需要填充时也会进行复制(浪费资源,且在不必要的情况下重新赋值 _agent_card)。这很小,不值得标记。

让我重新考虑安全角度:_jsonrpc_path_from_card 使用来自 agent cardurlparse(advertised).path —— agent card 是由部署者控制的 rpc_url,而不是不可信的用户输入。没有路径遍历风险(Starlette 路由)。没有安全问题。

让我重新考虑 _remote_a2a_agent 中的 tenant="" —— 硬编码了空 tenantSendMessageRequest(tenant="", ...)。这很可能是设计使然。

现在让我最后检查一件事 —— 关于 Messagemetadata 使用字典构造函数与 update 的问题。在 _build_message_from_artifact_event 中,Message(role=..., parts=..., message_id=...) 创建了一个默认 metadata(空 Struct)的消息,然后 msg.metadata.update(event.metadata)。很好。

我认为迁移是扎实的,测试也做得很好。让我确定要报告的实际问题。

让我重新审视一个我想要确认的真正正确性问题:在 _event_converter.pyconvert_event_to_a2a_events 中,修改后追加了 error_event(整个状态事件),而不是 error_event.status.message。但随后它也追加了 message = convert_event_to_a2a_message(event, ...)。因此,对于带有 error_code 的错误事件,它会追加:error_event (TaskStatusUpdateEvent),然后是 message (Message)。注释说 task-mode 禁止在初始 Task 之后出现裸的 Message。但是,这里在 TaskStatusUpdateEvent 之后追加了 Message —— 在 task-mode 下允许这样做吗?1.x 的 task-mode 规则:第一个事件必须是 Task,然后是 TaskStatusUpdateEvent/TaskArtifactUpdateEventMessage 通常是不允许的……但是 convert_event_to_a2a_message 在这里被无条件地用于非错误部分。等等——这个 message 追加在错误分支和正常分支中都发生了。让我重读一下:

if event.error_code:
    error_event = _create_error_status_event(...)
    _notify(error_event)
    if error_event.status.HasField("message"):
        a2a_events.append(error_event)   # was: append(error_event.status.message)

message = convert_event_to_a2a_message(event, invocation_context)
if message:
    a2a_events.append(message)

所以对于正常的(非错误)事件,它总是追加一个 Message。对于 task-mode 流式传输,在初始 Task 之后追加一个裸 Message 将是无效的。但也许此路径用于非 task-mode(事件模式)?执行器调用 convert_event_to_a2a_events 并将结果入队。执行器是 task-mode 的。因此,为正常事件追加 Message 会违反 task-mode。但这似乎是预先存在的行为(之前也追加了 message)。注释只更改了错误分支。所以正常事件 Message 的追加是预先存在的,大概没问题,因为 a2a-sdk 接受 task 流中的 Message 事件(或过滤器)。不是此 diff 引入的回归问题。

我很有信心。让我写下最终审查报告。我有几个低严重性的发现;让我确保它们是真实且值得提及的。

真正的发现:

  1. _application.py enable_v0_3_compat + 自定义 request_handler 不一致:修改后的卡片(带有 v0.3 接口)用于卡片路由和路径推导,但没有传递给自定义 request_handler,因此处理程序的内部卡片缺少 v0.3 接口 → 代理卡片广告 v0.3,但处理程序可能会拒绝 0.3 请求。警告(高级用法,可能属于小问题)。实际上这可能是一个真正的正确性差距。但 request_handler 是“完全控制权”——用户有责任。文档注明了这一点。我将其标记为建议/警告。

  2. _remote_a2a_agent.py 文档不匹配force_v0_3 优先使用卡片 JSONRPC url 而非 agent_base_url,与 README 声明相矛盾。文档轻微不准确。建议。

  3. 测试覆盖率差距_build_message_from_artifact_eventStruct-to-Struct 更新确实已被覆盖。_application.py 自定义处理程序 + compat 组合未测试。建议。

老实说,这些都不严重。让我重新考虑是否还有我遗漏的真正 Bug。

让我重新检查 _part_converter 中的 convert_a2a_part_to_genai_parturl 的处理:genai_types.Part(file_data=genai_types.FileData(file_uri=a2a_part.url, mime_type=a2a_part.media_type))。在 1.x 中,Part 可能使用 file_uriurl。测试断言 result.url == "gs://b/f"result.media_type == "image/png",确认字段名是 urlmedia_type。往返转换使用 a2a_part.url/a2a_part.media_type。一致。

我认为迁移实现得很好。让我写一份简洁的审查报告,重点关注可验证的发现。鉴于质量很高,我将报告少量的警告/建议。

让我再仔细检查一个潜在问题:_metadata_to_dict(在 _event_converter 和执行器中)通过引用返回字典(if isinstance(metadata, dict): return metadata)。在执行器的 _get_user_session_from_task_metadata 中,它对 Struct 调用 _metadata_to_dict,返回一个新的字典。没问题。对于字典输入,按引用返回。没问题。

让我重新考虑 set_metadataStructmetadata.update({key: value}),其中 value 可能是嵌套字典(例如 {"video_metadata": {...}}{"version": ...})。对于 Structupdate({"thought": True}) —— Struct.update 通过 ParseDict 处理嵌套值。没问题。

最终答案。我将保持简洁,并标记出真正的问题,且不夸大其词。基于我对 pr.diff(a2a-sdk 0.3 → 1.0 protobuf 迁移,加上新的 create_a2a_application 便利层和 force_v0_3 客户端路径)的审查,并辅以相关源码和测试上下文,我的结论如下。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_application.py:90-102:自定义 request_handler + enable_v0_3_compat 存在卡片不一致
    • 开启 compat 时,框架拷贝卡片并 _ensure_v0_3_interface(card) 追加 0.3 接口,但该 card 只用于 create_agent_card_routes 和路径推导,并没有传给用户传入的 request_handler。结果对外 agent-card 路由声明了 0.3 接口,而 handler 内部卡片仍是 1.0,0.3 客户端按卡片发现后调用可能在 handler 侧被拒绝。建议在自定义 handler 路径下要么也把补全后的 card 传给 handler,要么明确文档约束该组合由调用方自行保证。现有测试仅覆盖了默认 handler 的 compat 路径,未覆盖此组合。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:175-180force_v0_3 实际优先用卡片 JSONRPC url,与文档描述不一致
    • _build_v0_3_a2a_clienturl = self._first_jsonrpc_url() or self.agent_base_url,即卡片有 url 时走卡片 url 而非 agent_base_url;但 README.md/a2a.md 写的是 “force_v0_3=True: CompatJsonRpcTransport to agent_base_url”。代码行为更合理,建议把文档措辞改为“优先卡片 JSONRPC url,缺失时回退 agent_base_url”,避免误用。不影响正确性。

总结

这是一次质量较高的 0.3→1.0 protobuf 迁移,核心转换器、executor 的 task-mode 流式约束、remote agent 的 StreamResponse oneof 解析与 v0.3 兼容路径均有对应单测覆盖,未发现阻塞级正确性或安全问题。唯一值得修的是自定义 request_handlerenable_v0_3_compat 组合下的卡片一致性缺口。

测试建议

  • 补一条 create_a2a_application(svc, request_handler=custom, enable_v0_3_compat=True) 的测试,断言 handler 收到的卡片与对外 agent-card 路由声明的接口集合一致(或显式记录该组合的行为契约)。


- API:`http://127.0.0.1:18082`
- Agent Card:`http://127.0.0.1:18082/.well-known/agent.json`
- Agent Card:`http://127.0.0.1:18082/.well-known/agent-card.json`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

新版本没兼容获取agent-card的方式吗?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

新版本没兼容获取agent-card的方式吗?

旧版本服务端兼容了 agent.json,客户端没有。对齐了

这个文档本身写得有点问题

service_name="weather_agent_cancel_service",
agent=root_agent,
# Public address advertised in the agent card; clients call this url.
rpc_url=f"http://{HOST}:{PORT}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

为啥要加这个字段呢,之前AgentCard好像是默认注入了服务端serving的IP:PORT

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

为啥要加这个字段呢,之前AgentCard好像是默认注入了服务端serving的IP:PORT

之前的版本没有注入 url,暴露的AgentCard的url都是空的。这里加一个给用户设置的参数

)
_ensure_card_has_url(card)
routes: list[Any] = []
routes.extend(create_agent_card_routes(card))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

这里有多个Agent注册的需求,可以考虑下怎么支持

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

这个routes需要给用户自己调吗?看起来这个接口有点高度封装了

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

这个routes需要给用户自己调吗?看起来这个接口有点高度封装了

旧版本SDK内部有自动注册路由,用户默认不用配置。新版本SDK内部没有自动注册的逻辑,需要框架或者用户自己注册,这里做了一个封装让用户可以直接用。用户也可以不用框架提供的 create_a2_application接口,自定义即可

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from fb15c45 to 6e55319 Compare August 18, 2026 02:58
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

目前没有针对 Struct-to-Struct 更新的测试。但我相当确信 protobuf Struct.update 可以接受另一个 Struct(因为它支持映射协议)。这不是 Bug。

让我最后确定一下。这个 PR 很稳健。我将报告一个真正的 Warning 和一个 Suggestion。让我重新考虑一下 _a2a_part_requests_euc_auth 问题是否值得标记。实际上它是真实的:对于每个文本/url/raw 部分,都会在默认的空 Value 上调用 MessageToDict。让我验证一下这是否真的不会崩溃。MessageToDict(Value()) 的行为:实际上,对于没有显式设置 kind 的 Value(默认 null_value=0),MessageToDict 会返回 ""(空字符串)—— 这是因为 a2a/protobuf 将 null Value 序列化为空。嗯,实际上我记得 MessageToDict 会为 null Value 返回 None。无论哪种情况,短路机制都能防止崩溃。

但是等等——这里其实有一个更隐蔽的问题。_metadata_to_dict(part.data) 被无条件调用。如果 MessageToDict 因为某些意外的 proto 状态而引发异常怎么办?它不会引发异常。没问题。

我思考得够多了。这个 PR 很干净。让我写出评审意见,包含一个真正的 Warning(文档字符串不匹配 + 不必要的工作属于可维护性问题)。实际上,让我重新考虑一下是否存在关于 force_v0_3 + _response_payload 以及流产生非 protobuf 响应的真正问题。让我更仔细地检查一下兼容传输(compat transport)—— README 中说 send_message 返回 StreamResponse(oneof)。所以我将相信这一点。

让我写下最终结论。鉴于该 PR 质量很高且测试覆盖率全面,且没有明确的严重/警告问题,我将如实报告。让我明确地重新验证一个潜在问题:_ensure_card_has_url 是否在 request_handler 参数提供了自定义处理器(带有不同的 card)之后运行,并对(可能是已打补丁的)card 发出警告。如果自定义处理器的 card 有 URL,但服务 card 的 URL 为空,它会错误地发出警告。但这只是外观问题(日志警告)。属于 Suggestion 级别。

让我完成最终评审。我已完整审查了 pr.diff(a2a-sdk 0.3 → 1.0 协议升级),重点阅读了 _application.py_remote_a2a_agent.py_agent_card_builder.py、converters、executor、aggregator、log_utils 及对应测试。下面是结论。

发现的问题

🚨 Critical

未发现 Critical 问题。

⚠️ Warning

  • trpc_agent_sdk/server/a2a/converters/_event_converter.py:618_a2a_part_requests_euc_auth 对每个 part 无条件调用 _metadata_to_dict(part.data)

    • 该函数在 _create_status_update_event 中对 message.parts每个 part 调用,包括 text/url/raw part。对非 data part,part.data 是 protobuf 默认空 ValueMessageToDict(Value()) 会返回 None/"";当前仅因后续 and 短路(t == FUNCTION_CALL 为 False)才未触发 data.get("name")AttributeError,逻辑脆弱且对每个文本分片都做一次无谓的 MessageToDict 序列化。建议先用 part.HasField("data") 守卫,与同文件 _mark_long_running_tools_event_converter.py:245)保持一致,既消除脆弱性也避免热路径上的无谓序列化。
    • t = get_metadata(md, A2A_DATA_PART_METADATA_TYPE_KEY)
      data = _metadata_to_dict(part.data)  # 非 data part 时为 None/"",靠后续短路才不崩
      return (t == ... and ... and data.get("name") == ...)
  • trpc_agent_sdk/server/a2a/executor/_task_result_aggregator.py:50-66:聚合器不再将中间事件状态改写为 working

    • 旧实现会把每个中间 TaskStatusUpdateEvent.status.state 改写为 working,注释明确说明“other state may terminate the event aggregation in a2a request handler”。新实现遵循 1.x 共享 protobuf 不可变约定,保留事件真实状态(input_required/auth_required)。这是有意改动且有测试覆盖,但行为依赖 a2a-sdk 1.x DefaultRequestHandler 不会因非 working 状态提前终止队列消费;若 SDK 在收到 input_required/auth_required 后即停止聚合,会导致后续 artifact/completed 事件丢失。建议补充一个端到端(真实 EventQueue + DefaultRequestHandler)测试确认非 working 中间状态不会截断流,以覆盖该回归风险。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/_utils.py:40-49set_metadata 的 docstring 与实现不符
    • docstring 声称 Struct 分支“value is converted via ParseDict”,实际代码用的是 metadata.update({key: value})。功能正确(Struct.update 会递归转换 dict/list/scalar,已有 test_nested_dict_round_trip 验证),但注释会误导维护者。建议将注释改为说明 Struct.update 接受 dict 并递归转换,避免后续维护者误加 ParseDict

总结

整体是一次结构清晰、测试覆盖充分(application/remote agent/executor/aggregator/converters 均有针对 1.x proto 行为的新测试)的 a2a-sdk 1.0 迁移,未发现安全漏洞或明确逻辑错误。仅有两处值得修复:_a2a_part_requests_euc_auth 对非 data part 的脆弱/无谓序列化,以及聚合器不再改写状态后对 SDK 终止行为缺少端到端验证。

测试建议

  • 补充一个端到端测试:通过真实 DefaultRequestHandler + EventQueue 消费包含 input_required/auth_required 中间状态的流,确认聚合器保留真实状态后后续 artifact/completed 事件不被 SDK 提前截断。
  • _a2a_part_requests_euc_auth 补一个 text/url part 输入用例,确认不会因 part.data 默认值导致异常或无谓序列化。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from 6e55319 to 935ed8e Compare August 18, 2026 06:24
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经对本次 PR 有了全面的理解。下面我来整理最终的审查结论。

发现的问题

🚨 Critical

  • tests/server/a2a/executor/test_request_handler_stream_contract.py:160:契约测试函数缺少 pytest.mark.asyncio 标记,asyncio_mode = "auto"(pyproject.toml:217)下顶层模块级 @pytest.mark.parametrize 装饰的 async def 用例虽可被自动收集,但该测试构造的 SendMessageRequest 缺少 SendMessageRequest 在 1.x 通常必填的 tenant 等字段(对比 test_remote_a2a_agent.py 和生产代码 _remote_a2a_agent.py:302 都显式传 tenant="")。
    • 该 contract test 直接 SendMessageRequest(message=...) 不传 tenant,若 1.x SendMessageRequesttenant 为 required 字段,会在运行期抛 ValidationError/ValueError 而非真正验证流不被截断的契约,使测试给出假阳性结论。建议显式传 tenant="" 与生产代码保持一致。
    params = SendMessageRequest(
        message=Message(...),
    )

⚠️ Warning

  • trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py:269-283(及 :255-268 RunLimitException 分支):异常路径下 create_exception_status_event 的失败事件仅入队,但未通过 TaskResultAggregator 聚合。

    • 正常路径走 convert_event_to_a2a_events(..., on_event=aggregator.process_event) 让聚合器记录 failed/auth_required 等终态;但异常分支直接 enqueue_event(create_exception_status_event(...)),绕过了 aggregator.process_event。随后 _handle_request 末尾的 if aggregator.task_state == TASK_STATE_WORKING ... 判定会因聚合器仍是 working 而再追加一个 last_chunk=True 的 artifact + create_completed_status_event:366-384),即在 failed 状态事件之后又追加 completed,造成“先 failed 后 completed”的矛盾终态序列。建议异常分支也调用 aggregator.process_event(...),或调整末尾收尾逻辑在已发出终态失败事件时跳过 completed 收尾。
  • trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py:366-384:正常结束(task_state == WORKING)时追加 last_chunk=TrueTaskArtifactUpdateEvent 再追加 create_completed_status_event,但当本次 run 全程未产生任何内容事件(aggregator.task_status_message 为 None 或 parts 为空)时会走 else 分支 create_final_status_event(state=WORKING, message=None)

    • 1.x 任务流要求最终状态为终态(completed/failed/canceled/input_required/auth_required)。在“运行结束但无内容”的情况下用 WORKING 作为最终 TaskStatusUpdateEvent 的 state,可能不是合法终态,导致 DefaultRequestHandler 认为任务未结束。建议无内容收尾时也用 completed 终态(或确认 SDK 接受 working 作为最终事件)。
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:292-294:outgoing message metadata 合并改为“只补缺失键”,改变了覆盖语义。

    missing = {k: v for k, v in request_meta.items() if k not in a2a_message.metadata}
    if missing:
        a2a_message.metadata.update(missing)
    • build_request_message_metadata 会写入 user_id 等键(来自 ctx.user_id)。新逻辑下,若调用方在 a2a_message.metadata 中已存在同名字段(例如业务自定义的 user_id),框架不再用 ctx.user_id 覆盖。原实现是 request_meta.update(existing)(业务优先),新实现改为“框架键不被业务覆盖”语义相反。test_merges_existing_message_metadata 显式断言 user_id=="user-1",说明意图是框架值胜出——但这与 0.3 行为(业务值胜出)不一致,属于对外可观测的兼容性变更。若确为有意,请在 changelog 注明;否则可能让既有依赖“业务覆盖 user_id”的部署丢失用户标识。
  • trpc_agent_sdk/server/a2a/_application.py:96-118enable_v0_3_compat=True 且传入自定义 request_handler 时,_ensure_v0_3_interface 只作用于局部 card(well-known 路由用),而 create_jsonrpc_routes(request_handler, enable_v0_3_compat=True) 依赖 handler 自身的 agent_card;自定义 handler 的 card 未追加 0.3 接口。

    • test_compat_does_not_rewrite_custom_handler_card 明确断言 handler card 不被改写,且仅校验 /.well-known/agent-card.json 返回 url。但文档(README/a2a.md)宣称 compat 开关会让“0.3 客户端能正确发现并调用”。当使用自定义 handler 时,handler 的 card 没有 0.3 接口,0.3 客户端走 JSONRPC 路由(enable_v0_3_compat=True 传给 create_jsonrpc_routes)仍可工作,但 handler 内部若按其 agent_card.supported_interfaces 做版本判断则行为不确定。建议在 docstring/README 明确“自定义 handler 模式下 0.3 接口需由调用方自行保证”,避免使用者误以为开关对所有 handler 等效。
  • trpc_agent_sdk/server/a2a/converters/_part_converter.py:341-364convert_a2a_part_to_genai_parta2a_part 直接调用 HasField,但 _convert_a2a_data_part 内部用 getattr(part, "metadata", None) / getattr(part, "data", None) 做了鸭子类型容错,而主入口没有。

    • 若传入非 protobuf 的 duck-typed part(测试中存在 MagicMock(spec=A2APart) 用例,test_unsupported_part_returns_none 设置 HasField.side_effect=lambda f: False 才通过),生产路径下 a2a-sdk 1.x 始终是 protobuf,风险低;但 _a2a_part_requests_euc_auth 等同样直接 part.HasField("data"),与 _convert_a2a_data_part 的容错风格不一致。建议统一访问方式,避免后续接入非 proto 来源时主入口 HasFieldValueError

💡 Suggestion

  • trpc_agent_sdk/server/a2a/converters/_event_converter.py:494-583create_cancellation_event / create_exception_status_event / create_submitted_status_event / create_working_status_event / create_completed_status_event / create_final_status_event 仍保留 final: bool 形参但完全不再使用(1.x TaskStatusUpdateEventfinal 字段)。create_submitted_status_event 已无任何调用方(executor 改用 Task)。建议删除死参数与无调用方的函数,避免误用并减少维护面。

总结

本 PR 将 A2A 从 0.3 升级到 1.0(protobuf 化),整体迁移较完整且配有较充分的测试。存在一个 Critical(contract test 构造的 SendMessageRequest 可能缺必填字段导致测试假通过)和若干 Warning(异常/空内容终态序列可能与 1.x 任务终态约束不符、outgoing metadata 合并语义的兼容性变更、compat + 自定义 handler 的行为边界未对齐文档)。建议优先修复 Critical 与异常收尾状态序列问题。

测试建议

  • 补充“run 抛异常”端到端场景:断言失败后不再追加 completed 终态、且 aggregator.task_state 被正确置为 failed(当前 test_execution_error_enqueues_status_event 只断言存在 failed 事件,未断言后续无 completed)。
  • _handle_request 无内容收尾路径补充用例,断言最终 TaskStatusUpdateEvent.state 为 SDK 接受的终态(completed 等),而非 working
  • 修复并跑通 test_request_handler_stream_contract.py(确认 SendMessageRequest(tenant="", ...) 后能真正驱动 DefaultRequestHandler.on_message_send_stream)。

pytest.param(TaskState.TASK_STATE_AUTH_REQUIRED, id="auth_required"),
],
)
async def test_non_working_intermediate_status_does_not_truncate_stream(interrupted_state):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

contract test 构造 SendMessageRequest 缺少必填字段导致测试假通过

该 async 测试用例在 asyncio_mode=auto 下虽可被收集,但构造 SendMessageRequest(message=...) 未传 1.x 通常必填的 tenant 等字段。若 tenant 为 required,运行期会抛 ValidationError/ValueError 而非真正验证流不被截断,使契约测试给出假阳性结论。建议显式传 tenant="" 与生产代码保持一致。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from 935ed8e to b7544d3 Compare August 18, 2026 07:35
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经有足够的信息来撰写审查意见了。让我整合一下发现的问题。

基于我对 diff(a2a SDK 0.3 → 1.0 协议升级)和仓库上下文的审查,我的发现如下:

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/converters/_event_converter.py:705-736:error 事件后仍会追加同一 Event 的内容(artifact/status_update)

    • event.error_code 非空时,_create_error_status_event 已生成并追加一个 TASK_STATE_FAILEDTaskStatusUpdateEvent;随后 convert_event_to_a2a_message(event, ...) 仍基于同一个含 error 的事件产出 message 和 status_update(状态被 _create_status_update_event 覆盖为 working/input_required/auth_required)。最终一条流里先出现 failed 再出现 working,状态倒退,聚合器 TaskResultAggregator 又只记录首个 failed,容易导致最终态与流中事件不一致。建议 error 分支命中后直接 return,不再产出后续 message/status/artifact。
    • if event.error_code:
          error_event = _create_error_status_event(...)
          _notify(error_event)
          if error_event.status and error_event.status.HasField("message"):
              a2a_events.append(error_event)
          # 缺少 return:继续执行下面的 convert_event_to_a2a_message(...)
  • trpc_agent_sdk/server/a2a/_application.py:115-117enable_v0_3_compat 关闭时默认仍只发布 1.x 现代卡片路由,但 README/文档多处说明旧 0.3 客户端“无需改动”,存在兼容性缺口

    • enable_v0_3_compat=False(默认),well-known 仅挂 /.well-known/agent-card.json/.well-known/agent.json 返回 404(见 tests/server/a2a/test_application.py:177-182)。而旧 0.3 客户端 A2ACardResolver 默认拉取 /.well-known/agent.json,将无法发现该服务。文档(docs/mkdocs/en/a2a.md:142)称“卡片路径不变、发现无需迁移”,与默认行为矛盾。建议至少在文档中明确默认不兼容 0.3 客户端、需显式开启 compat,或将“无需改动”表述限定到 compat 已开启场景。
  • trpc_agent_sdk/server/a2a/_application.py:213-222_ensure_v0_3_interface)与 trpc_agent_sdk/server/a2a/_agent_card_builder.py:103-108:0.3 接口 url 复用首个已声明 url,但 builder 默认 url 可能为空

    • _ensure_v0_3_interface 复用 card.supported_interfaces 中首个非空 url 作为 0.3 接口 url;若部署方未配 rpc_url,1.0 与 0.3 接口 url 均为空,0.3 客户端发现顶层 url 缺失仍会失败。代码已 warn,但 compat 开关对“空 url 卡片”实际无补救作用,README 中“开 compat 后 0.3 客户端能正确发现并调用”的承诺在此常见配置下不成立。建议在 compat 路径下当 url 全空时显式 raise 或更醒目地提示,避免静默不可用。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:207-216_build_v0_3_a2a_client 构造 BaseClient 时传入 card=self._agent_card or AgentCard(),当 self._agent_cardNone 时传一张空卡,后续若 transport/client 读取卡内字段(skills/capabilities)将拿到默认空值;虽然当前 force_v0_3 路径已要求 url,行为可控,但传入空 AgentCard 语义不清,建议显式校验并给出明确报错而非静默兜底。

总结

整体是 a2a-sdk 0.3→1.0 协议升级,核心转换、aggregator 不变性、流契约均有对应测试锁定,主体稳定。存在一个 Warning:convert_event_to_a2a_events 在 error 分支未提前 return,会在 failed 后追加 working 状态事件,造成状态倒退;以及 0.3 兼容文档与默认行为不一致的兼容性风险。未发现安全或权限类 Critical 问题。

测试建议

  • 补充 convert_event_to_a2a_eventsevent.error_code 非空且 event.content 非空时的用例,断言输出中不包含 error 之后的 working/input_required 状态事件,并断言聚合器最终态为 failed。
  • 补充默认(非 compat)模式下旧 0.3 客户端发现路径 /.well-known/agent.json 的行为用例已存在,建议同时覆盖“未配 rpc_url + compat 开启”场景,断言 0.3 接口 url 仍为空且 warn 触发,明确该路径不可用。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from b7544d3 to ce74193 Compare August 18, 2026 07:47
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经掌握了所需的所有信息。现在让我来撰写评审意见。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:341:出站请求头传播改为 ClientCallContext(service_parameters=...) 后缺少任何测试覆盖

    • 原实现通过 state={"http_kwargs": {"headers": out_headers}}X-User-ID / opentelemetry trace 头注入 HTTP 请求;1.x 改为 service_parameters=out_headers,该字段是否被 SDK 真正转成出站 HTTP 头无法从 diff/仓库上下文验证。若 service_parameters 不映射到 HTTP 头,则 user_id 与分布式追踪上下文会静默丢失,跨服务链路断开。建议补一个断言出站请求带上 X-User-ID/trace 头的测试(mock httpx transport),确认映射成立后再合入。
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:218-234:仅当存在空 JSONRPC url 时才拷贝 caller 卡片,全非空 url 路径会把共享卡片直接交给 create_client

    • _fill_empty_jsonrpc_urls 的"不修改入参卡片"保证只在有空 url 时生效;当所有 JSONRPC url 都已非空时,self._agent_card 仍是调用方传入的原始卡片,随后 _build_standard_a2a_client 将其传给 create_client(self._agent_card, ...)。若 create_client 内部会改写卡片字段(如补全/规范化),会回写污染调用方持有的共享卡片。test_non_empty_jsonrpc_url_not_overwritten 仅校验了 url 未变,未覆盖其他字段。建议统一在 build 路径上 CopyFrom 后再传给 create_client,或在测试中校验卡片整体未被修改。

💡 Suggestion

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:210-216_build_v0_3_a2a_clientcard=self._agent_card or AgentCard()AgentCard() 兜底分支不可达——该方法只在 _a2a_client is None 时经 _build_a2a_client 调用,而其前 _discover_card/构造注入已保证 self._agent_card 非空。可去掉冗余兜底以免误导读者认为 None 卡片是合法输入。

总结

本次 PR 是 a2a-sdk 0.3→1.0 的协议升级,整体改动结构清晰、状态流转与 protobuf oneof/Struct 适配正确,并配有较完善的契约测试(stream 不被中断态截断、Task 优先事件、失败态不再追加 completed)。存在两处需关注的点:出站请求头传播方式变更缺少测试验证(可能静默丢失 user_id/trace),以及共享卡片在非空 url 路径下未做防御性拷贝;均非确定性的线上失败,但建议在合入前补测或加拷贝。

测试建议

  • 补一个验证 _run_async_impl 出站请求实际携带 X-User-ID 及 trace 头的测试(mock 底层 transport/httpx),确认 ClientCallContext(service_parameters=...) 的头映射成立。
  • 补一个"传入全非空 url 的共享卡片 + create_client 后卡片未被修改"的测试,固化非空路径下的不可变性。

@bochencwx
bochencwx force-pushed the feature/update_a2a_version_v2 branch from ce74193 to eb84ebf Compare August 18, 2026 08:14
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

发现的问题

⚠️ Warning

  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:541-543:初始 Task 事件会把用户输入回显到 agent 输出流

    • 1.x 服务端 execute() 现以 Task(..., history=[context.message]) 作为首个流事件(见 executor/_a2a_agent_executor.py:229-235),远程客户端在 _events_from_responseTask 无条件调用 convert_a2a_task_to_event,而该函数取 history[-1](即用户消息)转成 author=self.name、partial=False 的事件 yield 出去。0.3 时首个 submitted 状态事件会被 _events_from_response 的 state 白名单跳过,不会回显;本次升级引入了回归——调用方会在真正 agent 回复之前收到一条内容为用户输入、标记为非 partial 的 agent 事件。建议对 Task 分支按 status.state(submitted/working)跳过,或不把 context.message 放入初始 Task 的 history,并补充对应测试(现有 test_cancel_uses_id_from_initial_task 的 Task 无 history,未覆盖此路径)。
  • trpc_agent_sdk/server/a2a/_remote_a2a_agent.py:341,345-348ClientCallContext(service_parameters=...)send_message 调用未被任何测试覆盖

    • 请求头(OpenTelemetry inject + X-User-ID)从 0.3 的 state={"http_kwargs": {"headers": ...}} 改为 ClientCallContext(service_parameters=...),且 send_message(原 send_message_streaming)的返回值约定也变了。test_remote_a2a_agent.py 中相关用例均用 MagicMock 替换 a2a_client,从未触达真实 ClientCallContext 构造或 send_message 协议;若 service_parameters 并非 1.x 传递 HTTP header 的字段,trace 上下文与 user_id 透传会静默失效。建议加一个针对真实 ClientCallContext/transport 的契约测试,确认 header 实际下发。
  • trpc_agent_sdk/server/a2a/executor/_a2a_agent_executor.py:229-235:初始 Task 携带 history=[context.message] 与上面回显问题同源

    • 把用户消息塞进首事件 Task 的 history 是触发回显的根因之一。若保留 Task 作为提交信号,可仅用 Task(id, context_id, status=SUBMITTED) 不带 history,既能满足 1.x “首事件须为 Task”的约束,又避免远程端把用户输入当作 agent 内容。可与上一条合并修复。

💡 Suggestion

  • examples/agui/run_server.py:6-9,27:docstring 已改为 AG-UI,但 serve() 的函数注释仍是 "Start the A2A server using standard HTTP",建议同步改为 AG-UI 描述以免误导。

总结

整体是把 a2a-sdk 0.3 升级到 1.x(protobuf 类型、create_a2a_applicationStreamResponse oneof、Task 首事件)的大规模适配,核心路径(aggregator 不再改写事件、compat 开关、jsonrpc 路径推导)均有契约测试覆盖,未发现安全或崩溃级问题。主要风险是远程客户端对初始 Task 事件的处理引入了用户输入回显回归,且 ClientCallContext/send_message 的新调用缺少真实协议测试。

测试建议

  • 补充:远程客户端收到携带 history=[user_message] 的初始 Task 时,不应把用户文本作为 agent 事件 yield(覆盖 _events_from_responseTask 分支)。
  • 补充:用真实 ClientCallContext 验证 service_parameters 中的 X-User-ID/trace header 能否通过 1.x transport 实际下发到远端。

@weimch weimch closed this Aug 19, 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.

3 participants