How OmniRoute's A2A Server Enables JSON-RPC 2.0 Communication Between Agents
OmniRoute implements a standards-compliant Agent-to-Agent (A2A) server that enables autonomous AI agents to discover and invoke each other's capabilities through the JSON-RPC 2.0 protocol over HTTP.
The A2A layer in diegosouzapw/OmniRoute provides a clean bridge for inter-agent communication, allowing any agent to route requests through OmniRoute's existing infrastructure of smart routing, provider failover, and cost optimization. This article explains how the implementation works from protocol definition to production deployment.
Overview of the A2A Architecture
The A2A server follows a layered design built on four core components:
- Protocol layer — Zod schemas enforce JSON-RPC 2.0 compliance
- Transport layer — HTTP POST endpoints with optional STDIO support
- Dispatch layer — Maps JSON-RPC methods to skill handlers
- Execution layer — Task manager handles asynchronous lifecycle
Each layer operates independently, letting you swap transports or add skills without touching protocol validation.
JSON-RPC 2.0 Schema Validation with Zod
OmniRoute defines strict types for every A2A message in src/lib/a2a/schemas/a2a.ts. The schemas enforce required JSON-RPC 2.0 fields: jsonrpc (must equal "2.0"), method, id, and params.
// From src/lib/a2a/schemas/a2a.ts
const JSONRPCRequestSchema = z.object({
jsonrpc: z.literal("2.0"),
method: z.string(),
id: z.union([z.string(), z.number()]),
params: z.record(z.unknown())
});
const JSONRPCResponseSchema = z.object({
jsonrpc: z.literal("2.0"),
id: z.union([z.string(), z.number()]),
result: z.unknown().optional(),
error: z.object({
code: z.number(),
message: z.string(),
data: z.unknown().optional()
}).optional()
});
The same file defines AgentCard, Task, and SSE event schemas. AgentCard describes an agent's capabilities, endpoint URL, and authentication requirements. Task tracks asynchronous execution state.
Dispatch Pipeline: From HTTP to Skill Execution
Incoming requests hit src/app/a2a/route.ts, the Next.js API handler that serves as the JSON-RPC entry point.
// From src/app/a2a/route.ts
export async function POST(request: Request) {
const body = await request.json();
// Validate against Zod schema
const parseResult = JSONRPCRequestSchema.safeParse(body);
if (!parseResult.success) {
return jsonResponse({
jsonrpc: "2.0",
id: body.id || null,
error: {
code: -32600,
message: "INVALID_REQUEST"
}
});
}
// Forward to dispatcher
return executeTask(parseResult.data);
}
The dispatcher in src/lib/a2a/taskExecution.ts maps the validated method field to a concrete handler. Supported methods include:
smartRouting— Route to optimal providerquotaManagement— Check and allocate rate limitsproviderDiscovery— List available providersfusion— Aggregate multiple providershealth— System status reporting
// From src/lib/a2a/taskExecution.ts
const skillRegistry: Record<string, SkillHandler> = {
smartRouting: require('./skills/smartRouting').execute,
quotaManagement: require('./skills/quotaManagement').execute,
providerDiscovery: require('./skills/providerDiscovery').execute,
fusion: require('./skills/fusion').execute,
health: require('./skills/health').execute
};
export async function executeTask(request: JSONRPCRequest): Promise<JSONRPCResponse> {
const handler = skillRegistry[request.method];
if (!handler) {
return {
jsonrpc: "2.0",
id: request.id,
error: { code: -32601, message: "METHOD_NOT_FOUND" }
};
}
const result = await handler(request.params);
return {
jsonrpc: "2.0",
id: request.id,
result
};
}
Each skill lives in src/lib/a2a/skills/ and encapsulates reusable behavior. For example, smartRouting.ts leverages OmniRoute's combo routing engine to select providers based on latency, cost, and availability.
Task Lifecycle and Asynchronous Execution
Not all operations complete synchronously. The TaskManager in src/lib/a2a/taskManager.ts tracks long-running work across multiple stages.
Creating a Task
POST /a2a/tasks HTTP/1.1
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": "task-123",
"method": "fusion",
"params": {
"targets": ["gpt-4o", "claude-3-5-sonnet"],
"messages": [{"role": "user", "content": "Summarize the article."}]
}
}
Response:
{
"jsonrpc": "2.0",
"id": "task-123",
"result": {
"taskId": "f7c2e9b1-9a4d-4e6a-8f3b-2c1d9e5a6b7c",
"status": "pending"
}
}
Querying Task Status
GET /a2a/tasks/f7c2e9b1-9a4d-4e6a-8f3b-2c1d9e5a6b7c HTTP/1.1
Response:
{
"jsonrpc": "2.0",
"id": "task-123",
"result": {
"taskId": "f7c2e9b1-9a4d-4e6a-8f3b-2c1d9e5a6b7c",
"status": "completed",
"output": {"completion": "Combined summary..."}
}
}
Task state persists in SQLite through OmniRoute's generic database layer, enabling recovery across process restarts.
Streaming Progress with Server-Sent Events
For real-time feedback, clients connect to the streaming endpoint:
GET /a2a/tasks/f7c2e9b1-9a4d-4e6a-8f3b-2c1d9e5a6b7c/stream HTTP/1.1
Accept: text/event-stream
The streaming helper in src/lib/a2a/streaming.ts formats SSE events:
event: progress
data: {"percent":30}
event: chunk
data: {"content":"First part of the answer..."}
event: done
data: {"completion":"Final answer"}
Event types include:
progress— Percentage completion updateschunk— Partial LLM output tokenserror— Terminal failure statedone— Successful completion with final result
Transport Flexibility: HTTP and STDIO
While HTTP is the primary transport, OmniRoute's A2A server also supports STDIO for local CLI agents. The same dispatcher code runs in both modes:
# STDIO mode: pipe JSON-RPC messages directly
echo '{"jsonrpc":"2.0","id":1,"method":"health","params":{}}' | omniroom --a2a-stdio
This dual transport design lets you embed OmniRoute as a local MCP-style server or expose it as a networked service.
Error Handling Spec Compliance
OmniRoute returns standard JSON-RPC 2.0 error codes:
| Code | Message | When Returned |
|---|---|---|
| -32700 | PARSE_ERROR | Invalid JSON in request body |
| -32600 | INVALID_REQUEST | Schema validation failure (Zod) |
| -32601 | METHOD_NOT_FOUND | Unknown method parameter |
| -32602 | INVALID_PARAMS | Missing required skill parameters |
| -32603 | INTERNAL_ERROR | Unhandled exception in skill |
Example error response:
{
"jsonrpc": "2.0",
"id": "42",
"error": {
"code": -32600,
"message": "INVALID_REQUEST",
"data": "Missing required field 'method'"
}
}
Summary
- Zod schemas in
src/lib/a2a/schemas/a2a.tsenforce strict JSON-RPC 2.0 compliance with type-safe validation - Next.js API routes in
src/app/a2a/andsrc/app/api/a2a/expose HTTP endpoints for synchronous and asynchronous operations - TaskManager in
src/lib/a2a/taskManager.tstracks multi-step agent workflows with SQLite persistence - Skill registry in
src/lib/a2a/skills/maps JSON-RPC methods to reusable business logic including smart routing and multi-provider fusion - SSE streaming via
src/lib/a2a/streaming.tsdelivers real-time progress for long-running tasks - Dual transport support (HTTP and STDIO) accommodates both networked and local agent deployments
Frequently Asked Questions
What JSON-RPC 2.0 methods does OmniRoute's A2A server support?
The A2A server exposes smartRouting, quotaManagement, providerDiscovery, fusion, and health as core methods. Each maps to a skill handler in src/lib/a2a/skills/. You can extend the registry in src/lib/a2a/taskExecution.ts to add custom agent capabilities.
How does OmniRoute validate incoming JSON-RPC requests?
All requests pass through Zod schemas defined in src/lib/a2a/schemas/a2a.ts. The JSONRPCRequestSchema requires the standard jsonrpc, method, id, and params fields. Failed validation returns a -32600 INVALID_REQUEST error with specific details about the schema violation.
Can agents cancel long-running A2A tasks?
Yes. Send a POST request to /a2a/tasks/{taskId}/cancel. The TaskManager updates the task state to cancelled and propagates the signal to any running skill handlers. Partial results remain queryable via GET /a2a/tasks/{taskId}.
Is the A2A server compatible with the Model Context Protocol (MCP)?
OmniRoute's A2A implementation predates the official MCP specification but shares conceptual similarities. Both use JSON-RPC 2.0 and support STDIO transport. The skill registry design resembles MCP's tool registration. Future versions may add explicit MCP compatibility as the protocol standardizes.
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 →