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.
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:
- The SDK is young — created May 2025, 23 open issues at the time
- 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
- Zero dependencies is a feature — enterprise users care about this
- 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 feature — asTool(). 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