Once you've built the A2A spec surface (Build an A2A Agent) your agent works as a plain A2A responder — workstacean discovers skills, dispatches, tracks tasks, reports to fleet health. But five additional capabilities give the dispatcher, observability, and HITL policy richer signal about each skill:
| Extension | Purpose | Direction |
|---|---|---|
| cost-v1 | Token + wall-time observations → fleet-health + dashboards | workstacean → stores |
| confidence-v1 | Agent self-reported confidence → calibration metrics | your agent → stores |
| effect-domain-v1 | Agent-reported world-state deltas → world.state.delta observability | your agent's response artifacts |
| blast-v1 | Scope-of-effect declaration → HITL/policy routing | your agent's card |
| hitl-mode-v1 | Per-skill approval policy → HITL flow selection | your agent's card |
You don't have to implement any of them. You keep working as a plain A2A agent. But each extension unlocks behavior on the workstacean side that only fires when you opt in.
A sixth extension — tool-call-v1 — also exists, but it is not part of this interceptor pack. It is a streaming-only construct (live started → completed/failed tool frames emitted as artifact-update DataParts), not a card-declared before/after interceptor. See its own subsection below.
How workstacean picks them up
All five interceptors are registered unconditionally at workstacean startup (src/index.ts). They run on every outbound A2A call, but self-gate on whether you advertised the URI in your agent card. Workstacean never forces behavior on agents that don't opt in.
Two concrete consequences:
- Card-only extensions (blast-v1, hitl-mode-v1, effect-domain-v1's declarations): the
beforehook readsdefaultXxxRegistry— populated whenSkillBrokerPluginrefreshes your card every 10 min. If you don't declare, the registry miss is a no-op; nothing is stamped on the outbound request. - Observation extensions (cost-v1, confidence-v1): the
afterhook reads the response'sdatafield. If your agent doesn't includeusage/confidencefields, the interceptor early-returns without recording.
This means extensions are a zero-risk gradient. Ship your agent, observe fleet health, decide one extension at a time whether it's worth declaring.
A complete example — Quinn's card
Here's what it looks like when an agent opts in to all five:
{
"name": "quinn",
"description": "QA engineer — PR review, bug triage, board audit",
"url": "http://quinn:7870/a2a",
"version": "1.0.0",
"provider": { "organization": "protoLabsAI" },
"capabilities": {
"streaming": true,
"pushNotifications": true,
"stateTransitionHistory": false,
"extensions": [
{ "uri": "https://proto-labs.ai/a2a/ext/cost-v1" },
{ "uri": "https://proto-labs.ai/a2a/ext/confidence-v1" },
{
"uri": "https://proto-labs.ai/a2a/ext/effect-domain-v1",
"params": {
"skills": {
"pr_review": { "effects": [{ "domain": "pr_pipeline", "path": "data.conflicting", "delta": -1, "confidence": 0.7 }] },
"bug_triage": { "effects": [{ "domain": "board", "path": "data.openBugs", "delta": -1, "confidence": 0.8 }] },
"board_audit": { "effects": [] }
}
}
},
{
"uri": "https://proto-labs.ai/a2a/ext/blast-v1",
"params": {
"skills": {
"sitrep": { "radius": "self" },
"board_audit": { "radius": "project" },
"pr_review": { "radius": "repo" },
"bug_triage": { "radius": "project" },
"security_triage": { "radius": "fleet", "note": "Can affect the entire fleet's security posture" }
}
}
},
{
"uri": "https://proto-labs.ai/a2a/ext/hitl-mode-v1",
"params": {
"skills": {
"sitrep": { "mode": "autonomous" },
"board_audit": { "mode": "notification" },
"pr_review": { "mode": "veto", "vetoTtlMs": 300000 },
"bug_triage": { "mode": "notification" },
"security_triage": { "mode": "gated", "reviewer": "operator" }
}
}
}
]
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/markdown"],
"skills": [
{ "id": "sitrep", "name": "Situation Report", "description": "…" },
{ "id": "board_audit", "name": "Board Audit", "description": "…" },
{ "id": "pr_review", "name": "PR Review", "description": "…" },
{ "id": "bug_triage", "name": "Bug Triage", "description": "…" },
{ "id": "security_triage", "name": "Security Triage", "description": "…" }
],
"securitySchemes": {
"apiKey": { "type": "apiKey", "in": "header", "name": "X-API-Key" }
},
"security": [{ "apiKey": [] }]
}Notice:
- cost-v1 + confidence-v1 have no params — declaring the URI is enough to opt in. Your agent still has to include
usage/confidencefields on terminal-taskdatafor samples to be recorded. - effect-domain-v1, blast-v1, hitl-mode-v1 carry
params.skills— a map keyed by skill id. - You don't declare every skill. Only the ones where the default (no declaration → no-op) isn't what you want.
Response-side requirements
For the observation extensions (cost-v1, confidence-v1), you also need to include the expected fields on your terminal Task.data. None of these are required by the A2A spec itself — they're extension-specific conventions:
{
"status": { "state": "completed" },
"artifacts": [{ "parts": [{ "kind": "text", "text": "…" }] }],
"data": {
"usage": {
"input_tokens": 3421,
"output_tokens": 890,
"cache_read_input_tokens": 0
},
"durationMs": 4823,
"costUsd": 0.0187,
"confidence": 0.88,
"confidenceExplanation": "Spec was unambiguous; all tests pass.",
"success": true
}
}usage.input_tokens/output_tokens— Anthropic-shaped token counts. Cached tokens are optional but tracked when provided.durationMs/costUsd— wall-time + dollar cost.costUsdis optional; the cost-v1 consumer can compute from tokens +MODEL_RATESif missing.confidence— float in[0, 1]. Required for confidence-v1 sample recording; the extension defensively clamps out-of-range values.confidenceExplanation— free-text, surfaced in calibration views.success—falseexplicitly marks a failure even whenstate: completed. Use this when your skill returns a textual failure message rather than throwing.
For effect-domain-v1 (the response side of it), attach a worldstate-delta DataPart to your terminal task's artifacts when you've mutated shared state:
{
"artifacts": [
{
"parts": [
{ "kind": "text", "text": "Closed 3 stale PRs" },
{
"kind": "data",
"data": {
"deltas": [
{ "domain": "pr_pipeline", "path": "data.staleOpen", "op": "inc", "value": -3 }
]
},
"metadata": { "mimeType": "application/vnd.protolabs.worldstate-delta-v1+json" }
}
]
}
]
}Workstacean's effect-domain-v1 after-hook extracts this and publishes world.state.delta so dashboards and fleet-health observe the mutation in near-real-time.
Registration at workstacean side
For reference — you don't need to do anything here; this is what workstacean runs at startup:
// src/index.ts
registerCostExtension(bus);
registerConfidenceExtension(bus);
registerEffectDomainExtension(bus);
registerBlastExtension();
registerHitlModeExtension();All five interceptors live in defaultExtensionRegistry and fire on every A2AExecutor.execute() call. Self-gating keeps them safe for non-opt-in agents.
Adoption order — recommended
- cost-v1 — zero card changes, just emit
usage+durationMsin your terminal data. Gets you dashboard + fleet-health visibility for your agent. - confidence-v1 — add
confidenceto your terminal data. Gets your agent into calibration views and unblocks the "high-confidence failure" signal for OutcomeAnalysis. - blast-v1 — declare scope per skill on the card. Unlocks policy-driven HITL gating without code changes.
- hitl-mode-v1 — declare per-skill approval policy. Now your skills route through the right HITL flow by default (autonomous / notification / veto / gated / compound).
- effect-domain-v1 — include observed
worldstate-deltaartifacts in your response. Lets workstacean publishworld.state.deltaso the mutation shows up in observability in ~1s instead of one poll cycle.
Each step is independently useful. The pack composes — e.g. blast-v1 + hitl-mode-v1 together let you say "anything fleet or public radius is gated, reviewer: operator."
tool-call-v1 — a streaming-only extension, not an interceptor
tool-call-v1 is the odd one out: it is not one of the five interceptors above and not something you declare on your card with before / after hooks. It is a live streaming construct — per-tool lifecycle frames (started → completed/failed) emitted on the SSE stream as artifact-update DataParts (MIME application/vnd.protolabs.tool-call-v1+json) while the task is still working.
Key differences from the interceptor pack:
- No card declaration, no self-gating. The five interceptors register on the
A2AExecutordispatch pipeline and fire (then no-op) based on whether you advertised the URI. tool-call-v1 has no dispatch-pipeline registration at all — the frames originate inside the in-process runtime and ride the live stream. - Streaming, not terminal telemetry. cost-v1 / confidence-v1 read the terminal task's
data; tool-call-v1 frames arrive mid-task, oneartifact-updateper tool turn, keyed bytoolCallIdso a client can stitch astartedto its latercompleted/failed. - It's the structured sibling of
status.messagenarration. Workstacean's in-process agents emit both a humanized text line (onstatus.message) and these structured frames in parallel. Simple clients read the text; rich clients read the frames to draw a tool timeline. See A2A Streaming → Structured tool-call frames.
You don't opt into it from your card. Workstacean's own /a2a endpoint emits it for its in-process agents; a remote A2A agent that wants to offer the same rich timeline streams equivalent artifact-update DataParts (built with emitToolCall) on its own SSE response.
Related
- Build an A2A Agent — the spec-side recipe (task store, webhooks, health)
- Extension reference pages — cost-v1, confidence-v1, effect-domain-v1, blast-v1, hitl-mode-v1, tool-call-v1
- A2A Streaming (SSE) — the streaming pipeline tool-call-v1 frames ride on