← Back to Blog
5 min read

I Implemented Google's A2A Protocol From Scratch in TypeScript

How I built native Agent-to-Agent protocol support for Cogitator — 1500 lines, 119 tests, zero dependencies, one evening.

#ai#typescript#a2a#agents#open-source

I Implemented Google's A2A Protocol From Scratch in TypeScript

A few months ago I started building Cogitator — an open-source AI agent runtime for TypeScript. Think Kubernetes, but for AI agents. Self-hosted, production-grade, no vendor lock-in.

The project grew fast. 26 packages, 10+ LLM providers, workflows, swarms, memory/RAG, sandboxing — the whole deal. But something was missing.

Agents couldn't talk to agents from other frameworks.

Your Cogitator agent couldn't communicate with a LangChain agent, or a CrewAI agent, or anything else. They were all isolated islands.

Enter A2A Protocol

In April 2025, Google released the Agent-to-Agent Protocol (A2A) — an open standard for agent interoperability. JSON-RPC 2.0 over HTTPS, with Agent Cards for discovery, task lifecycle management, and SSE streaming.

Think of it like HTTP for AI agents. A universal language so any agent can talk to any other agent, regardless of what framework built it.

The spec is backed by 50+ companies and now lives under the Linux Foundation. This isn't some random proposal — it's becoming the standard.

Why Build It From Scratch?

There's an official JavaScript SDK (@a2a-js/sdk). I could've just wrapped it. But:

  1. The SDK is young — created May 2025, 23 open issues at the time
  2. Cogitator already has its own patterns — we have 5 server adapters (Express, Hono, Fastify, Koa, Next.js), our own streaming protocol, our own tool system
  3. Zero dependencies is a feature — enterprise users care about this
  4. I wanted to understand the protocol deeply — you don't learn by wrapping someone else's code

So I read the spec cover to cover and implemented everything from scratch.

What I Built

The @cogitator-ai/a2a package in one evening:

Server side — expose any Cogitator agent as an A2A service:

import { A2AServer } from '@cogitator-ai/a2a';

import { a2aExpress } from '@cogitator-ai/a2a/express';

const a2aServer = new A2AServer({ agents: { researcher: myAgent }, cogitator, });

app.use(a2aExpress(a2aServer)); // GET /.well-known/agent.json — Agent Card

// POST /a2a — JSON-RPC endpoint

Client side — connect to any A2A agent:

import { A2AClient } from '@cogitator-ai/a2a';

const client = new A2AClient('https://remote-agent.example.com'); const card = await client.agentCard(); const task = await client.sendMessage({ role: 'user', parts: [{ type: 'text', text: 'Research quantum computing' }],

});

The killer featureasTool(). Wrap any remote A2A agent as a local tool:

const remoteTool = client.asToolFromCard(await client.agentCard());

const orchestrator = new Agent({ tools: [remoteTool], // remote agent looks like a regular tool instructions: 'Use the researcher to gather information.',

});

The orchestrator agent doesn't know or care that the researcher lives on another server. It just uses it like any other tool. The A2A protocol handles everything underneath.

The Architecture

I designed it as a single package with subpath exports:

@cogitator-ai/a2a          — core (server, client, types)

@cogitator-ai/a2a/express — Express adapter @cogitator-ai/a2a/hono — Hono adapter @cogitator-ai/a2a/fastify — Fastify adapter @cogitator-ai/a2a/koa — Koa adapter

@cogitator-ai/a2a/next — Next.js adapter

Internally:

  • JSON-RPC 2.0 layer — own parser/serializer, no deps
  • Agent Card generation — auto-creates A2A cards from Cogitator Agent metadata
  • Task Manager — full lifecycle (working → completed/failed/canceled) with EventEmitter for streaming
  • SSE streaming — both server (AsyncGenerator pattern) and client (manual frame parsing)
  • TaskStore — pluggable storage, in-memory by default

The A2AServer is completely framework-agnostic. It takes raw JSON and returns JSON. The adapters are thin wrappers (50-80 lines each) that handle HTTP plumbing.

Numbers

  • 1,500 lines of production code
  • 1,447 lines of test code
  • 119 tests across 8 test files
  • 0 external dependencies (only workspace packages + zod)
  • 5 framework adapters
  • Built in one evening

Lessons Learned

Implementing a protocol from spec is underrated. You understand every decision, every trade-off. When a bug shows up, you know exactly where to look because you wrote every line.

SSE is trickier than it looks. The EventSource API only works with GET. For POST-based streaming (which A2A uses), you need to parse SSE frames manually on the client. Buffer management, frame delimiting, handling partial chunks — it adds up.

Race conditions hide in async generators. My initial streaming implementation had a subtle bug — reattaching .then() to an already-resolved Promise on every loop iteration. A code review caught it before it shipped.

Agent Cards are a great idea. The concept of a machine-readable manifest describing what an agent can do, how to authenticate, what it accepts — this is the DNS of the agent world.

What's Next

This is v1. On the roadmap for v2:

  • Push notifications (webhooks)
  • gRPC binding
  • Agent Card signing
  • Multi-turn conversations with contextId
  • RedisTaskStore for production persistence
  • Token-level streaming

Try It

npm install @cogitator-ai/a2a

If you're building AI agents in TypeScript, give Cogitator a look. And if you find bugs in the A2A implementation — PRs welcome.

Stay curious, Pavel