A2A JSON-RPC 2.0 Agent Protocol Implementation in OmniRoute Explained
The OmniRoute A2A JSON-RPC 2.0 agent protocol is implemented as a lightweight /a2a HTTP endpoint that supports synchronous requests and Server-Sent Events (SSE) streaming, with built-in authentication, task lifecycle management, and pluggable skill dispatch.
The Agent-to-Agent (A2A) JSON-RPC 2.0 protocol in OmniRoute enables external systems—CLI tools, automation scripts, or other services—to invoke internal routing capabilities programmatically. This protocol follows the JSON-RPC 2.0 specification strictly while adding streaming extensions for real-time agent interactions. The implementation is production-ready with constant-time authentication, in-memory task tracking, and TTL-based cleanup.
Core Architecture of the A2A JSON-RPC 2.0 Protocol
The A2A implementation spans five architectural layers, each with dedicated responsibilities and source files.
Route Handler and HTTP Interface
The entry point resides in [src/app/a2a/route.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/app/a2a/route.ts). This Next.js API route handles POST requests to /a2a, parses JSON-RPC envelopes, validates authentication, and dispatches to the appropriate method handler.
Key characteristics:
- Responds to
OPTIONSrequests with CORS headers (lines 68–78) - Globally disableable via
getSettings().a2aEnabled(lines 5–18) - Returns standard JSON-RPC error codes:
-32600(invalid request),-32700(parse error),-32603(internal error)
Authentication Layer
Authentication uses constant-time token comparison to prevent timing attacks. The authenticate helper extracts Authorization: Bearer <token> and validates against process.env.OMNIROUTE_API_KEY using Node.js timingSafeEqual (lines 68–90 in route.ts).
Critical security behavior: if no OMNIROUTE_API_KEY is configured, the endpoint is open. This supports development environments but requires explicit configuration for production.
Task Management with A2ATaskManager
The singleton A2ATaskManager in [src/lib/a2a/taskManager.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/a2a/taskManager.ts) tracks the complete task lifecycle:
- States:
submitted→working→completed|failed|cancelled - Storage: In-memory Map with UUID-v4 task identifiers
- Cleanup: TTL-based eviction for completed/failed tasks
- Artifacts: Structured output storage with metadata
Task creation occurs via getTaskManager().createTask (lines 45–66 in route.ts), generating unique task IDs for every invocation.
Skill Dispatch System
Skill handlers are registered in A2A_SKILL_HANDLERS within [src/lib/a2a/taskExecution.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/a2a/taskExecution.ts). This registry maps skill names to dynamic import wrappers:
// From taskExecution.ts - simplified structure
const A2A_SKILL_HANDLERS: Record<string, SkillHandler> = {
'smart-routing': async (params) => {
const { executeSmartRouting } = await import('./skills/smartRouting');
return executeSmartRouting(params);
},
// Additional skills...
};
Each handler returns { artifacts, metadata }, which the task manager persists before state transition.
Streaming Engine with SSE
The createA2AStream function in [src/lib/a2a/streaming.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/a2a/streaming.ts) constructs ReadableStream responses for message/stream calls. Features include:
- Heartbeat events: Every 15 seconds via
createHeartbeat - Per-artifact chunks: Via
createChunkEvent - Completion event: Final metadata via
createCompletionEvent - Error propagation:
createFailureEventfor cancellations or exceptions
A2A JSON-RPC 2.0 Method Reference
The /a2a endpoint implements four methods with strict JSON-RPC 2.0 compliance.
message/send — Synchronous Execution
Executes a skill and returns complete results as a JSON-RPC response.
// Example: Synchronous smart-routing call
const response = await fetch('http://localhost:20128/a2a', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-secret-key'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'message/send',
params: {
skill: 'smart-routing',
messages: [
{ role: 'user', content: 'What is the weather today?' }
]
}
})
});
const result = await response.json();
// { jsonrpc: '2.0', id: 1, result: { artifacts: [...], metadata: {...} } }
Implementation path: route.ts lines 47–65 → taskExecution.ts → skill handler → synchronous return.
message/stream — SSE Streaming
Returns a streaming response for real-time artifact delivery.
// Example: Streaming consumption
const resp = await fetch('http://localhost:20128/a2a', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-secret-key'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'abc-123',
method: 'message/stream',
params: {
skill: 'smart-routing',
messages: [
{ role: 'user', content: 'Explain quantum entanglement.' }
]
}
})
});
const reader = resp.body?.getReader();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const lines = new TextDecoder().decode(value);
// SSE format: data: {"jsonrpc":"2.0","method":"...",...}\n\n
console.log(lines);
}
Stream construction uses createA2AStream (lines 90–149), which manages backpressure, heartbeat scheduling, and graceful termination.
tasks/get — Task State Query
Retrieves current task state, artifacts, and metadata by ID.
tasks/cancel — Cancellation Request
Requests graceful termination of a running task. The cancellation signal propagates through the stream controller in streaming.ts.
// Example: Cancel a running task
await fetch('http://localhost:20128/a2a', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-secret-key'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tasks/cancel',
params: {
taskId: 'f0a1b2c3-d4e5-6f7a-89b0-c1d2e3f45678'
}
})
});
Request Flow Through the A2A Protocol
- Authentication:
authenticatehelper validates bearer token withtimingSafeEqual(lines 68–90) - Validation: JSON-RPC 2.0 schema check for
jsonrpcfield andmethodpresence (lines 37–41) - Enablement check:
getSettings().a2aEnabledgate (lines 5–18) - Task creation:
getTaskManager().createTaskgenerates UUID-v4 task ID (lines 45–66) - Method dispatch: Branch on
methodfield to handler logic (lines 47–65) - Skill execution: Dynamic import via
A2A_SKILL_HANDLERS[skill]with state updates - Response formatting: JSON-RPC envelope construction or SSE stream initiation
- Observability: Routing decisions logged when
smart-routingskill invoked
Extending the A2A JSON-RPC 2.0 Implementation
Adding Custom Skills
- Create implementation file under
src/lib/a2a/skills/ - Export async
execute<SkillName>function returning{ artifacts, metadata } - Register in
A2A_SKILL_HANDLERSintaskExecution.ts
// src/lib/a2a/skills/customAnalytics.ts
export async function executeCustomAnalytics(params: A2AParams) {
// Implementation
return {
artifacts: [{ type: 'analysis', content: result }],
metadata: { durationMs, confidence: 0.95 }
};
}
// src/lib/a2a/taskExecution.ts
'custom-analytics': async (params) => {
const { executeCustomAnalytics } = await import('./skills/customAnalytics');
return executeCustomAnalytics(params);
},
Custom Error Codes
Follow JSON-RPC 2.0 conventions per the OmniRoute source:
- Negative codes: Standard errors (-32600 invalid, -32603 internal)
- Positive codes: Application-specific errors defined per skill
Key Source Files
| File | Purpose |
|---|---|
[src/app/a2a/route.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/app/a2a/route.ts) |
HTTP handler, routing, authentication, method dispatch |
[src/lib/a2a/taskManager.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/a2a/taskManager.ts) |
Task lifecycle, state machine, TTL cleanup |
[src/lib/a2a/taskExecution.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/a2a/taskExecution.ts) |
Skill registry, dynamic imports, execution wrapper |
[src/lib/a2a/streaming.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/a2a/streaming.ts) |
SSE stream creation, event formatting, heartbeat management |
src/lib/a2a/skills/*.ts |
Individual skill implementations |
Summary
The A2A JSON-RPC 2.0 agent protocol in OmniRoute provides:
- Standards-compliant JSON-RPC 2.0 envelope format with four methods:
message/send,message/stream,tasks/get,tasks/cancel - Dual execution modes: synchronous JSON responses and SSE streaming for real-time delivery
- Security-first design: Constant-time
timingSafeEqualauthentication with optional endpoint disable - Pluggable architecture:
A2A_SKILL_HANDLERSregistry with dynamic imports for skill extension - Production reliability: In-memory task tracking with TTL cleanup, heartbeat keepalives, and graceful cancellation
Frequently Asked Questions
What JSON-RPC version does OmniRoute A2A use?
OmniRoute implements JSON-RPC 2.0 strictly. Every request must include "jsonrpc":"2.0" and responses follow the specification's envelope format with jsonrpc, id, and either result or error fields.
How does A2A streaming work in OmniRoute?
The message/stream method returns a Server-Sent Events (SSE) stream via createA2AStream in streaming.ts. The stream emits heartbeat events every 15 seconds, artifact chunks as they're produced, and a final completion or failure event. Clients consume this using standard fetch with ReadableStream readers.
Can I disable the A2A endpoint entirely?
Yes. Set a2aEnabled: false in your OmniRoute settings (checked at getSettings().a2aEnabled in lines 5–18 of route.ts). When disabled, all /a2a requests return an appropriate error response without processing.
How do I add a new skill to the A2A protocol?
Create a TypeScript module in src/lib/a2a/skills/ exporting an async function that accepts A2AParams and returns { artifacts, metadata }. Then add an entry to A2A_SKILL_HANDLERS in taskExecution.ts with a dynamic import wrapper. No changes to route.ts are required.
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 →