Skip to content

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

a2a://<server-addr>

<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 last agent history message.
  • Task (failed) → producer returns a2a: <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 — initClient runs under sync.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 TaskStatusUpdateEvent reaches a terminal state.
  • Cleanup — Stop calls client.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.