How the A2A Agent Protocol Works with OmniRoute: A Technical Deep Dive

OmniRoute exposes an Agent-to-Agent (A2A) JSON-RPC 2.0 endpoint at POST /a2a that authenticates requests via Bearer tokens, manages task lifecycles through an in-memory state machine with configurable TTL, and dispatches to pluggable skill handlers for smart routing and quota management.

The OmniRoute repository (diegosouzapw/OmniRoute) implements the A2A protocol to allow external AI agents to interact with its intelligent routing layer. This protocol enables synchronous and streaming communication patterns while enforcing strict security boundaries and resource quotas through a structured JSON-RPC interface.

Architecture of the A2A Protocol Implementation

Entry Point and Authentication

The canonical entry point resides in src/app/a2a/route.ts, which exposes a single POST /a2a endpoint. This handler validates the JSON-RPC envelope and authenticates the caller using an Authorization: Bearer <API-KEY> header. According to the source code, if no API key is configured in the environment, the authentication check is bypassed strictly for local development scenarios.

Task Manager and State Machine

The core lifecycle management lives in src/lib/a2a/taskManager.ts. The getTaskManager() singleton creates A2ATask instances identified by UUID v4, tracking states through a deterministic state machine: submitted → working → completed|failed|cancelled. Tasks reside in an in-memory Map with a default 5-minute TTL and optional SQLite persistence. The manager enforces owner scoping—referencing security advisory GHSA‑jcm5‑6wpp‑wjj8—to ensure tasks created under a hashed API key remain visible only to that principal.

Skill Dispatch Registry

Concrete execution logic resides in src/lib/a2a/taskExecution.ts, which exports A2A_SKILL_HANDLERS—a registry that lazily imports skill modules such as smart-routing and quota-management. The executeA2ATaskWithState helper orchestrates execution: it runs the selected skill, captures artifacts, and transitions the task state to completed or failed accordingly.

Streaming Layer

For real-time responses, src/lib/a2a/streaming.ts implements Server-Sent Events (SSE). The createA2AStream function returns a ReadableStream that formats SSE chunks via createChunkEvent, emits createHeartbeat events every 15 seconds, and terminates with either createCompletionEvent or createFailureEvent depending on the skill outcome. The stream also honors an optional abort signal for cancellation.

Protocol Execution Flow

The A2A protocol follows a strict 9-step lifecycle when processing agent requests:

  1. JSON-RPC Request: The client posts to POST /a2a with methods such as message/send or message/stream.

  2. Authentication: The route handler validates the Authorization Bearer token against the configured API key.

  3. Task Creation: getTaskManager().createTask() allocates a new A2ATask in the submitted state with a unique UUID.

  4. Skill Selection: The skill field in the JSON-RPC params selects a handler from the A2A_SKILL_HANDLERS registry.

  5. Skill Execution: The handler runs—for example, smartRouting.ts calls OmniRoute’s internal /v1/chat/completions endpoint to fulfill the request.

  6. State Update: On success, the manager calls updateTask(..., "completed"); on error, it transitions the state to "failed".

  7. SSE Streaming: For message/stream, createA2AStream drives the response with partial content chunks and heartbeats until resolution.

  8. Final Response: The JSON-RPC response includes the task ID, final state, artifacts, and rich metadata including routing_explanation, cost_envelope, resilience_trace, and policy_verdict.

  9. Lifecycle Management: A background process runs every minute to expire non-terminal tasks after TTL and purge terminal tasks after 2× TTL (10 minutes).

Practical Code Examples

Synchronous JSON-RPC Call

To invoke the smart-routing skill synchronously, send a standard JSON-RPC request:

POST http://localhost:20128/a2a
Content-Type: application/json
Authorization: Bearer YOUR_OMNIROUTE_API_KEY

{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "message/send",
  "params": {
    "skill": "smart-routing",
    "messages": [{ "role": "user", "content": "Explain quantum computing" }],
    "metadata": { "model": "auto", "budget": 0.01 }
  }
}

The response includes detailed execution metadata:

{
  "jsonrpc": "2.0",
  "id": "1",
  "result": {
    "task": { "id": "e3b0c442-…", "state": "completed" },
    "artifacts": [{ "type": "text", "content": "Quantum computing..." }],
    "metadata": {
      "routing_explanation": "Selected gpt‑4 via provider \"openai\" (latency: 842ms, cost: $0.004)",
      "cost_envelope": { "estimated": 0.005, "actual": 0.004, "currency": "USD" },
      "resilience_trace": [{ "event": "primary_selected", "provider": "openai", "timestamp": "2026‑08‑28T12:34:56Z" }],
      "policy_verdict": { "allowed": true, "reason": "within budget and quota limits" }
    }
  }
}

Streaming with Server-Sent Events

For real-time streaming responses, invoke the message/stream method:

curl -N -X POST http://localhost:20128/a2a \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_OMNIROUTE_API_KEY" \
  -d '{
        "jsonrpc":"2.0","id":"2","method":"message/stream",
        "params":{"skill":"smart-routing","messages":[{"role":"user","content":"Write a hello world in Rust"}]}
      }'

The SSE output includes chunked data events and periodic heartbeats:


data: {"jsonrpc":"2.0","method":"message/stream","params":{"task":{"id":"…","state":"working"},"chunk":{"type":"text","content":"fn main() {"}}}

: heartbeat 2026-08-28T12:35:15.123Z

data: {"jsonrpc":"2.0","method":"message/stream","params":{"task":{"id":"…","state":"completed"},"metadata":{"routing_explanation":"...","cost_envelope":{"estimated":0.004,"actual":0.003,"currency":"USD"}}}}

Internal Task Manager Usage

Developers can interact with the task manager directly for custom skill implementations:

import { getTaskManager } from "@/src/lib/a2a/taskManager";

const manager = getTaskManager();
const task = manager.createTask({
  skill: "quota-management",
  messages: [],
  metadata: {}
});

// Later, from within a skill implementation:
manager.updateTask(task.id, "completed", [{ type: "text", content: "All quotas OK" }]);

Summary

  • OmniRoute exposes a JSON-RPC 2.0 endpoint at POST /a2a defined in src/app/a2a/route.ts for Agent-to-Agent communication.
  • The Task Manager in src/lib/a2a/taskManager.ts maintains UUID-identified tasks with a submitted → working → completed|failed|cancelled state machine, 5-minute TTL, and API-key-based owner isolation.
  • Skill execution occurs through the A2A_SKILL_HANDLERS registry in src/lib/a2a/taskExecution.ts, enabling lazy-loaded modules like smart-routing to invoke internal OmniRoute APIs.
  • Streaming support via src/lib/a2a/streaming.ts provides SSE with 15-second heartbeats through createA2AStream, supporting abort signals for cancellation.
  • The protocol enforces authentication via Bearer tokens and returns rich metadata including cost envelopes, routing explanations, and policy verdicts.

Frequently Asked Questions

What authentication method does OmniRoute require for A2A protocol access?

OmniRoute requires an Authorization: Bearer <API-KEY> header for all requests to the POST /a2a endpoint. As implemented in src/app/a2a/route.ts, the handler validates this token against configured environment variables; if no key is set, authentication is bypassed for local development only.

How long do A2A tasks persist in the OmniRoute system?

Tasks live in an in-memory Map with a default 5-minute TTL. A background cleanup process runs every minute to mark expired non-terminal tasks as failed. Terminal tasks (completed, failed, or cancelled) are purged after 2× TTL (10 minutes by default), though optional SQLite persistence can retain them longer per the configuration in src/lib/a2a/taskManager.ts.

Can developers add custom skills to OmniRoute's A2A protocol?

Yes. The A2A_SKILL_HANDLERS registry in src/lib/a2a/taskExecution.ts lazily imports skill modules based on the skill parameter in the JSON-RPC request. Developers can extend the registry with new handlers following the pattern established in src/lib/a2a/skills/smartRouting.ts, which demonstrates how to call internal OmniRoute APIs and return structured artifacts.

Does OmniRoute support real-time streaming via the A2A protocol?

Yes. The message/stream method triggers Server-Sent Events (SSE) through createA2AStream in src/lib/a2a/streaming.ts. This streams partial content as createChunkEvent objects, emits createHeartbeat events every 15 seconds, and terminates with either createCompletionEvent or createFailureEvent based on the skill execution result, while respecting client-side abort signals.

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 →