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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions