feat(a2a): SSOT typed-variant response parser + auto-fallback for poll-mode peers (#2967) - #2979
Merged
HongmingWang-Rabbit merged 3 commits intoMay 6, 2026
Merged
Conversation
) Introduce ``workspace/a2a_response.py`` as the single source of truth for the wire shapes the workspace-server proxy can return at ``/workspaces/<id>/a2a``: * ``Result`` — JSON-RPC success * ``Error`` — JSON-RPC error or platform-level error (with restart-in-progress metadata when present) * ``Queued`` — poll-mode short-circuit envelope: the platform queued the message into the target's inbox, the target will fetch via /activity poll * ``Malformed`` — anything the parser can't classify (logged at WARNING so a future server change is loud) ``send_a2a_message`` (in ``a2a_client.py``) now dispatches via ``a2a_response.parse(data)`` instead of inline ``"result" in data`` / ``"error" in data`` sniffing. The Queued variant returns a new ``_A2A_QUEUED_PREFIX`` sentinel so callers can distinguish "delivered async, no synchronous reply" from both success-with-text and failure. reno-stars production data caught two intermittent failures that both reduced to the same root cause: 1. **File transfer announce silently failed** — when CEO Ryan PC (poll-mode external molecule-mcp) sent the harmi.zip announcement to Reno Stars Business Intelligent (also poll-mode external), ``send_a2a_message`` saw the platform's poll-queued envelope ``{"status":"queued","delivery_mode":"poll","method":"..."}``, didn't recognize it as the synthetic delivery-acknowledgement it is, and returned ``[A2A_ERROR] unexpected response shape``. The agent fell back to a chunk-shipping path; receiver did get the file but operator-facing logs showed a failure that didn't actually fail. 2. **Duplicated agent comm** — same bug, inverted direction. d76 delegated to 67d, send_a2a_message returned the unexpected-shape error, delegate_task wrapped it as DELEGATION FAILED, the calling agent retried with sharper wording, the recipient saw the same request twice and self-reported "二次请求 — 我先不执行". External molecule-mcp standalone runtimes are inherently poll-mode (they have no public URL), so every external↔external A2A pair was hitting this on every send. The pre-fix client only handled JSON-RPC ``result``/``error`` keys and treated the queued envelope (which has neither) as malformed. RFC #2339 PR 2 added the queued envelope on the server side; the client never caught up. When ``send_a2a_message`` returns the ``_A2A_QUEUED_PREFIX`` sentinel, ``tool_delegate_task`` now transparently falls back to ``_delegate_sync_via_polling`` (RFC #2829 PR-5's durable ``/delegate`` + ``/delegations`` polling path, which DOES work for poll-mode peers because the platform's executeDelegation goroutine writes to the inbox queue and the result row arrives when the target picks it up + replies). The agent gets a real synchronous reply instead of the empty queued sentinel. * ``test_a2a_response.py`` — 62 tests, **100% line coverage** on the parser (verified via ``coverage run --source=a2a_response``). Includes adversarial-input fuzzing across ~25 pathological payloads — parser must never raise. * ``test_a2a_client.py::TestSendA2AMessagePollMode`` — 4 tests for the new Queued/Error wiring in ``send_a2a_message``. * ``test_delegation_sync_via_polling.py::TestPollModeAutoFallback`` — 3 tests for the auto-fallback in ``tool_delegate_task``, including negative cases (push-mode reply must NOT trigger fallback; genuine error must NOT silently retry). * **Verified all new tests FAIL on pre-fix source** by stashing a2a_client.py + a2a_tools_delegation.py and re-running — 5 failures including ImportError for the missing ``_A2A_QUEUED_PREFIX``. Per the operator-debuggability directive: * INFO at every Queued classification (expected variant; operator sees normal poll-mode-peer queueing in log stream). * INFO at the auto-fallback decision in ``tool_delegate_task`` so a future operator can correlate "send returned queued → falling back to polling path" without reading the source. * WARNING at every Malformed classification (server contract drift; operator MUST see this immediately). * Existing transient-retry WARNING preserved. * Mirror Go-side typed model in workspace-server. The wire shape is documented in ``a2a_response.py``'s module docstring with file:line pointers to the canonical emitters; a future PR can introduce ``models/a2a_response.go`` without changing wire behavior. The fixture corpus in ``test_a2a_response.py`` is designed so a one-sided edit breaks CI. * ``send_message_to_user`` and ``chat_upload_receive`` use a different endpoint (``/notify``) and aren't affected by this bug; their parsing stays unchanged. * 135 tests pass across ``test_a2a_response.py`` + ``test_a2a_client.py`` + ``test_delegation_sync_via_polling.py`` + ``test_a2a_tools_impl.py``. * ``coverage run --source=a2a_response -m pytest`` reports 100% line coverage with 0 missing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2967) Previously Phase 3 only checked the workspace-server's poll-mode short-circuit emit shape ({"status":"queued","delivery_mode":"poll","method":"..."}); the matching client-side classification was tested in isolation against fixture dicts in test_a2a_response.py. This phase closes the loop by piping the actual on-the-wire response from a real workspace-server back through the wheel's a2a_response.parse() and asserting it classifies as the Queued variant with the right method + delivery_mode. A regression in EITHER the server emit shape OR the client parser will now fail this E2E, eliminating the gap that allowed the original "unexpected response shape" production bug to ship despite green unit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HongmingWang-Rabbit
requested a review
from hongmingwang-moleculeai
as a code owner
May 6, 2026 00:32
HongmingWang-Rabbit
enabled auto-merge
May 6, 2026 00:33
The drift gate caught the new SSOT parser module — without registration the wheel ships it un-rewritten and runtime imports fail. Same pattern as inbox_uploads, a2a_tools_delegation, a2a_tools_rbac registrations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
🔒 Auto-merge disabled — new commit ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to the #2972 hotfix. Lands the broader SSOT typed-A2AResponse refactor and the external↔external delegation auto-fallback that the hotfix explicitly deferred.
The hotfix added a third inline branch to
send_a2a_messageto recognize{status:"queued",delivery_mode:"poll"}and stop misclassifying it as[A2A_ERROR] unexpected response shape. That stops the symptom but leaves wire-shape detection scattered across the function and leaves external molecule-mcp standalone runtimes (which always poll, never push) without a delegation path:delegate_task → send_a2a_message → "queued for poll-mode peer"succeeds, butdelegate_taskdoesn't yet recognize the queued outcome, so the delegating agent gets no synchronous reply.This PR:
workspace/a2a_response.py) — single source of truth for A2A wire shapes.parse(data) → Result | Error | Queued | Malformedfrozen dataclasses. Never raises; unknown shapes route toMalformedso the operator sees a loud warning, not a silent fallthrough.send_a2a_messagedispatches via the typed variants — replaces the chainedif "result" / elif "error" / elif "queued"block withisinstance(variant, ...)branches. New_A2A_QUEUED_PREFIXsentinel lets callers branch on the typed outcome without substring sniffing.delegate_taskauto-fallback to /delegate-poll — when the legacymessage/sendpath returns the queued sentinel,tool_delegate_tasknow transparently retries via_delegate_sync_via_polling(the RFC RFC: Async-first A2A delegation with durable task ledger #2829 PR-5 path), so external↔external delegation Just Works without operators flippingDELEGATION_SYNC_VIA_INBOX.INFOfor queued classification +WARNINGfor malformed bodies — operators can grepqueued for poll-mode peerandmalformed responsecleanly.restarting=trueJSON-RPC error, the client renders a softer(restarting, retry_after=Ns)suffix instead of a generic failure.Tests
tests/test_a2a_response.py— 62 tests, 100% line coverage ona2a_response.py(verified independently viacoverage run --source=a2a_response). Includes 17-fixture corpus, 4 variant test classes, ~25-input parametrized robustness suite asserting the parser never raises on adversarial input, and a regression gate that explicitly fails on pre-fix source.tests/test_a2a_client.py— 4 new poll-mode tests (TestSendA2AMessagePollMode) on top of the 3 hotfix tests (rebased onto the new sentinel contract). Pins_A2A_QUEUED_PREFIXsemantics, restart-metadata surfacing, and the partial-envelope-still-malformed defense.tests/test_delegation_sync_via_polling.py— 3 new tests (TestPollModeAutoFallback) covering positive auto-fallback on queued sentinel, no-fallback on push-mode peer (preserve fast path), and no-fallback on genuine error (don't mask real failures).tests/e2e/test_poll_mode_e2e.sh— Phase 3.5 added: pipes the actual on-the-wire response from a real workspace-server through the wheel'sa2a_response.parse()and asserts it classifies asQueuedwith the correct method + delivery_mode. A regression in either the server emit shape or the client parser fails this E2E.Pre-fix verification confirmed: stashed the SSOT parser/sentinel/auto-fallback diff and re-ran the new tests against the hotfix-only base; 5 tests fail (including
ImportError: cannot import name '_A2A_QUEUED_PREFIX'), confirming each new assertion discriminates. Restored after.Rollout
DELEGATION_SYNC_VIA_INBOXenv support in_delegate_sync_via_pollingsince RFC RFC: Async-first A2A delegation with durable task ledger #2829 PR-5; this just reaches it without requiring operator config when the peer is poll-mode.Test plan
WORKSPACE_ID=... pytest tests/test_a2a_response.py tests/test_a2a_client.py::TestSendA2AMessage tests/test_delegation_sync_via_polling.py::TestPollModeAutoFallback— 129 passa2a_response.py(verified independently)git stash)a2a_response.parse()unexpected response shaperows in subsequent activity feedCloses #2967.
🤖 Generated with Claude Code