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

> Explore the A2A agent protocol architecture in OmniRoute, Google's agent-to-agent standard. Discover how OmniRoute implements this protocol using a layered architecture.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: architecture
- Published: 2026-08-11

---

**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`](https://github.com/diegosouzapw/OmniRoute/blob/main//.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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts), the OPTIONS handler serves the Agent Card:

```typescript
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.

```typescript
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/skills/smartRouting.ts) | Forwards prompts to OmniRoute's routing pipeline |
| `quota-management` | [`src/lib/a2a/skills/quotaManagement.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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:

```typescript
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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

```bash
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

```bash
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

```python
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts) | JSON-RPC endpoint, auth, dispatch, discovery |
| [`src/lib/a2a/taskManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskManager.ts) | Task lifecycle, TTL, cleanup, statistics |
| [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts) | Skill execution within state machine |
| [`src/lib/a2a/streaming.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/streaming.ts) | SSE stream creation and management |
| [`src/lib/a2a/skills/smartRouting.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/skills/smartRouting.ts) | Core routing skill implementation |
| [`src/lib/a2a/skills/quotaManagement.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/skills/quotaManagement.ts) | Quota query skill |
| [`src/lib/a2a/routingLogger.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/routingLogger.ts) | Observability and analytics |
| [`open-sse/mcp-server/schemas/a2a.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.