fix(a2a): wait for final replies before resolving RPCs - #56437
fix(a2a): wait for final replies before resolving RPCs#56437davidrobertson wants to merge 4 commits into
Conversation
…search#514) Single platform-adapter plugin under plugins/platforms/a2a/ — zero core edits — that supersedes the entire A2A PR/issue cluster. Built on the ctx.register_platform + ctx.register_tool surface the codebase now exposes. Outbound (a2a toolset): a2a_discover / a2a_call / a2a_list let the agent call any A2A-compliant peer over JSON-RPC message/send. Inbound (platform adapter): a stdlib http.server serves an Agent Card at /.well-known/agent.json and routes incoming tasks into the agent's LIVE gateway session (the NousResearch#11025 insight) — same agent, full memory — returning the reply over A2A. Security on by default: no bearer token => 127.0.0.1-only bind; constant- time bearer auth; inbound prompt-injection filtering + untrusted-peer framing; outbound credential redaction; append-only audit log; per-context conversation persistence outside the compaction pipeline. Stdlib only (no a2a-sdk). 37 tests incl. a live HTTP round-trip (card + message/send + reply) and a bearer-auth 401 path.
The a2a client tools are registered unconditionally by the plugin, but a newly-registered plugin toolset defaults to ENABLED for every platform until the user has seen it in 'hermes tools'. That force-injected 'a2a' into every agent's enabled_toolsets, leaking 3 tool schemas to all users and breaking tests that assert exact toolset membership (test_api_server_toolset::test_create_agent_respects_config_override). Add 'a2a' to _DEFAULT_OFF_TOOLSETS so it stays opt-in (user enables via 'hermes tools'), matching the spotify precedent. The inbound platform adapter is already opt-in (only instantiated when the a2a platform is enabled); this aligns the outbound client tools with the same posture.
…e alias Live Tier-3 testing (CLI agent -> a2a tools -> live peer gateway -> model) surfaced two bugs the kwarg-style unit tests masked: 1. registry.dispatch calls handlers as handler(args, **kwargs) — args is the whole dict positional. The handlers used keyword params (url=, agent=), so the dict bound to the first param and .strip() raised 'dict object has no attribute strip'. Rewrote all three handlers to take args: dict (matching the spotify/google_meet convention). Added a registry-dispatch regression test that exercises the real call path the direct-kwarg tests never hit. 2. The model repeatedly reached for agent_name= instead of agent= (6 retries before success). Accept agent_name/name and message/text/task aliases so a reasonable guess succeeds first try. Verified live: client agent discovers the peer's Agent Card, calls it, and gets the reply back (PONG round-trip confirmed on both client audit log and peer conversation log). 39 plugin tests pass.
kuangmi-bit
left a comment
There was a problem hiding this comment.
Small, focused fix. Two issues addressed:
-
RPC reply timing - waiting for the final gateway reply before resolving is correct. Interim/status sends should not prematurely resolve the A2A request. This matches the A2A protocol expectation that the response carries the final state.
-
Reconnect kwargs passthrough - is_reconnect and similar adapter-agnostic kwargs need to flow through to connect(). Good catch for gateway restart scenarios.
80 additions, 2 files, no new deps. LGTM.
Consolidates 5 follow-up PRs onto the a2a-work branch: 1. Reply-capture fix (NousResearch#56437): adapter.send() now only resolves the blocked RPC Future when metadata['notify'] is True (the gateway's final-reply marker). Interim sends no longer short-circuit the response. Also accepts **kwargs in connect() for reconnect compat. 2. Slash command passthrough (NousResearch#53743): wrap_inbound() passes /-prefixed text through unwrapped so the gateway command processor sees it. Fixes /sethome deadlock during A2A onboarding. Documented security trade-off (bearer auth at network layer compensates). 3. Routable URL in Agent Card (NousResearch#53736): _build_card() now derives URL from A2A_PUBLIC_URL env > X-Forwarded-Host/Host header > bind host. Fixes k8s bug where Agent Card advertised 0.0.0.0. 4. contextId multi-turn memory (NousResearch#53756): _handle_inbound_task() now checks top-level params.contextId first (A2A spec), falls back to params.message.contextId (legacy). Outbound a2a_call also sends contextId at both top-level and inside message. 5. Type checker fixes (NousResearch#53759): TypedDict for _SCHEMAS, _FunctionSchema, _ToolSchema. Removes str() band-aid casts. All 45 tests pass including new tests for each fix. Zero core files modified — only plugins/platforms/a2a/ and tests/. Credits: @davidrobertson (NousResearch#56437), @knoal (NousResearch#53736, NousResearch#53743, NousResearch#53756, NousResearch#53759), @kuangmi-bit (slash command bug report), @gfdsa (k8s URL bug report), @shivasymbl (NousResearch#45996 userContext OBO).
2c31741 to
9bf2dac
Compare
|
Thanks for the focused stacked fix. The gateway contract supports its approach: final streaming sends receive Current Automated hermes-sweeper review. |
Consolidates 5 follow-up PRs onto the a2a-work branch: 1. Reply-capture fix (#56437): adapter.send() now only resolves the blocked RPC Future when metadata['notify'] is True (the gateway's final-reply marker). Interim sends no longer short-circuit the response. Also accepts **kwargs in connect() for reconnect compat. 2. Slash command passthrough (#53743): wrap_inbound() passes /-prefixed text through unwrapped so the gateway command processor sees it. Fixes /sethome deadlock during A2A onboarding. Documented security trade-off (bearer auth at network layer compensates). 3. Routable URL in Agent Card (#53736): _build_card() now derives URL from A2A_PUBLIC_URL env > X-Forwarded-Host/Host header > bind host. Fixes k8s bug where Agent Card advertised 0.0.0.0. 4. contextId multi-turn memory (#53756): _handle_inbound_task() now checks top-level params.contextId first (A2A spec), falls back to params.message.contextId (legacy). Outbound a2a_call also sends contextId at both top-level and inside message. 5. Type checker fixes (#53759): TypedDict for _SCHEMAS, _FunctionSchema, _ToolSchema. Removes str() band-aid casts. All 45 tests pass including new tests for each fix. Zero core files modified — only plugins/platforms/a2a/ and tests/. Credits: @davidrobertson (#56437), @knoal (#53736, #53743, #53756, #53759), @kuangmi-bit (slash command bug report), @gfdsa (k8s URL bug report), @shivasymbl (#45996 userContext OBO).
|
Merged to main via #77109 with your commit cherry-picked intact (authorship preserved) — the notify-gated final-reply capture is now part of the landed A2A plugin. Thanks @davidrobertson! |
Consolidates 5 follow-up PRs onto the a2a-work branch: 1. Reply-capture fix (NousResearch#56437): adapter.send() now only resolves the blocked RPC Future when metadata['notify'] is True (the gateway's final-reply marker). Interim sends no longer short-circuit the response. Also accepts **kwargs in connect() for reconnect compat. 2. Slash command passthrough (NousResearch#53743): wrap_inbound() passes /-prefixed text through unwrapped so the gateway command processor sees it. Fixes /sethome deadlock during A2A onboarding. Documented security trade-off (bearer auth at network layer compensates). 3. Routable URL in Agent Card (NousResearch#53736): _build_card() now derives URL from A2A_PUBLIC_URL env > X-Forwarded-Host/Host header > bind host. Fixes k8s bug where Agent Card advertised 0.0.0.0. 4. contextId multi-turn memory (NousResearch#53756): _handle_inbound_task() now checks top-level params.contextId first (A2A spec), falls back to params.message.contextId (legacy). Outbound a2a_call also sends contextId at both top-level and inside message. 5. Type checker fixes (NousResearch#53759): TypedDict for _SCHEMAS, _FunctionSchema, _ToolSchema. Removes str() band-aid casts. All 45 tests pass including new tests for each fix. Zero core files modified — only plugins/platforms/a2a/ and tests/. Credits: @davidrobertson (NousResearch#56437), @knoal (NousResearch#53736, NousResearch#53743, NousResearch#53756, NousResearch#53759), @kuangmi-bit (slash command bug report), @gfdsa (k8s URL bug report), @shivasymbl (NousResearch#45996 userContext OBO).
Consolidates 5 follow-up PRs onto the a2a-work branch: 1. Reply-capture fix (NousResearch#56437): adapter.send() now only resolves the blocked RPC Future when metadata['notify'] is True (the gateway's final-reply marker). Interim sends no longer short-circuit the response. Also accepts **kwargs in connect() for reconnect compat. 2. Slash command passthrough (NousResearch#53743): wrap_inbound() passes /-prefixed text through unwrapped so the gateway command processor sees it. Fixes /sethome deadlock during A2A onboarding. Documented security trade-off (bearer auth at network layer compensates). 3. Routable URL in Agent Card (NousResearch#53736): _build_card() now derives URL from A2A_PUBLIC_URL env > X-Forwarded-Host/Host header > bind host. Fixes k8s bug where Agent Card advertised 0.0.0.0. 4. contextId multi-turn memory (NousResearch#53756): _handle_inbound_task() now checks top-level params.contextId first (A2A spec), falls back to params.message.contextId (legacy). Outbound a2a_call also sends contextId at both top-level and inside message. 5. Type checker fixes (NousResearch#53759): TypedDict for _SCHEMAS, _FunctionSchema, _ToolSchema. Removes str() band-aid casts. All 45 tests pass including new tests for each fix. Zero core files modified — only plugins/platforms/a2a/ and tests/. Credits: @davidrobertson (NousResearch#56437), @knoal (NousResearch#53736, NousResearch#53743, NousResearch#53756, NousResearch#53759), @kuangmi-bit (slash command bug report), @gfdsa (k8s URL bug report), @shivasymbl (NousResearch#45996 userContext OBO).
Consolidates 5 follow-up PRs onto the a2a-work branch: 1. Reply-capture fix (NousResearch#56437): adapter.send() now only resolves the blocked RPC Future when metadata['notify'] is True (the gateway's final-reply marker). Interim sends no longer short-circuit the response. Also accepts **kwargs in connect() for reconnect compat. 2. Slash command passthrough (NousResearch#53743): wrap_inbound() passes /-prefixed text through unwrapped so the gateway command processor sees it. Fixes /sethome deadlock during A2A onboarding. Documented security trade-off (bearer auth at network layer compensates). 3. Routable URL in Agent Card (NousResearch#53736): _build_card() now derives URL from A2A_PUBLIC_URL env > X-Forwarded-Host/Host header > bind host. Fixes k8s bug where Agent Card advertised 0.0.0.0. 4. contextId multi-turn memory (NousResearch#53756): _handle_inbound_task() now checks top-level params.contextId first (A2A spec), falls back to params.message.contextId (legacy). Outbound a2a_call also sends contextId at both top-level and inside message. 5. Type checker fixes (NousResearch#53759): TypedDict for _SCHEMAS, _FunctionSchema, _ToolSchema. Removes str() band-aid casts. All 45 tests pass including new tests for each fix. Zero core files modified — only plugins/platforms/a2a/ and tests/. Credits: @davidrobertson (NousResearch#56437), @knoal (NousResearch#53736, NousResearch#53743, NousResearch#53756, NousResearch#53759), @kuangmi-bit (slash command bug report), @gfdsa (k8s URL bug report), @shivasymbl (NousResearch#45996 userContext OBO).
What does this PR do?
Fixes two integration bugs in the A2A plugin branch from #41711:
A2AAdapter.connect()accepts adapter-agnostic gateway reconnect kwargs such asis_reconnect.The important behavior change is in
A2AAdapter.send(): only sends marked withmetadata["notify"] == Truesatisfy the pending A2A reply future. Interim/status/editable sends are acknowledged locally but do not complete the RPC.This is needed because #41711 routes inbound A2A tasks into the live Hermes gateway session, following the design direction from #11025. That is the right architecture, but the normal gateway path can emit progress/status/steering messages before the final answer. Without this guard, A2A callers can receive a compaction banner or steering notice instead of the actual task result.
Related contributor work:
contextIdmulti-turn fix by @knoalsend()lock race fix by @knoalThis PR is intentionally based on
hermes/hermes-8d223d48, the head branch for #41711, becausemaindoes not contain the A2A plugin files yet.Related Issue
Addresses #56433 when #41711 lands.
Also related: #514, #689, #56434, #56435.
Type of Change
Changes Made
plugins/platforms/a2a/adapter.pyconnect()to accept**_kwargsfor gateway lifecycle compatibility.send()to ignore non-final sends while a context has a pending A2A reply future.fut.done()/fut.set_result()so this remains compatible with the race fix from fix(a2a): hold lock across fut.done/set_result in send() to fix race #53757.tests/plugins/test_a2a_plugin.pymetadata={"notify": True}final marker the gateway uses.connect(is_reconnect=True).How to Test
Result from this branch:
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — N/Acli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
Focused A2A plugin suite: