ADR 0014 — A2A 0.3 → 1.0: adopt a2a-sdk + protolabs-a2a
- Status: Accepted — SHIPPED to
mainin #453 (2026-06-02). The hand-rolled handler is deleted; protoAgent speaks A2A 1.0 viaa2a-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 layer —a2a-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-state —
finalis removed; terminality is derived fromTASK_STATE_*. Our handler threads afinalflag through_build_status_event. - Member-discriminated Parts +
ROLE_*/TASK_STATE_*enums. - Agent card — served at
/.well-known/agent-card.json(we serveagent.json), withsupportedInterfaces[]and declaredcapabilities.extensions[]. - Custom DataParts — discriminator moves to
metadata.mimeTypehttps://proto-labs.ai/a2a/ext/<ext>with the payload incontent.value. We currently key them asapplication/vnd.protolabs.<ext>+json(see the MIME constants ina2a_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:
Created and vendored into protoAgent asprotoLabsAI/protolabs-a2adoes not exist yet.protolabs_a2a/(the four fleet extensions + card + auth- parts), byte-for-byte with the hub's
@protolabs/a2a.
- parts), byte-for-byte with the hub's
Flag-day — must not merge toMerging ≠ deploying. Landing the template's 1.0 code onmain.maindoesn'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 tomainwhile the deploy stays a coordinated, on-demand lever.Canonical wire contract lives in protoWorkstacean ADR-0006.Mirrored inprotolabs_a2a(member-discriminated parts,metadata.mimeTypediscriminator, the-v1MIMEs) 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:
| Concern | Today (hand-rolled) | 1.0 target |
|---|---|---|
| Routing / JSON-RPC dispatch | manual message/send, message/stream, tasks/* | a2a-sdk DefaultRequestHandler + A2AStarletteApplication |
| Turn execution | _run_task_background + TaskRecord state machine | AgentExecutor.execute/cancel wrapping the LangGraph graph |
| Status / artifact frames | _build_status_event (carries final), _build_artifact_event, _build_terminal_artifact_event | sdk event queue; terminal-by-state |
| Task store | A2ATaskStore + optional A2ATaskPersistence (a2a_task_store.py) | sdk TaskStore interface (keep our durable impl behind it) |
| Push notifications | A2APushStore (a2a_push_store.py), _spawn_webhook, SSRF allowlist | sdk push-notification support (keep the SSRF guard) |
| Auth | set_a2a_token / _check_auth bearer | protolabs-a2a auth scheme |
| Agent card | server.py::_build_agent_card (0.3 shape, agent.json) | protolabs-a2a card defaults (1.0, agent-card.json) |
| Custom DataParts | WORLDSTATE_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)
- Prereqs (gate):
protolabs-a2aexists (or we bootstrap it); pin thea2a-sdkPython version; have protoWorkstacean ADR-0006 + the hub spike branch open. - Executor spike —
AgentExecutorthat wraps the existing LangGraph graph; serve the 1.0 agent card viaprotolabs-a2a. Provemessage/send+message/streamround-trip. - Port the extension DataParts to the 1.0 discriminator (
metadata.mimeType+content.value) viaprotolabs-a2a; keep emission wired where it is today (cost onon_chat_model_end, confidence/tool-call/ hitl in the run loop). - Stores — adapt
A2ATaskStore/A2APushStorebehind the sdk'sTaskStore/ push interfaces (keep durability + the SSRF allowlist). - HITL — re-express
input-requiredpause/resume (LangGraphinterrupt+ thehitl-v1form payload) on the 1.0 task lifecycle. - Conformance — a real ws ↔ protoAgent 1.0 round-trip (the vertical slice). Port
tests/test_a2a_*.pyto 1.0 shapes; add a 1.0 round-trip test. - 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.
6. Related
- #443 — tracking issue (fleet template task)
- protoWorkstacean ADR-0006 — the canonical fleet decision + wire contract (upstream)
- React + Tauri console — the DataPart extensions also surface in the console (cost / tool-call / hitl cards)
- ADR 0003 — Reactive agent & Activity thread — the terminal-task hook + push notifications