Skip to content

fix(a2a): wait for final replies before resolving RPCs - #56437

Closed
davidrobertson wants to merge 4 commits into
NousResearch:hermes/hermes-8d223d48from
davidrobertson:fix/a2a-final-reply-capture
Closed

fix(a2a): wait for final replies before resolving RPCs#56437
davidrobertson wants to merge 4 commits into
NousResearch:hermes/hermes-8d223d48from
davidrobertson:fix/a2a-final-reply-capture

Conversation

@davidrobertson

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes two integration bugs in the A2A plugin branch from #41711:

  1. Inbound A2A RPCs now wait for the final gateway reply before resolving the blocked JSON-RPC request.
  2. A2AAdapter.connect() accepts adapter-agnostic gateway reconnect kwargs such as is_reconnect.

The important behavior change is in A2AAdapter.send(): only sends marked with metadata["notify"] == True satisfy 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:

This PR is intentionally based on hermes/hermes-8d223d48, the head branch for #41711, because main does not contain the A2A plugin files yet.

Related Issue

Addresses #56433 when #41711 lands.

Also related: #514, #689, #56434, #56435.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • plugins/platforms/a2a/adapter.py
  • tests/plugins/test_a2a_plugin.py
    • Added regression coverage showing interim/editable sends do not resolve the pending reply future.
    • Updated the live inbound round-trip mock to send the same metadata={"notify": True} final marker the gateway uses.
    • Added coverage for connect(is_reconnect=True).

How to Test

  1. Check out this branch against the feat(a2a): consolidated Agent-to-Agent protocol plugin (closes #514) #41711 A2A branch.
  2. Run:
/home/hermes/.hermes/hermes-agent/venv/bin/python -m pytest -o addopts='' tests/plugins/test_a2a_plugin.py -q

Result from this branch:

.........................................                                [100%]
41 passed in 2.45s

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

Focused A2A plugin suite:

/home/hermes/.hermes/hermes-agent/venv/bin/python -m pytest -o addopts='' tests/plugins/test_a2a_plugin.py -q
.........................................                                [100%]
41 passed in 2.45s

teknium1 and others added 4 commits June 7, 2026 19:47
…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.
@alt-glitch alt-glitch added type/bug Something isn't working comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have labels Jul 1, 2026

@kuangmi-bit kuangmi-bit left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small, focused fix. Two issues addressed:

  1. 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.

  2. 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.

bennybuoy added a commit to bennybuoy/hermes-agent that referenced this pull request Jul 5, 2026
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).
@teknium1
teknium1 force-pushed the hermes/hermes-8d223d48 branch from 2c31741 to 9bf2dac Compare July 6, 2026 09:24
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused stacked fix. The gateway contract supports its approach: final streaming sends receive metadata["notify"] = True in gateway/stream_consumer.py:246, while reconnect plumbing invokes adapters as connect(is_reconnect=...) in gateway/run.py:3500.

Current main has no tracked plugins/platforms/a2a/ implementation, so this behavior cannot be independently reproduced against HEAD; the PR depends on the unmerged #41711 branch whose head is this commit's parent (2c317416).

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 15, 2026
teknium1 pushed a commit that referenced this pull request Aug 2, 2026
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).
@teknium1

teknium1 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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!

@teknium1 teknium1 closed this Aug 2, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
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).
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
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).
murraysu pushed a commit to murraysu/hermes-agent that referenced this pull request Aug 14, 2026
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants