NEWn1n v2.0.1 is live! Enterprise Unified LLM API Gateway with 500+ AI Models, up to 90% off,Try now

Building a Real A2A Handoff: Researcher and Writer Over the Agent-to-Agent Protocol

Authors
  • avatar
    Name
    Nino
    Occupation
    Senior Tech Editor

Most contemporary AI agent demos suffer from a hidden architectural limitation: they wire components together inside a single monolithic framework, such as LangChain, LangGraph, or Agno. While this approach works well for building self-contained software, it fails to address the foundational interop challenge. How do independent agents built by different teams, written in different programming languages, and hosted on isolated cloud runtimes talk to each other reliably?

Enter the Agent-to-Agent (A2A) protocol (agent2agent.dev)—an open, vendor-neutral standard often called "HTTP for agents." By leveraging standardized discovery cards, JSON-RPC 2.0 endpoints, and Server-Sent Events (SSE), A2A enables decoupled agent communication without vendor or library lock-in. To power high-performance multi-agent architectures like this, developers often rely on fast, centralized model routing provided by platforms like n1n.ai, which offers reliable unified LLM APIs for high-concurrency workloads.

In this comprehensive tutorial, we will construct a complete, framework-free implementation of an A2A workflow: a Researcher Agent that performs web retrieval via Tavily, handoff execution, and a Writer Agent that streams structured Markdown drafts token-by-token back to a client interface.


1. Architectural Overview and Protocol Specification

The A2A protocol standardizes agent interaction into distinct capabilities:

  1. Discovery via Agent Cards: Every A2A-compliant agent exposes an agent card at GET /.well-known/agent.json or through the agent/getCard JSON-RPC method. This card advertises skills, input/output schemas, supported capabilities (such as live streaming), and authentication specifications.
  2. Task Lifecycle Management: Tasks follow an explicit state machine: submittedworkingcompleted (or failed, canceled, rejected, input-required).
  3. Request Transmission Modes: Synchronous processing uses standard message/send calls over JSON-RPC 2.0, while asynchronous token delivery uses message/stream over Server-Sent Events (SSE).

Here is the architecture of our three-node system:

┌────────────────────────────────────────────────────────────┐
Browser Client (Vite + React)The A2A OrchestratorInput Form · Config Panel · Protocol Timeline · Preview└───────────────┬──────────────────────────────┬─────────────┘
JSON-RPC 2.0 over HTTP (CORS)                ▼                              ▼
   ┌────────────────────┐          ┌────────────────────┐
Researcher Agent  │          │    Writer Agent       (Port 3001)    (Port 3002)   │  streaming: false  │          │  streaming: true   │  message/send      │          │  message/stream    │
   │   ├─ Tavily Search │          │   └─ LLM Stream   │   └─ Model Summarization      │                    │
   └────────────────────┘          └────────────────────┘

Sequence Flow

Browser UI (5173)         Researcher Agent (3001)         Writer Agent (3002)
       │                            │                             │
       │────── agent/getCard ──────>│                             │
<───── Card Response ───────│                             │
       │                                                          │
       │───────────────────────── agent/getCard ─────────────────><──────────────────────── Card Response ──────────────────│
       │                                                          │
       │────── message/send { topic } ───────────────────────────><───── Task Completed { findings, source URLs } ──────────│
       │                                                          │
       │───────────────────────── message/stream { findings } ---><──────────────────────── SSE: working state ─────────────│
<──────────────────────── SSE: message/part (chunks) ─────│
<──────────────────────── SSE: message/complete ──────────│

2. Implementing Agent Discovery Cards

Agent discovery allows an orchestrator to inspect dynamic agent capabilities before assigning tasks. For instance, the Writer Agent advertises its ability to stream content using the capabilities.streaming parameter.

Writer Agent Card JSON Schema

{
  "name": "Writer Agent",
  "description": "Transforms raw research data into structured, publishable drafts via token streaming.",
  "url": "http://localhost:3002",
  "version": "0.1.0",
  "skills": [
    {
      "id": "structured-drafting",
      "name": "Structured Drafting",
      "description": "Generates organized Markdown drafts from supplied research findings."
    }
  ],
  "capabilities": {
    "streaming": true
  },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"]
}

When standardizing your multi-agent architecture across teams, accessing diverse foundational models via n1n.ai simplifies cost management and ensures consistent token streaming benchmarks regardless of whether underlying models switch between DeepSeek-V3, Claude 3.5 Sonnet, or OpenAI o3.


3. Building the Researcher Agent (message/send)

The Researcher Agent handles non-streaming synchronous tasks. When it receives a request, it generates multi-perspective search queries, executes live searches via Tavily, dedupes URLs, and synthesizes results using an LLM.

Implementation Overview (researcher-agent/src/index.ts)

import express, { Request, Response } from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

interface JSONRPCRequest {
  jsonrpc: string;
  method: string;
  params: any;
  id: string | number;
}

app.post('/jsonrpc', async (req: Request, res: Response) => {
  const { jsonrpc, method, params, id } = req.body as JSONRPCRequest;

  if (method === 'agent/getCard') {
    return res.json({
      jsonrpc: '2.0',
      id,
      result: {
        name: 'Researcher',
        capabilities: { streaming: false },
        skills: [{ id: 'web-search', name: 'Web Research' }]
      }
    });
  }

  if (method === 'message/send') {
    const { topic, apiKey } = params;
    
    // 1. Generate search queries using LLM
    const queries = await generateSearchQueries(topic, apiKey);
    
    // 2. Fetch live data from Tavily API
    const sources = await executeTavilySearch(queries);
    
    // 3. Synthesize findings with strict attribution rules
    const summary = await synthesizeFindings(topic, sources, apiKey);

    return res.json({
      jsonrpc: '2.0',
      id,
      result: {
        taskId: crypto.randomUUID(),
        status: { state: 'completed' },
        message: {
          role: 'agent',
          parts: [{ kind: 'text', text: summary }]
        },
        sources: sources.map(s => ({ title: s.title, url: s.url }))
      }
    });
  }

  return res.status(404).json({ error: 'Method not found' });
});

Truthfulness Rule: Graceful Fallbacks

A critical principle in agent protocol design is avoiding hallucinated source data. If no external search key is available, the agent should transparently inform the client:

if (!tavilyKey) \{
  return \{
    taskId: crypto.randomUUID(),
    status: \{ state: 'completed' \},
    message: \{
      role: 'agent',
      parts: [\{ kind: 'text', text: "⚠️ Note: Live search disabled. Output relies entirely on internal model knowledge.