InitRunner

A2A Server

The initrunner a2a serve command exposes any agent as an A2A (Agent-to-Agent) 1.0 server. A2A is the Linux Foundation protocol for agents to discover and invoke each other across frameworks. Other A2A 1.0 clients can find your InitRunner agent at /.well-known/agent-card.json and call it over JSON-RPC.

InitRunner speaks A2A 1.0 only (since v2026.8.4). There is no 0.3 compatibility mode. FastA2A / 0.3 peers no longer work. Clients and curl examples must send A2A-Version: 1.0 on every JSON-RPC request. Method names are gRPC-style: SendMessage, GetTask, CancelTask (not message/send / tasks/get).

Quick Start

# Install the A2A extra
uv pip install initrunner[a2a]

# Start the server
initrunner a2a serve role.yaml

# With authentication
initrunner a2a serve role.yaml --api-key my-secret-key

# Custom host/port. Binding a non-loopback host without --api-key fails closed:
# a key is generated and printed rather than serving the agent unauthenticated.
# Pass --url so the card advertises a dialable address (not http://0.0.0.0:9000).
initrunner a2a serve role.yaml --host 0.0.0.0 --port 9000 \
  --url http://agent.example:9000 --api-key my-secret-key

The server exposes:

  • GET /.well-known/agent-card.json — agent card (discovery; no auth)
  • POST / — JSON-RPC (SendMessage, SendStreamingMessage, GetTask, CancelTask, SubscribeToTask, …)
curl -s http://127.0.0.1:8000/.well-known/agent-card.json | jq .supportedInterfaces

curl -s http://127.0.0.1:8000/ \
  -H 'Content-Type: application/json' \
  -H 'A2A-Version: 1.0' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "SendMessage",
    "params": {
      "message": {
        "role": "ROLE_USER",
        "messageId": "m1",
        "parts": [{"text": "hello"}]
      }
    }
  }' | jq .result.task.status

A request without A2A-Version: 1.0 is treated as protocol 0.3 and rejected with JSON-RPC error -32009.

Token stream (SSE):

curl -N http://127.0.0.1:8000/ \
  -H 'Content-Type: application/json' \
  -H 'A2A-Version: 1.0' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "SendStreamingMessage",
    "params": {
      "message": {
        "role": "ROLE_USER",
        "messageId": "m1",
        "parts": [{"text": "hello"}]
      }
    }
  }'

CLI Options

OptionTypeDefaultDescription
role_filePath(required)Path to the role YAML file, or a group file together with --agent
--agentstrnullWhich member of a group to serve. A2A publishes one agent card per URL, so a group target requires it. Since v2026.8.6.
--hoststr127.0.0.1Host to bind to. Use 0.0.0.0 to expose on all interfaces.
--portint8000Port to listen on
--urlstrhttp://{host}:{port}Public URL written into the agent card. Required for a dialable card when --host is 0.0.0.0 or ::.
--api-keystrNoneAPI key for Bearer token authentication. When set, all endpoints except the agent card require Authorization: Bearer <key>. Binding a non-loopback --host without a key fails closed — one is generated and printed so the JSON-RPC endpoint is never served unauthenticated off-host.
--cors-originstrNoneAllowed CORS origin. Can be repeated.
--audit-dbPath~/.initrunner/audit.dbPath to audit database
--no-auditboolfalseDisable audit logging
--skill-dirPathNoneExtra skill search directory
--modelstrNoneModel alias or provider:model override

How It Works

The server is a Starlette app assembled from a2a-sdk 1.0 routes and DefaultRequestHandlerV2. The custom InitRunnerAgentExecutor routes every task through execute_run_stream_async(), so A2A-served agents match --serve and CLI runs:

  • Input content validation
  • Role guardrail usage limits
  • Retry/timeout wrapping
  • Output validation and serialization
  • Audit logging
  • Agent-principal context

Blocking SendMessage waits until the task is terminal (COMPLETED, FAILED, CANCELED, REJECTED) or interrupted (INPUT_REQUIRED, AUTH_REQUIRED). configuration.returnImmediately: true returns the SUBMITTED/WORKING task and finishes in the background; poll with GetTask.

CancelTask cancels the running execute_run_stream_async() coroutine and marks the task TASK_STATE_CANCELED.

SendStreamingMessage (and SubscribeToTask) stream over SSE. Text roles emit append-artifact chunks as tokens arrive (lastChunk: true on the final delta). Structured output is published once at the end as a data part. Delegation (A2AInvoker) stays on blocking SendMessage.

Inbound url / raw / data parts map to PydanticAI content. Raw parts over 20 MB fail the task. Failed tasks include result.error in status.message.

Agent Card

The card at /.well-known/agent-card.json is built from the role YAML. It includes supportedInterfaces (JSON-RPC, protocol version 1.0), version from the role, one default skill for the role plus one per resolved SKILL.md, and a Bearer security scheme when --api-key is set. The card advertises streaming: true and wider defaultInputModes.

The SDK serializer also emits a few A2A 0.3 mirror fields (url, preferredTransport, …). Treat supportedInterfaces as the source of truth.

{
  "name": "researcher",
  "description": "Gathers and summarizes research from the web",
  "version": "1.0.0",
  "supportedInterfaces": [
    {
      "url": "http://127.0.0.1:8000",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "extendedAgentCard": false
  },
  "defaultInputModes": [
    "text/plain",
    "application/json",
    "image/*",
    "audio/*",
    "video/*",
    "application/pdf",
    "application/octet-stream"
  ],
  "defaultOutputModes": ["text/plain", "application/json"],
  "skills": [
    {
      "id": "researcher",
      "name": "researcher",
      "description": "Gathers and summarizes research from the web"
    }
  ]
}

Conversation Context

A2A uses contextId to keep conversation threads. The executor stores PydanticAI message_history in an in-process LRU (1000 contexts) keyed by contextId. Tasks live in the SDK InMemoryTaskStore. Both die when the process exits. Delegation keeps a per-invoker contextId so repeated delegate_to_* calls share server history.

Calling A2A Agents from a Role

Use the delegate tool with mode: a2a to call a remote A2A agent from within another agent:

name: coordinator
model: openai:gpt-4o
prompt: >
  You coordinate research tasks by delegating to specialized agents.
tools:
  - delegate:
      mode: a2a
      timeout_seconds: 120
      agents:
        - name: research-agent
          url: http://research-server:8000
          description: Gathers and summarizes research from the web
        - name: analysis-agent
          url: http://analysis-server:8000
          description: Performs data analysis and generates reports
          headers_env:
            Authorization: ANALYSIS_AGENT_API_KEY

When the LLM calls delegate_to_research_agent("find papers on transformers"), InitRunner:

  1. Resolves http://research-server:8000/.well-known/agent-card.json (cached on the invoker)
  2. Sends JSON-RPC SendMessage with A2A-Version: 1.0 and a per-invoker contextId
  3. If the task completes, extracts text from artifacts, then status.message, then the last agent history message
  4. If the server returns SUBMITTED / WORKING (it honored returnImmediately), polls GetTask with exponential backoff until completion or timeout
  5. Returns the result text to the LLM

Repeated delegate_to_* calls on the same invoker reuse that contextId, so the remote server can keep multi-turn history.

Delegate Config Reference

FieldTypeRequiredDescription
mode"a2a"YesSelects the A2A protocol
agentslistYesList of agent references
agents[].namestrYesAgent name (used in tool function name)
agents[].urlstrYesA2A server URL
agents[].descriptionstrNoDescription shown to the LLM
agents[].headers_envdictNoMap of header name to environment variable name
timeout_secondsintNoTimeout for the full request+polling cycle. Default: 120.
max_depthintNoMax delegation depth. Default: 3.

Error Handling

All errors are returned as strings prefixed with [DELEGATION ERROR] so the LLM can see and handle failures gracefully. This includes:

  • Task failed, rejected, or canceled
  • Timeout (connection or polling)
  • HTTP errors
  • JSON-RPC errors
  • Policy denial (when agent authorization is configured)

Debugging a Failed Task

When a run fails, the reason travels two ways. The client gets it in the failed task's status message:

"status": {"state": "TASK_STATE_FAILED", "message": {"parts": [{"text":
  "Model API error: status_code: 401, model_name: gpt-4o-mini, body:
   {'message': 'Invalid API key provided', 'code': 'invalid_api_key'}"}]}}

Since v2026.8.8, the server logs the same failure, so an operator watching the process does not need the client to report it:

[agent.run] run 01b147e59c75 of agent 'support-bot' failed [auth]: Model API error: status_code: 401, ...

For the provider request behind the failure (useful against a self-hosted OpenAI-compatible endpoint such as LiteLLM or vLLM), start the server with debug logging:

INITRUNNER_LOG_LEVEL=DEBUG initrunner a2a serve role.yaml

See Logging for the levels, the failed-run format, and the error categories.

Comparison with Other Interfaces

Feature--serve (OpenAI)mcp servea2a serve
ProtocolOpenAI chat completionsMCP (JSON-RPC)A2A 1.0 (JSON-RPC)
DiscoveryManualMCP tool listingAgent card at /.well-known/agent-card.json
Multi-turnServer-side via x-conversation-idPer-tool callVia contextId
Agents per server1Multiple1
Client tooldelegate mode mcpNative MCP clientsdelegate mode a2a
Use caseDrop-in OpenAI replacementTool sharing with AI IDEsCross-framework agent communication

See also: API Server for the OpenAI-compatible --serve mode, MCP Gateway for the MCP server, and Delegate tool for calling agents from within roles.

On this page