# A2A JSON-RPC 2.0 Agent Protocol Implementation in OmniRoute Explained

> Explore the OmniRoute A2A JSON-RPC 2.0 agent protocol implementation. Discover its lightweight HTTP endpoint, SSE streaming, authentication, and task management features.

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

---

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

```typescript
// 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/main/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**: `createFailureEvent` for 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.

```typescript
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) lines 47–65 → [`taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/taskExecution.ts) → skill handler → synchronous return.

### message/stream — SSE Streaming

Returns a streaming response for real-time artifact delivery.

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

```typescript
// 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

1. **Authentication**: `authenticate` helper validates bearer token with `timingSafeEqual` (lines 68–90)
2. **Validation**: JSON-RPC 2.0 schema check for `jsonrpc` field and `method` presence (lines 37–41)
3. **Enablement check**: `getSettings().a2aEnabled` gate (lines 5–18)
4. **Task creation**: `getTaskManager().createTask` generates UUID-v4 task ID (lines 45–66)
5. **Method dispatch**: Branch on `method` field to handler logic (lines 47–65)
6. **Skill execution**: Dynamic import via `A2A_SKILL_HANDLERS[skill]` with state updates
7. **Response formatting**: JSON-RPC envelope construction or SSE stream initiation
8. **Observability**: Routing decisions logged when `smart-routing` skill invoked

## Extending the A2A JSON-RPC 2.0 Implementation

### Adding Custom Skills

1. Create implementation file under `src/lib/a2a/skills/`
2. Export async `execute<SkillName>` function returning `{ artifacts, metadata }`
3. Register in `A2A_SKILL_HANDLERS` in [`taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/taskExecution.ts)

```typescript
// 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/main/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/main/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/main/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/main/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 `timingSafeEqual` authentication with optional endpoint disable
- **Pluggable architecture**: `A2A_SKILL_HANDLERS` registry 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/taskExecution.ts) with a dynamic import wrapper. No changes to [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) are required.