Skip to content

ADR 0014 — A2A 0.3 → 1.0: adopt a2a-sdk + protolabs-a2a

  • Status: Accepted — SHIPPED to main in #453 (2026-06-02). The hand-rolled handler is deleted; protoAgent speaks A2A 1.0 via a2a-sdk + protolabs_a2a. The decision is inherited from protoWorkstacean ADR-0006; this ADR is the protoAgent-local plan + record. Tracked by #443 (closes at the fleet flag-day cutover). Merging ≠ deploying — the 0.3→1.0 cutover is a publish/deploy-time step coordinated with the hub + other agents, not gated on this merge.
  • Date: 2026-06-02
  • Deciders: Josh Mabry; protoAgent maintainers
  • Tags: a2a, protocol, migration, fleet
  • Supersedes / Superseded by:

protoAgent speaks A2A through a ~2,083-LOC hand-rolled handler (a2a_handler.py) implementing 0.3-era shapes. The fleet is moving to A2A 1.0, and the canonical strategy (protoWorkstacean ADR-0006) is adopt the official SDK, delete the hand-rolled handler, add a thin shared conventions layera2a-sdk (Python canon) + protolabs-a2a (the fleet's extensions / card defaults / auth scheme). protoAgent is the template, so it migrates first and proves the vertical slice; roxy forked this handler and inherits the migration, so getting protoAgent right unblocks roxy cheaply. This ADR records the plan; no code lands until the blockers below clear, and then only on an undeployed branch for a coordinated cutover.


1. Why not just patch the handler

0.3 → 1.0 is a wire-shape break, not a tweak:

  • Terminal-by-statefinal is removed; terminality is derived from TASK_STATE_*. Our handler threads a final flag through _build_status_event.
  • Member-discriminated Parts + ROLE_* / TASK_STATE_* enums.
  • Agent card — served at /.well-known/agent-card.json (we serve agent.json), with supportedInterfaces[] and declared capabilities.extensions[].
  • Custom DataParts — discriminator moves to metadata.mimeTypehttps://proto-labs.ai/a2a/ext/<ext> with the payload in content.value. We currently key them as application/vnd.protolabs.<ext>+json (see the MIME constants in a2a_handler.py).

Re-implementing protocol mechanics by hand again is exactly what ADR-0006 says not to do. Adopt a2a-sdk (AgentExecutor.execute/cancel, DefaultRequestHandler, A2AStarletteApplication) and put the protolabs specifics (extensions, card defaults, auth) in protolabs-a2a.

2. Blockers (resolved — kept for the record)

The plan originally listed three blockers; all cleared, and #453 merged:

  1. protoLabsAI/protolabs-a2a does not exist yet. Created and vendored into protoAgent as protolabs_a2a/ (the four fleet extensions + card + auth
    • parts), byte-for-byte with the hub's @protolabs/a2a.
  2. Flag-day — must not merge to main. Merging ≠ deploying. Landing the template's 1.0 code on main doesn't ship anything to prod; the 0.3→1.0 cutover is a publish/deploy-time step (the GHCR image / release), coordinated with the hub + other agents. So #453 merged to main while the deploy stays a coordinated, on-demand lever.
  3. Canonical wire contract lives in protoWorkstacean ADR-0006. Mirrored in protolabs_a2a (member-discriminated parts, metadata.mimeType discriminator, the -v1 MIMEs) and verified against the hub's @a2a-js/sdk.

3. What we have today (the surface to replace)

All in a2a_handler.py unless noted; server.py::register_a2a_routes(...) wires it:

ConcernToday (hand-rolled)1.0 target
Routing / JSON-RPC dispatchmanual message/send, message/stream, tasks/*a2a-sdk DefaultRequestHandler + A2AStarletteApplication
Turn execution_run_task_background + TaskRecord state machineAgentExecutor.execute/cancel wrapping the LangGraph graph
Status / artifact frames_build_status_event (carries final), _build_artifact_event, _build_terminal_artifact_eventsdk event queue; terminal-by-state
Task storeA2ATaskStore + optional A2ATaskPersistence (a2a_task_store.py)sdk TaskStore interface (keep our durable impl behind it)
Push notificationsA2APushStore (a2a_push_store.py), _spawn_webhook, SSRF allowlistsdk push-notification support (keep the SSRF guard)
Authset_a2a_token / _check_auth bearerprotolabs-a2a auth scheme
Agent cardserver.py::_build_agent_card (0.3 shape, agent.json)protolabs-a2a card defaults (1.0, agent-card.json)
Custom DataPartsWORLDSTATE_DELTA_MIME, COST_MIME, CONFIDENCE_MIME, TOOL_CALL_MIME, HITL_MIME (application/vnd.protolabs.*)metadata.mimeType …/ext/<ext> + content.value, defined in protolabs-a2a

Extensions protoAgent emits (these drive protolabs-a2a's shape): cost-v1, confidence-v1, worldstate-delta-v1, tool-call-v1 — plus hitl-v1 (added this cycle for the HITL forms). The card also declares hitl-mode-v1 (capability) and leaves blast-v1 / effect-domain-v1 commented for forks to fill.

4. Migration plan (slices, on an undeployed feature/a2a-1.0 branch)

  1. Prereqs (gate): protolabs-a2a exists (or we bootstrap it); pin the a2a-sdk Python version; have protoWorkstacean ADR-0006 + the hub spike branch open.
  2. Executor spikeAgentExecutor that wraps the existing LangGraph graph; serve the 1.0 agent card via protolabs-a2a. Prove message/send + message/stream round-trip.
  3. Port the extension DataParts to the 1.0 discriminator (metadata.mimeType + content.value) via protolabs-a2a; keep emission wired where it is today (cost on on_chat_model_end, confidence/tool-call/ hitl in the run loop).
  4. Stores — adapt A2ATaskStore/A2APushStore behind the sdk's TaskStore / push interfaces (keep durability + the SSRF allowlist).
  5. HITL — re-express input-required pause/resume (LangGraph interrupt + the hitl-v1 form payload) on the 1.0 task lifecycle.
  6. Conformance — a real ws ↔ protoAgent 1.0 round-trip (the vertical slice). Port tests/test_a2a_*.py to 1.0 shapes; add a 1.0 round-trip test.
  7. Cutover (flag-day) — coordinate the deploy with the hub + the other agents; roxy rebases its fork onto the migrated handler.

5. Consequences

Positive — delete ~2,083 LOC of hand-rolled protocol; conform to A2A 1.0; share extension/card/auth conventions across the fleet via one package; roxy inherits the migration nearly for free.

Negative / costs — new dependencies (a2a-sdk, protolabs-a2a); a flag-day cutover with real coordination cost (no interop, so timing matters); the conventions package must be authored first; a long-lived undeployed branch that has to be kept in sync with main until cutover.

Part of the protoLabs autonomous development studio.