How to Implement the A2A Protocol for Agent-to-Agent Communication in OmniRoute

OmniRoute provides a built-in A2A (Agent-to-Agent) server that implements the JSON-RPC 2.0 specification, enabling autonomous services to communicate through a standardized task lifecycle, skill dispatch system, and SSE streaming interface.

OmniRoute ships with a complete A2A protocol implementation that allows agents to invoke skills and exchange data via JSON-RPC endpoints. To implement A2A protocol for agent-to-agent communication in OmniRoute, you configure the environment, understand the five-layer architecture, and interact with the built-in skill registry through HTTP or CLI interfaces.

Understanding the A2A Architecture

The A2A stack in OmniRoute consists of five distinct layers that handle everything from HTTP ingress to skill execution:

Layer Responsibility Source File
Task lifecycle Creation, state transitions, and TTL cleanup src/lib/a2a/taskManager.ts
Skill dispatch Mapping skill names to handlers and transaction management src/lib/a2a/taskExecution.ts
SSE streaming Formatting streaming responses for real-time updates src/lib/a2a/streaming.ts
HTTP entry point Next.js route for JSON-RPC ingestion and validation src/app/a2a/route.ts
Skill registration Exposing built-in capabilities to the A2A namespace src/lib/skills/a2a.ts

Core Implementation Components

Task Lifecycle Management

The A2ATaskManager class in src/lib/a2a/taskManager.ts orchestrates the complete lifecycle of an agent task. When a client sends a message/send request, the manager creates a task with a UUID v4 identifier, stores it in-memory (with optional SQLite persistence), and enforces state transitions through the VALID_TRANSITIONS map.

Valid states follow a strict flow: submitted → working → completed (or failed / cancelled). The manager automatically expires tasks after a configurable TTL (defaulting to five minutes) and tracks active SSE streams through beginStream and endStream methods for concurrency metrics.

Skill Dispatch and Execution

Skill routing occurs in src/lib/a2a/taskExecution.ts through the executeA2ATaskWithState function. This wrapper uses the A2A_SKILL_HANDLERS object to lazily import skill implementations on first use, optimizing cold start performance.

When executing a skill, the wrapper runs the handler inside a transaction, then updates the task state to completed (attaching artifacts) or failed (attaching error artifacts). Errors are caught, logged, and re-thrown so the HTTP layer can return proper JSON-RPC error responses.

SSE Streaming Infrastructure

For real-time communication, src/lib/a2a/streaming.ts exports createA2AStream, which formats Server-Sent Events (SSE) for the message/stream RPC method. The stream formats task artifacts into data: blocks and event: names, maintaining a connection to the task manager's beginStream / endStream hooks to track activeStreams count.

HTTP Route Handler

The HTTP entry point resides in src/app/a2a/route.ts, implemented as a Next.js API route. This handler:

  • Parses incoming JSON-RPC 2.0 payloads
  • Validates the optional REQUIRE_API_KEY guard
  • Routes methods (message/send, message/stream, tasks/get, tasks/cancel) to the A2ATaskManager
  • Serializes responses while preserving the request id field

When processing message/send, the route creates a task via createTask, immediately transitions it to working, executes the corresponding skill via executeA2ATaskWithState, and returns the final state.

Built-in A2A Skills

OmniRoute registers six built-in skills in src/lib/skills/a2a.ts, with individual implementations located in src/lib/a2a/skills/. Each skill exports an async function receiving an A2ATask and returning:

{
  artifacts: TaskArtifact[];
  metadata: Record<string, unknown>;
}

The default skill set includes:

  • smart-routing – Selects optimal provider combinations for requests
  • quota-management – Reports remaining quota and usage statistics per provider
  • provider-discovery – Lists available providers and their models
  • cost-analysis – Estimates monetary costs for specific requests
  • health-report – Returns health metrics for the router and downstream services
  • list-capabilities – Generates an HTML artifact enumerating all available A2A skills

Enabling and Configuring the A2A Server

The A2A endpoint is disabled by default. To activate the server, set the environment variable in your .env file:

A2A_ENABLED=true

When enabled, the /a2a tab appears in the OmniRoute dashboard (declared in src/shared/constants/endpointCategories.ts), and the CLI command omniroute a2a … becomes available for interactive invocation (implemented in src/lib/agentSkills/openapiParser.ts).

Client Integration Examples

Sending a Task via HTTP

Invoke a skill from a Node.js client using standard fetch:

import fetch from 'node-fetch';

const endpoint = 'http://localhost:3000/a2a';
const payload = {
  jsonrpc: '2.0',
  id: '12345',
  method: 'message/send',
  params: {
    skill: 'provider-discovery',
    messages: [{ role: 'system', content: 'List all providers' }],
  },
};

const resp = await fetch(endpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
});
const result = await resp.json();
console.log(result);

Streaming Responses with SSE

For long-running skills, use the message/stream method with EventSource:

import EventSource from 'eventsource';

const url = new URL('http://localhost:3000/a2a');
url.searchParams.set('jsonrpc', '2.0');
url.searchParams.set('id', 'stream1');
url.searchParams.set('method', 'message/stream');
url.searchParams.set(
  'params',
  JSON.stringify({
    skill: 'smart-routing',
    messages: [{ role: 'user', content: 'Explain the best model for a 10k token prompt' }],
  })
);

const es = new EventSource(url.toString());
es.onmessage = ev => console.log('chunk →', ev.data);
es.onerror = err => console.error('stream error', err);

CLI Usage

List available capabilities directly from the terminal:

omniroute a2a list-capabilities

Summary

  • OmniRoute implements the JSON-RPC 2.0 specification for A2A communication through a dedicated Next.js route at src/app/a2a/route.ts
  • The task lifecycle is managed by A2ATaskManager in src/lib/a2a/taskManager.ts, enforcing states and TTL cleanup
  • Skills are dispatched through executeA2ATaskWithState in src/lib/a2a/taskExecution.ts with lazy loading and transaction safety
  • SSE streaming for real-time updates is handled by createA2AStream in src/lib/a2a/streaming.ts
  • Six built-in skills provide immediate capabilities for routing, quota management, and health monitoring
  • Enable the server by setting A2A_ENABLED=true and interact via HTTP, SSE, or the omniroute a2a CLI

Frequently Asked Questions

What is the A2A protocol in OmniRoute?

The A2A (Agent-to-Agent) protocol in OmniRoute is a JSON-RPC 2.0 compliant communication standard that allows autonomous agents to invoke skills, manage task lifecycles, and stream results. It enables external services or internal components to interact with OmniRoute's routing and provider management capabilities through a standardized HTTP interface.

How do I enable SSE streaming for A2A tasks?

SSE streaming is available by calling the message/stream method on the /a2a endpoint. The server uses createA2AStream from src/lib/a2a/streaming.ts to format task artifacts as server-sent events. Clients should connect using an EventSource interface and handle onmessage events to receive real-time updates while the task state transitions from working to completed.

What skills are available in the default A2A implementation?

OmniRoute provides six built-in A2A skills registered in src/lib/skills/a2a.ts: smart-routing (provider selection), quota-management (usage tracking), provider-discovery (model listing), cost-analysis (price estimation), health-report (service metrics), and list-capabilities (skill enumeration). Each skill resides in src/lib/a2a/skills/ and returns standardized artifacts.

How does OmniRoute handle A2A task failures?

When a skill handler throws an error, executeA2ATaskWithState catches the exception, logs it, creates an error artifact, and transitions the task state to failed. The error is then re-thrown to the HTTP layer in src/app/a2a/route.ts, which returns a properly formatted JSON-RPC error response containing the original request ID and error details.

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 →