a2a://¶
Send a message to an external A2A (Agent-to-Agent) server from inside a route. The producer opens a JSON-RPC session against the remote agent's /invoke endpoint, calls SendMessage (or SendStreamingMessage when streaming), and writes the agent's reply onto the exchange. Use a2a:// when you want to delegate a step to a peer agent that speaks the A2A protocol — typically another Helix node running a2a-server://, or a Google A2A-compliant agent.
URI shape¶
<server-addr> is the host:port of the remote A2A agent. The producer prepends http:// and appends /invoke to form the JSON-RPC endpoint.
Producer-only — CreateConsumer returns an error. For the server side, see a2a-server://.
Parameters¶
| Param | Type | Default | Producer/Consumer/Both | Description |
|---|---|---|---|---|
(path) server-addr |
string | — | Producer | Host:port of the remote A2A agent (no scheme). |
The URI accepts arbitrary additional params (stashed in the endpoint) for forward compatibility, but none are read by the current implementation.
Headers¶
Read by the producer¶
| Header | Description |
|---|---|
HelixStream |
When "true", the producer uses SendStreamingMessage. The exchange body is set to a <-chan string of incremental chunks and Content-Type is set to text/event-stream. |
Written by the producer (non-streaming Task results)¶
| Header | Description |
|---|---|
HelixA2ATaskID |
The A2A task ID returned by the server. |
HelixA2ATaskState |
Task state (completed, working, failed, ...). |
Body shaping¶
Outbound: ex.In().BodyAsString() becomes the text part of an A2A user message. The producer errors on an empty body.
Inbound (non-streaming): the JSON-RPC result is either a Message or a Task:
Message→ text parts concatenated into the body.Task(completed) → text from artifacts, falling back to the status message, falling back to the lastagenthistory message.Task(failed) → producer returnsa2a: <text>as an error.
Inbound (streaming): a <-chan string is set as the body. The goroutine drains the SDK's SendStreamingMessage iterator, emitting text extracted from Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent events. The channel closes when the stream ends or hits a terminal state.
Examples¶
Synchronous delegation:
workflows:
- id: route-to-peer
trigger: { uri: direct://route }
steps:
- target: a2a://peer-agent.internal:9000
Streaming proxy via HTTP SSE:
workflows:
- id: peer-stream
trigger:
uri: http-server://0.0.0.0:8080/peer
steps:
- set-header:
name: HelixStream
constant: "true"
- target: a2a://peer-agent.internal:9000
Chain across nodes (Helix-on-Helix):
workflows:
- id: ask-then-translate
trigger: { uri: direct://ask }
steps:
- target: a2a://kb-agent:9001 # ask the knowledge agent
- set-body:
starlang: '"Translate to French: " ++ payload'
- target: a2a://translator:9002 # then the translator
Behavior notes¶
- Lazy connect —
initClientruns undersync.Once. The first call constructs the SDK client from[]AgentInterface{...URL+'/invoke', Transport: JSONRPC}. Init error is sticky. - Streaming termination — the channel closes when:
- the SDK iterator finishes,
- any error is returned (logged, no error on the producer-side body),
- a
TaskStatusUpdateEventreaches a terminal state. - Cleanup —
Stopcallsclient.Destroy()to release the SDK's HTTP client / goroutines.
Source¶
pkg/component/a2a/client.go, pkg/component/a2a/tools.go
See also¶
- a2a-server — expose Helix agents as A2A servers.
- mcp, mcp-server — same pattern for the MCP protocol.
- agent — the underlying agent runtime.