Skip to content

a2a-server://

Expose Helix agents (and invocable routes) as an A2A (Agent-to-Agent) server. The consumer stands up an HTTP listener that serves the A2A JSON-RPC protocol at /invoke and an agent card at /.well-known/agent-card.json. Peers — including other Helix nodes via a2a://, or any A2A-compliant client — can list skills and invoke them, with streaming over JSON-RPC events.

Consumer-only — CreateProducer returns an error. Configure as a from: trigger; the steps below the trigger become the request-handling pipeline for every incoming A2A message.

URI shape

a2a-server://<host:port>?<params>

If the path lacks :port, port 9000 is used.

Parameters

Param Type Default Producer/Consumer/Both Description
(path) addr string — Consumer Listen address (e.g. 0.0.0.0:9000).
name string helix-a2a Consumer Agent name (advertised in the agent card).
description string Helix AI Agent Consumer Agent card description.
version string 1.0.0 Consumer Version string.

How skills are discovered

The agent card's skills are populated from:

  1. Registered agents — every agent registered with the agent component appears as a skill (tags: ["agent", <type>]).
  2. Invocable routes — every workflow whose from: URI is direct:// or seda:// appears as a skill (tags: ["route"]).
  3. Fallback — if neither list yields anything, a single default skill matching the configured name/description is added.

The card also advertises:

  • URL: http://<addr>/invoke
  • PreferredTransport: JSONRPC
  • DefaultInputModes: ["text"], DefaultOutputModes: ["text"]
  • Capabilities.Streaming: true

Per-call exchange shape

Every A2A call creates a fresh Exchange(InOut):

Header Description
HelixA2ATaskID Task ID assigned by the SDK.
HelixA2AContextID Context ID (conversation thread).
HelixStream Set to "true" — downstream agent/LLM producers use streaming mode automatically.

The body is the text extracted from the inbound message's parts. workflowID is a2a-task, fromURI is a2a-server://task/<taskID>. Session state is keyed by the A2A ContextID (falling back to the TaskID).

After the pipeline runs, the result is sent back to the client in one of three shapes:

  • *llm.StreamReader — chunks become ArtifactUpdateEvents on a single artifact; final chunk has LastChunk: true, then a final Message is sent with the full accumulated text.
  • <-chan string — same streaming flow, but driven by a channel.
  • anything else — final Message containing ex.In().BodyAsString().

Errors and ex.IsFailed() produce a StatusUpdateEvent{State: TaskStateFailed, Message: <error>}.

Examples

Expose a single agent over A2A:

agents:
  - id: helper
    type: react
    model: { provider: openai, model: gpt-4o }
    system-prompt: "You are a helpful assistant."

workflows:
  - id: a2a-server
    trigger:
      uri: a2a-server://0.0.0.0:9000?name=helper&description=General+helper
    steps:
      - target: agent://helper

The card is served at http://localhost:9000/.well-known/agent-card.json; JSON-RPC at http://localhost:9000/invoke.

Helix-on-Helix delegation:

# Node A — serves a translator
workflows:
  - id: translator-a2a
    trigger: { uri: a2a-server://0.0.0.0:9001?name=translator }
    steps:
      - target: agent://translator

# Node B — calls Node A
workflows:
  - id: call-translator
    trigger: { uri: direct://translate }
    steps:
      - target: a2a://node-a.internal:9001

Bridge to Google A2A clients (the card and JSON-RPC are the standard A2A SDK shapes — they interoperate without code on the client side).

Behavior notes

  • One pipeline, every call — the same configured pipeline runs for every A2A call. Branch inside the pipeline (using headers or body content) if you need to dispatch.
  • Streaming preference — because the consumer sets HelixStream=true, an agent:// step downstream will return a *llm.StreamReader, which the consumer translates into artifact events automatically. Pure non-streaming pipelines also work — the final body is sent as one message.
  • Session continuity — A2A's ContextID becomes the session key. ex.Property("helix.session.state") persists between calls with that context.
  • Cancel — A2A's Cancel is handled by emitting TaskStateCanceled for the matching task. There's no in-flight processor interrupt today.

Source

pkg/component/a2a/server.go, pkg/component/a2a/tools.go

See also

  • a2a — the client side.
  • mcp-server — same idea, MCP protocol.
  • agui, a2ui — frontend-focused alternatives.