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: truein the settings table; the router validates this viarejectIfA2ADisabledinsrc/app/a2a/route.ts. - Authenticate with
OMNIROUTE_API_KEYandAuthorization: Bearerheaders, or disable for development. - Submit tasks via
message/sendfor synchronous results ormessage/streamfor SSE-based streaming. - Track lifecycle through
taskManager.tsmethods:createTask,updateTask,cancelTask,getTask. - Extend capabilities by implementing
A2ASkillHandlerfunctions and registering them inA2A_SKILL_HANDLERSatsrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →