How to Set Up A2A Agent Protocol Communication in OmniRoute for Multi-Agent Workflows

OmniRoute implements the A2A (Agent-to-Agent) protocol as a JSON-RPC 2.0 service that lets independent agents exchange tasks, stream results, and query task status through a standardized HTTP endpoint.

The A2A protocol enables autonomous agents to delegate work, monitor progress, and consume streaming outputs without tight coupling. In the diegosouzapw/OmniRoute repository, this capability is built on three core components: a JSON-RPC router, an in-memory task manager with optional SQLite persistence, and a pluggable skill registry. This guide walks through enabling the protocol, authenticating requests, and integrating custom skills for production multi-agent systems.

Enable and Secure the A2A Endpoint

Activate the Protocol in Settings

The /a2a route checks a feature flag before processing any request. Store a2aEnabled: true in the settings table to activate the endpoint:

// Example using the Settings DB API
import { getSettings, updateSettings } from "@/lib/db/settings";

async function enableA2A() {
  const settings = await getSettings();
  await updateSettings({ ...settings, a2aEnabled: true });
}
enableA2A();

The router validates this flag via rejectIfA2ADisabled in src/app/a2a/route.ts (lines 91-93). Requests return HTTP 404 when disabled.

Configure Authentication (Optional)

Set the OMNIROUTE_API_KEY environment variable to require bearer token validation:

export OMNIROUTE_API_KEY="sk-omni-abc123xyz"

The authenticate helper in src/app/a2a/route.ts (lines 66-85) compares the Authorization: Bearer <token> header against this value. Omit the variable to run without authentication—suitable only for development.

Send Tasks via JSON-RPC

Basic Task Submission with message/send

POST JSON-RPC 2.0 payloads to https://<host>/a2a. The message/send method executes skills synchronously and returns complete artifacts:

fetch("https://my-omniroute.example.com/a2a", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + process.env.OMNIROUTE_API_KEY,
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: "task-123",
    method: "message/send",
    params: {
      skill: "smart-routing",
      messages: [{ role: "user", content: "Explain quantum entanglement." }],
      metadata: { role: "explain", model: "gpt-4o" },
    },
  }),
})
  .then((r) => r.json())
  .then(console.log);

The router in src/app/a2a/route.ts (lines 33-84) handles this flow: parse → authenticate → create task via tm.createTask → dispatch to registered handler → return jsonRpcResult.

Stream Partial Results with message/stream

For long-running operations, use message/stream to receive Server-Sent Events (SSE):

const ev = new EventSource(
  "https://my-omniroute.example.com/a2a",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + process.env.OMNIROUTE_API_KEY,
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: "stream-1",
      method: "message/stream",
      params: { 
        skill: "smart-routing", 
        messages: [{ role: "user", content: "Summarize the article." }] 
      },
    }),
  }
);

ev.onmessage = (e) => console.log("Chunk:", e.data);
ev.onerror = (e) => console.error("Stream error", e);

The createA2AStream function in src/lib/a2a/streaming.ts (lines 90-110) wraps the execution in SSE formatting with proper headers from SSE_HEADERS.

Manage Task Lifecycle

Task State Transitions

The A2A Task Manager in src/lib/a2a/taskManager.ts tracks states: submitted → working → completed | failed | cancelled.

Method Purpose Implementation
createTask Initialize task record Lines 94-110
updateTask Transition state, append artifacts Lines 111-130
cancelTask Move to cancelled, cleanup resources Lines 131-145
getTask Retrieve current state and history Lines 146-156

Query and Cancel Existing Tasks

Use JSON-RPC methods to inspect or terminate work:

// tasks/get — fetch current state
{
  "jsonrpc": "2.0",
  "id": "query-1",
  "method": "tasks/get",
  "params": { "id": "task-123" }
}

// tasks/cancel — terminate execution
{
  "jsonrpc": "2.0",
  "id": "cancel-1",
  "method": "tasks/cancel",
  "params": { "id": "task-123" }
}

Register Custom A2A Skills

Implement a Skill Handler

Create a TypeScript module in src/lib/a2a/skills/ following the A2ASkillHandler signature:

// src/lib/a2a/skills/helloWorld.ts
import type { A2ATask } from "@/lib/a2a/taskManager";

export async function helloWorldHandler(task: A2ATask) {
  const result = {
    artifacts: [{ type: "text", content: "Hello from A2A!" }],
    metadata: { greeting: true, processedAt: Date.now() },
  };
  return result;
}

Handlers receive the full A2ATask object and return Promise<StreamTaskResult> containing artifacts and optional metadata.

Add to the Skill Registry

Import and register in src/lib/a2a/taskExecution.ts (lines 19-23):

import { helloWorldHandler } from "@/lib/a2a/skills/helloWorld";

export const A2A_SKILL_HANDLERS: Record<string, A2ASkillHandler> = {
  "smart-routing": smartRoutingHandler,
  "list-capabilities": listCapabilitiesHandler,
  "cost-analysis": costAnalysisHandler,
  "health-report": healthReportHandler,
  "quota-management": quotaManagementHandler,
  "hello-world": helloWorldHandler, // ← custom skill
};

Registration is automatic—the router resolves skill names against this record at runtime.

Interact via CLI

The omniroute a2a commands in bin/cli/commands/a2a.mjs mirror the HTTP API:


# List available skills

npx omniroute a2a list

# Send a task

npx omniroute a2a send --skill smart-routing "Analyze this log file"

# Stream results

npx omniroute a2a stream --skill smart-routing "Generate report"

# Check task status

npx omniroute a2a get task-123

# Cancel running task

npx omniroute a2a cancel task-123

The CLI reads A2A_SKILL_HANDLERS directly, ensuring consistency with the HTTP interface.

Key Source Files and Architecture

File Responsibility
src/app/a2a/route.ts JSON-RPC router, authentication, method dispatch
src/lib/a2a/taskManager.ts Task lifecycle, state machine, persistence
src/lib/a2a/taskExecution.ts Skill registry (A2A_SKILL_HANDLERS), execution orchestration
src/lib/a2a/streaming.ts SSE stream construction, header constants
bin/cli/commands/a2a.mjs Command-line interface implementation
docs/frameworks/A2A-SERVER.md Protocol design documentation

Summary

  • Enable A2A by setting a2aEnabled: true in the settings table; the router validates this via rejectIfA2ADisabled in src/app/a2a/route.ts.
  • Authenticate with OMNIROUTE_API_KEY and Authorization: Bearer headers, or disable for development.
  • Submit tasks via message/send for synchronous results or message/stream for SSE-based streaming.
  • Track lifecycle through taskManager.ts methods: createTask, updateTask, cancelTask, getTask.
  • Extend capabilities by implementing A2ASkillHandler functions and registering them in A2A_SKILL_HANDLERS at src/lib/a2a/taskExecution.ts.
  • Operate at scale using the CLI client or direct HTTP JSON-RPC calls from any agent runtime.

Frequently Asked Questions

What is the A2A protocol in OmniRoute?

The A2A (Agent-to-Agent) protocol is a JSON-RPC 2.0 service layer that standardizes how independent software agents delegate tasks, exchange data, and monitor execution. In OmniRoute, it enables multi-agent workflows where specialized agents can hand off work through a common HTTP interface without direct integration.

How does streaming work in A2A communication?

OmniRoute uses Server-Sent Events (SSE) for streaming. The message/stream method triggers createA2AStream in src/lib/a2a/streaming.ts, which repeatedly calls executeA2ATaskWithState and emits partial artifacts as SSE data chunks. Clients consume these via EventSource or any SSE-compatible library.

Can I persist A2A tasks beyond server restarts?

Yes. The taskManager.ts implementation supports optional SQLite persistence in addition to its default in-memory store. Configure persistence through environment variables or settings—task state survives process restarts and enables recovery of long-running operations.

How do I add authentication to the A2A endpoint?

Set the OMNIROUTE_API_KEY environment variable, then include Authorization: Bearer <token> in all requests. The authenticate function in src/app/a2a/route.ts validates this header on every call. Return 401 for missing or invalid tokens.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →