How the A2A Agent Protocol Enables Communication Between OmniRoute and Other Agents
OmniRoute implements the A2A (Agent-to-Agent) Protocol v0.3 as a lightweight JSON-RPC 2.0 service that lets autonomous agents discover, invoke, and stream results from one another through a standardized POST /a2a endpoint.
The A2A agent protocol powers OmniRoute's interoperability with external agent systems. This open standard, implemented in the diegosouzapw/OmniRoute repository, transforms the routing engine into a discoverable, callable service that any compliant agent can integrate with. The protocol rests on three architectural pillars: a JSON-RPC 2.0 entry point, a stateful task manager, and a pluggable skill dispatcher.
Core Components of OmniRoute's A2A Implementation
JSON-RPC 2.0 Endpoint
All A2A communication flows through a single canonical entry point. In src/app/a2a/route.ts, the server exposes POST /a2a and accepts four standard methods:
message/send— synchronous request/responsemessage/stream— Server-Sent Events for incremental updatestasks/get— query task status by UUIDtasks/cancel— terminate an in-flight task
The endpoint validates jsonrpc: "2.0" in every payload and returns standard JSON-RPC error codes for malformed requests (-32700), invalid requests (-32600), unknown methods (-32601), or invalid parameters (-32602).
Task Manager
The A2ATaskManager class in src/lib/a2a/taskManager.ts tracks the full lifecycle of every request:
- Submitted → task created with UUID and 5-minute TTL
- Working → skill handler executing
- Completed | Failed | Cancelled → terminal states
The manager enforces timeouts, persists state transitions, and exposes getStats() for observability. Each task carries its skill name, message array, and optional metadata.
Skill Dispatcher
Concrete business logic lives in src/lib/a2a/taskExecution.ts. The A2A_SKILL_HANDLERS registry maps skill names to TypeScript modules. When a task arrives, executeA2ATaskWithState invokes the matching handler, which returns { artifacts, metadata } for the response.
Agent Discovery via the Agent Card
Before invoking OmniRoute, external agents discover capabilities by fetching /.well-known/agent.json. This JSON document, generated in src/app/.well-known/agent.json/route.ts, contains:
- Node name and version (pulled from
package.json) - List of available A2A skills with input schemas
- Authentication requirements
The card is cached for one hour to reduce overhead.
Request Flow Step-by-Step
Understanding how the A2A agent protocol processes a request helps with debugging and integration:
- Authentication check — If
OMNIROUTE_API_KEYis configured, theAuthorization: Bearer <token>header must match; otherwise the endpoint remains open - Enabled toggle — The
rejectIfA2ADisabledhelper checksa2aEnabledin settings, returning error-32000if disabled - JSON-RPC parsing — Request body validated against the 2.0 specification
- Task creation —
taskManager.createTaskinstantiatesA2ATaskwith UUID and TTL - Skill execution — Handler from
A2A_SKILL_HANDLERSreceives the task object - State updates — Manager transitions through
workingtocompletedorfailed - Response formatting — Single JSON object for
message/send, SSE stream formessage/stream
Built-In A2A Skills
OmniRoute ships with six production-ready skills in src/lib/a2a/skills/:
| Skill | Source File | Purpose |
|---|---|---|
| Smart Routing | smartRouting.ts |
Selects optimal provider/combo for prompts |
| Quota Management | quotaManagement.ts |
Reports per-provider quota consumption |
| Provider Discovery | providerDiscovery.ts |
Lists installed providers and capabilities |
| Cost Analysis | costAnalysis.ts |
Estimates request or conversation cost |
| Health Report | healthReport.ts |
Summarizes circuit-breaker and provider health |
| List Capabilities | listCapabilities.ts |
Returns full skill catalog for dynamic discovery |
Adding custom skills requires creating a module under src/lib/a2a/skills/ and registering it in A2A_SKILL_HANDLERS.
Streaming Support with Server-Sent Events
The message/stream method enables real-time collaboration. Implemented in src/lib/a2a/streaming.ts, this returns SSE with headers defined in SSE_HEADERS. Calling agents receive:
- Incremental artifacts (partial text generations)
- Progress metadata
- Final state and complete artifacts
This avoids blocking waits for long-running operations like multi-provider routing chains.
Code Examples for A2A Integration
Synchronous Request with cURL
curl -X POST http://localhost:20128/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"skill": "smart-routing",
"messages": [{"role": "user", "content": "Write a hello world program in Python"}],
"metadata": {"model": "auto", "combo": "fast-coding"}
}
}'
This matches the documented snippet in docs/frameworks/A2A-SERVER.md lines 55-68.
Streaming Response in Node.js
import fetch from "node-fetch";
const resp = await fetch("http://localhost:20128/a2a", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_KEY",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "stream-1",
method: "message/stream",
params: {
skill: "smart-routing",
messages: [{ role: "user", content: "Explain quantum computing in simple terms" }],
},
}),
});
for await (const line of resp.body) {
console.log(line.toString());
}
The streaming logic is defined in src/lib/a2a/streaming.ts.
Query Task Status
curl -X POST http://localhost:20128/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{"jsonrpc":"2.0","id":"2","method":"tasks/get","params":{"taskId":"<TASK_UUID>"}}'
The tasks/get handler appears in src/app/a2a/route.ts approximately lines 90-110.
Discover Available Capabilities
curl http://localhost:20128/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{"jsonrpc":"2.0","id":"cap","method":"list-capabilities","params":{}}'
This invokes the skill in src/lib/a2a/skills/listCapabilities.ts.
Observability and Logging
Every smart-routing decision is logged via logRoutingDecision in src/lib/a2a/routingLogger.ts. The task manager's getStats() method exposes:
- Task counts per state (submitted, working, completed, failed, cancelled)
- Total tasks processed
- Active SSE stream count
Key Implementation Files
| File | Role in A2A Agent Protocol |
|---|---|
src/app/a2a/route.ts |
Main JSON-RPC router, method dispatch, error handling |
src/lib/a2a/taskManager.ts |
Task lifecycle, state machine, TTL enforcement |
src/lib/a2a/taskExecution.ts |
Skill handler registry, execution wrapper |
src/lib/a2a/streaming.ts |
SSE stream creation and header constants |
src/lib/a2a/skills/*.ts |
Concrete skill implementations |
docs/frameworks/A2A-SERVER.md |
Authoritative protocol documentation |
src/app/.well-known/agent.json/route.ts |
Agent Card generation for discovery |
Summary
- The A2A agent protocol in OmniRoute v0.3 uses JSON-RPC 2.0 over HTTP with a single
POST /a2aendpoint - Agent discovery works via
/.well-known/agent.jsonfor capability advertisement - Six built-in skills handle routing, quotas, providers, cost, health, and capability listing
- Streaming responses use Server-Sent Events for incremental results without blocking
- Task lifecycle management includes UUID tracking, 5-minute TTL, and state transitions
- Pluggable architecture lets developers add custom skills under
src/lib/a2a/skills/
Frequently Asked Questions
What authentication does the A2A endpoint require?
Authentication is optional and controlled by the OMNIROUTE_API_KEY environment variable. When set, all requests must include Authorization: Bearer <token> matching that key. If unset, the endpoint accepts unauthenticated requests. The authenticate function in src/app/a2a/route.ts enforces this policy.
How do I enable or disable A2A functionality?
A2A is disabled by default. Set a2aEnabled: true in your OmniRoute configuration to activate the endpoint. When disabled, the rejectIfA2ADisabled helper returns JSON-RPC error -32000 for any incoming request.
Can I add custom skills to OmniRoute's A2A implementation?
Yes. Create a TypeScript module under src/lib/a2a/skills/ exporting a handler function, then register it in A2A_SKILL_HANDLERS within src/lib/a2a/taskExecution.ts. The handler receives the full A2ATask object and must return { artifacts, metadata }. The existing listCapabilities.ts skill demonstrates the required pattern.
What happens when a task exceeds its time limit?
The task manager enforces a default 5-minute TTL on all tasks. Expired tasks transition to failed state automatically. This prevents resource leaks from abandoned or hung requests.
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 →