A2A Agent Protocol Architecture in OmniRoute: A Deep Dive into Google's Agent-to-Agent Standard

OmniRoute implements the Google A2A (v0.3) protocol as a first-class A2A Server that other agents can discover, call, and stream results from, using a layered architecture built on top of its existing /v1/chat/completions endpoint.

The A2A agent protocol architecture in OmniRoute enables seamless interoperability between AI agents through a JSON-RPC 2.0 interface. Rather than building a separate stack, OmniRoute extends its production routing pipeline to support agent-to-agent communication, allowing external agents to leverage its smart routing capabilities through a standardized protocol.

Core Architecture Layers

OmniRoute's A2A implementation consists of nine coordinated layers, each handling a specific concern in the agent communication lifecycle.

Discovery Layer: Agent Card

The discovery layer exposes an Agent Card at /.well-known/agent.json that advertises the server's URL, version, capabilities, and available skills. This follows the A2A specification for automatic agent discovery.

In src/app/a2a/route.ts, the OPTIONS handler serves the Agent Card:

// src/app/a2a/route.ts - Agent Card discovery endpoint
export async function OPTIONS(request: NextRequest) {
  return NextResponse.json({
    name: "OmniRoute A2A Server",
    version: "0.3.0",
    url: "http://localhost:20128/a2a",
    skills: [
      { id: "smart-routing", name: "Smart Routing", description: "Route prompts to optimal LLM providers" },
      { id: "quota-management", name: "Quota Management", description: "Query provider quotas and usage" }
    ]
  });
}

Any A2A-compatible agent can fetch this card to understand what the server offers before initiating communication.

JSON-RPC Router Layer

The JSON-RPC router in src/app/a2a/route.ts handles all /a2a POST traffic. It validates authentication using constant-time bearer token comparison (tokensMatch), parses requests, dispatches to skill handlers, and returns either JSON-RPC responses or SSE streams.

// Authentication check in src/app/a2a/route.ts
const authHeader = request.headers.get("authorization") || "";
const token = authHeader.replace("Bearer ", "");
if (!tokensMatch(token, process.env.OMNIROUTE_API_KEY || "")) {
  return jsonRpcError("1", -32000, "Unauthorized");
}

If no OMNIROUTE_API_KEY is configured, the endpoint operates in open mode—suitable for development, not production.

Task Manager: Stateful Lifecycle Control

The task manager in src/lib/a2a/taskManager.ts implements an in-memory state machine with optional persistence:


submitted → working → completed | failed | cancelled

Key responsibilities include:

  • UUIDv4 task ID generation for unique task tracking
  • TTL enforcement with automatic cleanup of expired tasks
  • Per-state event recording for audit trails
  • Statistics aggregation for monitoring

This layer ensures that long-running agent tasks remain trackable even across temporary disconnections.

Task Execution Layer

The executeA2ATaskWithState function in src/lib/a2a/taskExecution.ts wraps skill handlers within the task lifecycle. It:

  1. Transitions task to working state
  2. Invokes the selected skill handler
  3. Captures results or errors
  4. Updates to completed, failed, or cancelled

Errors propagate back to the JSON-RPC response with appropriate codes and messages.

Skills Registry: Pluggable Business Logic

Skills are registered in a A2A_SKILL_HANDLERS map, with each skill ID mapping to an async handler:

Skill ID Handler File Purpose
smart-routing src/lib/a2a/skills/smartRouting.ts Forwards prompts to OmniRoute's routing pipeline
quota-management src/lib/a2a/skills/quotaManagement.ts Natural-language quota and usage queries

Each skill implements its own business logic while conforming to a standard interface: (params: any, taskId: string) => Promise<any>.

Smart Routing Skill Implementation

The smart-routing skill is the flagship capability. It receives a prompt and metadata, forwards it to OmniRoute's existing /v1/chat/completions infrastructure, then enriches the response with routing metadata:

// src/lib/a2a/skills/smartRouting.ts - conceptual flow
export async function smartRoutingHandler(params: any, taskId: string) {
  const { messages, metadata } = params;
  
  // Use OmniRoute's production routing engine
  const routingResult = await routeToOptimalProvider({
    messages,
    modelPreference: metadata?.model || "auto",
    budgetConstraint: metadata?.budget
  });
  
  return {
    artifacts: [{ type: "text", content: routingResult.content }],
    metadata: {
      routing_explanation: routingResult.explanation,
      provider: routingResult.provider,
      model: routingResult.model,
      actual_cost: routingResult.cost,
      latency_ms: routingResult.latency
    }
  };
}

Routing Logger for Observability

After each successful smart-routing call, src/lib/a2a/routingLogger.ts records a compact representation containing:

  • Selected combo (provider + model)
  • Actual cost and latency
  • Routing explanation
  • Timestamp and task correlation

This enables analytics dashboards and cost attribution across agent-to-agent traffic.

SSE Streaming Layer

The createA2AStream function in src/lib/a2a/streaming.ts converts task execution into Server-Sent Events. It manages:

  • Stream initiation via beginStream
  • Chunk-by-chunk delivery of partial results
  • Stream termination via endStream with final state

Clients using message/stream receive data: lines containing JSON-RPC notification objects until the task reaches a terminal state.

Schema Validation Layer

All A2A data structures are validated using Zod schemas in open-sse/mcp-server/schemas/a2a.ts:

  • Agent Card structure
  • Task lifecycle objects
  • JSON-RPC request/response envelopes
  • SSE event shapes

This ensures type-safe handling across the entire pipeline and catches malformed requests early.

Complete Request Flow


External Agent
      ↓
GET /.well-known/agent.json (discover capabilities)
      ↓
POST /a2a with JSON-RPC body
      ↓
src/app/a2a/route.ts
  ├── Authenticate (bearer token check)
  ├── Parse method (message/send, message/stream, tasks/get, tasks/cancel)
  ├── Create task via taskManager
  └── Dispatch to skill handler
        ↓
src/lib/a2a/taskExecution.ts
  └── Execute skill within state machine
        ↓
src/lib/a2a/skills/smartRouting.ts
  └── Call OmniRoute /v1/chat/completions pipeline
        ↓
src/lib/a2a/routingLogger.ts (record decision)
        ↓
Return result or SSE stream

Client Integration Examples

Synchronous Task Execution

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 Python hello world"}],
      "metadata": {"model":"auto","budget":0.10}
    }
  }'

Streaming with SSE

curl -N -X POST http://localhost:20128/a2a \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_KEY" \
  -d '{
    "jsonrpc":"2.0",
    "id":"stream-1",
    "method":"message/stream",
    "params":{"skill":"smart-routing","messages":[{"role":"user","content":"Explain quantum computing"}]}
  }'

Python Client with Discovery

import requests, json

BASE = "http://localhost:20128"
HEADERS = {"Content-Type":"application/json","Authorization":"Bearer YOUR_KEY"}

# Discover capabilities

card = requests.get(f"{BASE}/.well-known/agent.json").json()
skills = [s['id'] for s in card['skills']]
print(f"Agent {card['name']} v{card['version']} – skills: {skills}")

# Execute smart-routing task

resp = requests.post(f"{BASE}/a2a", headers=HEADERS, json={
    "jsonrpc":"2.0","id":"task-1","method":"message/send",
    "params":{"skill":"smart-routing",
              "messages":[{"role":"user","content":"Write quicksort in Python"}],
              "metadata":{"model":"auto","budget":0.05}}
})
result = resp.json()["result"]
print("Response:", result["artifacts"][0]["content"])
print("Routing:", result["metadata"]["routing_explanation"])

Key Design Decisions

Decision Rationale
Reuse existing pipeline Smart-routing skill calls the same /v1/chat/completions infrastructure used by direct clients—no code duplication
In-memory task state by default Low latency for typical use cases; persistence pluggable for durability requirements
Constant-time token comparison Prevents timing attacks on the authentication layer
Zod for validation TypeScript-native, excellent error messages, runtime and static type alignment
SSE over WebSockets Simpler infrastructure, firewall-friendly, aligns with A2A specification

File Reference

Path Responsibility
src/app/a2a/route.ts JSON-RPC endpoint, auth, dispatch, discovery
src/lib/a2a/taskManager.ts Task lifecycle, TTL, cleanup, statistics
src/lib/a2a/taskExecution.ts Skill execution within state machine
src/lib/a2a/streaming.ts SSE stream creation and management
src/lib/a2a/skills/smartRouting.ts Core routing skill implementation
src/lib/a2a/skills/quotaManagement.ts Quota query skill
src/lib/a2a/routingLogger.ts Observability and analytics
open-sse/mcp-server/schemas/a2a.ts Zod schema definitions

Summary

  • OmniRoute's A2A architecture implements Google A2A v0.3 as a server that exposes smart routing capabilities to external agents
  • Nine coordinated layers handle discovery, routing, task lifecycle, execution, streaming, and observability
  • Skills-based extensibility allows new agent capabilities to be added without modifying core protocol code
  • Production-hardened design reuses the existing /v1/chat/completions pipeline while adding minimal A2A-specific overhead
  • Type safety throughout via Zod schemas with runtime validation

Frequently Asked Questions

What version of the A2A protocol does OmniRoute support?

OmniRoute implements Google A2A v0.3 according to the source code in src/lib/a2a/README.md and the Agent Card version fields. This is the latest stable specification as of the v3.8.50 release.

Can I add custom skills to the A2A server?

Yes. The skills registry in src/app/a2a/route.ts uses a simple handler map pattern. Create a new file in src/lib/a2a/skills/, export an async handler function with the signature (params: any, taskId: string) => Promise<any>, and register it in A2A_SKILL_HANDLERS with a unique skill ID.

How does authentication work for A2A endpoints?

Authentication uses bearer token validation with constant-time comparison (tokensMatch). Set OMNIROUTE_API_KEY in your environment, then include Authorization: Bearer YOUR_KEY in requests. If the environment variable is unset, the endpoint accepts all requests—useful for development but not recommended for production.

What's the difference between message/send and message/stream?

message/send executes synchronously and returns the complete result as a single JSON-RPC response. message/stream delivers partial output via Server-Sent Events, with each chunk containing a JSON-RPC notification object. Streaming is preferred for long-generation tasks or when lower time-to-first-token is desired.

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 →