x-protolabs/tool-call-v1 is a streaming progress construct: per-tool lifecycle frames (started → completed/failed) emitted on the live A2A stream as artifact-update DataParts, so a rich client can render which tool an agent is running mid-task and how each invocation resolves.
Extension URI: https://proto-labs.ai/a2a/ext/tool-call-v1MIME type (DataPart discriminator): application/vnd.protolabs.tool-call-v1+json
What makes it different from the other extensions
The other four extensions (cost-v1, confidence-v1, blast-v1, hitl-mode-v1) are interceptors: they hook the A2AExecutor dispatch pipeline with before / after hooks, and gate on whether an agent declared the URI in its card. They observe or stamp a single terminal exchange.
tool-call-v1 is not an interceptor. It is not declared with before / after hooks, and it is not a terminal-artifact telemetry payload. It rides the live SSE stream while the task is still working: each frame is its own artifact-update event, emitted the moment a tool turn streams out of the agent's reasoning loop. There is no dispatch-pipeline registration for it — the frames originate inside the in-process runtime and flow over the bus to the A2A server, which emits them on the wire.
Think of it as the structured sibling of the plain-text status.message narration described in A2A Streaming: same live stream, but a machine-readable tool timeline instead of a humanized sentence.
Payload shape
A single frame describes one tool invocation. The payload is a discriminated union on phase, so each phase carries only its relevant field. Multiple frames sharing a toolCallId describe that invocation's lifecycle.
interface ToolCallFrameBase {
toolCallId: string; // stable id correlating frames of the same invocation
name: string; // tool name (e.g. "github_create_issue")
}
type ToolCallArtifactData =
| (ToolCallFrameBase & { phase: "started"; args?: unknown })
| (ToolCallFrameBase & { phase: "completed"; result?: unknown })
| (ToolCallFrameBase & { phase: "failed"; error?: string });started— the agent's reasoning step requested this tool;argscarries the parsed call arguments.completed— the tool returned;resultcarries its output.failed— the tool errored;errorcarries the message.
A consumer keys on toolCallId to stitch started to its later completed / failed, rendering a per-tool spinner that resolves to a check or an X.
Emit / parse helpers
The contract lives in packages/a2a/src/extensions.ts (the TypeScript source of truth; the Python protolabs-a2a layer mirrors it):
import { emitToolCall, parseToolCall, TOOL_CALL_V1_MIME_TYPE } from "@protolabs/a2a";
// Build a DataPart for a streamed artifact-update.
const part = emitToolCall({ toolCallId, name: "pr_inspector", phase: "started", args });
// Extract the first tool-call-v1 payload from a parts array, or undefined.
const frame = parseToolCall(parts);emitToolCall builds a spec-correct A2A 1.0 DataPart: the structured payload lives in content.value, and the discriminator rides on metadata.mimeType (not the SDK's transport-level mediaType, which stays application/json). parseToolCall scans a parts array for the first part whose metadata.mimeType matches the MIME constant and returns its payload.
How a frame reaches the wire
The frames are produced by the in-process DeepAgentExecutor and bridged to the A2A server over the bus. The path:
Emit (executor).
DeepAgentExecutorstreams the LangGraph agent withagent.stream(..., { streamMode: "values" })rather thaninvoke()-ing it, so tool turns surface live as they stream in.extractToolFramesscans each newly-streamed message: astartedframe per tool call on an AI message (keyed by the call'sid), and acompleted/failedframe per tool-result message. Each frame fires the executor'sonToolFramecallback.Bridge (runtime plugin).
AgentRuntimePluginwiresonToolFrameto publish on the bus topic:agent.skill.toolframe.{correlationId}with payload
{ frame: ToolCallArtifactData }.Emit on the stream (A2A server).
BusAgentExecutor(src/api/a2a-server.ts) subscribes toagent.skill.toolframe.{correlationId}for the duration of an inboundmessage/send/message/streamcall. Each frame becomes anartifact-updateevent whose artifact (namedtool-call) carriesemitToolCall(frame):tseventBus.publish(AgentEvent.artifactUpdate({ taskId, contextId, artifact: dataArtifact([emitToolCall(frame)], { name: "tool-call" }), append: false, lastChunk: false, }));The subscription is torn down together with the reply / progress / input subscriptions on terminal or cancel, so frames never leak across overlapping task ids, and late frames after settle are dropped.
Two parallel channels — pick by client capability
A single in-process run feeds two streaming channels off the same tool turns:
| Channel | Topic | A2A surface | Reader |
|---|---|---|---|
| Text narration | agent.skill.progress.{cid} | status-update (state=working, status.message) | Simple clients |
| Structured frames | agent.skill.toolframe.{cid} | artifact-update DataPart (this extension) | Rich tool-timeline clients |
The narration channel carries a humanized one-liner (routing to quinn, searching the web) plus an initial thinking frame emitted the instant the graph starts — before the first LLM turn produces any tool call, so the caller sees motion during the model-latency window. The tool-call-v1 channel carries the machine-readable started/completed/failed frames in parallel.
A simple client reads status.message and ignores the artifact frames. A rich client reads the tool-call-v1 artifact frames to draw a live tool timeline. Neither blocks the other.
Reference implementations
| Side | Where | Notes |
|---|---|---|
| Contract | packages/a2a/src/extensions.ts | MIME + URI constants, ToolCallArtifactData union, emitToolCall / parseToolCall |
| Frame extraction | src/executor/executors/deep-agent-executor.ts — extractToolFrames | Cursor-driven scan over streamed messages; fires onToolFrame per lifecycle event |
| Bus bridge | src/agent-runtime/agent-runtime-plugin.ts — _publishToolFrame | Publishes agent.skill.toolframe.{cid} |
| Wire emission | src/api/a2a-server.ts — BusAgentExecutor | Subscribes to the toolframe topic, emits artifact-update DataParts |
This channel is specific to the in-process runtime (DeepAgent). Remote A2A agents emit their own streaming frames over their own /a2a SSE response; this bus bridge is how workstacean's in-process agents reach parity with that on their own /a2a endpoint.
Related
- A2A Streaming (SSE) — the streaming pipeline this rides on, including the
agent.skill.progress.{cid}text-narration channel - Extend an A2A Agent — the interceptor-style extensions (cost / confidence / blast / hitl-mode)
cost-v1— terminal-task telemetry, by contrast abefore/afterinterceptor