Skip to content

feat(server/a2a): structured error parity with MCP — emit field/details/retry_after, catch decisioning AdcpError #530

@bokelley

Description

@bokelley

Motivation

PR #525 (closes #509) shipped structured `adcp_error` envelopes on the MCP side — `CallToolResult.structuredContent.adcp_error` now carries the full structured shape (`code`, `message`, `recovery`, `field`, `details`, `retry_after`).

The A2A surface in `src/adcp/server/a2a_server.py` has two parity gaps that would make A2A storyboards (when they exercise structured error checks) report "actual: ..." failures the same way the MCP side did before #509:

  1. `_send_adcp_error` only catches `adcp.exceptions.ADCPError`. Decisioning-layer `AdcpError` raises (which adopters using `DecisioningPlatform` produce) fall into the generic `except Exception` and render as a plain "Skill execution failed" text message — losing the structured shape entirely.

  2. It only emits `code` / `message` / `recovery` / `suggestion`. The fields `field`, `details`, and `retry_after` are dropped from the wire envelope even when the raised error carries them. MCP's feat(server): MCP error responses populate structuredContent.adcp_error (closes #509) #525 fix populates all of them.

Why this matters

Structured A2A error envelopes are a wire-spec thing — buyers using A2A clients should get the same `adcp_error` shape as MCP buyers. Storyboards graded against A2A-served sellers will surface this gap as a wire-conformance failure once the storyboard runner's structured-error checks are run against A2A surfaces (or the same checks via #1527's storyboard runner UX work).

Proposed fix

Mirror PR #525's MCP path:

  1. Catch decisioning-layer `AdcpError` AND `ADCPError` together in the A2A handler. Same field-extraction logic (`_extract_structured_fields`) feat(server): MCP error responses populate structuredContent.adcp_error (closes #509) #525 factored into `translate.py` can be reused — both projections feed off the same source-of-truth shape.

  2. Populate the full envelope. The A2A `DataPart` for the failed-task envelope should carry `{code, message, recovery, field, details, retry_after}` whenever the raised error has those fields populated; omit when None.

  3. Add tests parametrized over the same code set as MCP (`MEDIA_BUY_NOT_FOUND`, `PACKAGE_NOT_FOUND`, `TERMS_REJECTED`, `BUDGET_TOO_LOW`) verifying the A2A `DataPart` carries all fields.

Should be ~50 LOC + tests, mirrors the structure of the MCP fix.

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions