A2A Protocol (Agent-to-Agent)
Native implementation of Google's A2A Protocol v0.3 for cross-framework agent interoperability.
Overview
The @cogitator-ai/a2a package implements Google's A2A Protocol v0.3 — the open standard for agent-to-agent communication. It enables Cogitator agents to interoperate with agents from any framework (LangChain, CrewAI, etc.) via a standardized JSON-RPC protocol.
- A2AServer -- expose any Cogitator agent as an A2A-compliant service with auto-generated Agent Cards
- A2AClient -- connect to any remote A2A agent, with discovery, messaging, and streaming
- asTool() Bridge -- wrap remote A2A agents as local Cogitator tools for seamless delegation
- Framework Adapters -- Express, Hono, Fastify, Koa, Next.js
pnpm add @cogitator-ai/a2aA2AServer
Exposing Agents
Create an A2A server to expose your agents to any A2A-compatible client:
import { Cogitator, Agent } from '@cogitator-ai/core';
import { A2AServer } from '@cogitator-ai/a2a';
import { a2aExpress } from '@cogitator-ai/a2a/express';
import express from 'express';
const cogitator = new Cogitator();
const researcher = new Agent({
name: 'researcher',
description: 'Research agent that finds information',
model: 'openai/gpt-6.1-sol',
instructions: 'You are a research assistant.',
});
const a2aServer = new A2AServer({
agents: { researcher },
cogitator,
cardUrl: 'https://my-server.com',
});
const app = express();
app.use(a2aExpress(a2aServer));
app.listen(3000);This exposes:
GET /.well-known/agent.json-- Agent Card discoveryPOST /a2a-- JSON-RPC endpoint for messaging, streaming, and task management
basePath (default /a2a, must start with /) is the JSON-RPC path the adapters serve, relative to where they are mounted, and the card URL when cardUrl is unset; the server exposes it as a2aServer.basePath. For Next.js, put the POST route file at that path. cardUrl is the url the cards advertise.
const a2aServer = new A2AServer({ agents: { researcher }, cogitator, basePath: '/rpc' });
app.use(a2aExpress(a2aServer)); // POST /rpc
const client = new A2AClient('http://localhost:3000', { rpcPath: a2aServer.basePath });Agent Cards
Agent Cards are auto-generated from your Agent configuration. Each tool becomes a skill in the card:
const card = a2aServer.getAgentCard('researcher');{
"name": "researcher",
"description": "Research agent that finds information",
"url": "https://my-server.com",
"version": "0.3",
"capabilities": {
"streaming": true,
"pushNotifications": false,
"extendedAgentCard": false
},
"skills": [
{
"id": "web_search",
"name": "web_search",
"description": "Search the web for information",
"inputModes": ["text/plain", "application/json"],
"outputModes": ["text/plain", "application/json"]
}
],
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain", "application/json"]
}capabilities.pushNotifications is true when you pass a pushNotificationStore, and extendedAgentCard when you pass an extendedCardGenerator. With auth, the card also lists securitySchemes and security. getAgentCards() returns every card, signed when cardSigning is set.
Multiple Agents
Register multiple agents on a single server (at least one is required):
const a2aServer = new A2AServer({
agents: {
researcher: researchAgent,
writer: writerAgent,
analyst: analystAgent,
},
cogitator,
});/.well-known/agent.json then returns an array of cards. message/send, message/stream and agent/extendedCard pick the agent from an agentName param and fall back to the first registered agent. To reach another agent from A2AClient, pass its registered name:
const writer = new A2AClient('https://my-server.com', { agentName: 'writer' });
const card = await writer.agentCard();
const task = await writer.sendMessage({
role: 'user',
parts: [{ type: 'text', text: 'Draft a summary' }],
});The client sends agentName with message/send, message/stream and agent/extendedCard, and agentCard() returns the card of that name (-32007 Agent not found when the server has none).
A2AClient
Connecting to a Remote Agent
import { A2AClient } from '@cogitator-ai/a2a';
const client = new A2AClient('https://remote-agent.example.com');
const card = await client.agentCard();
console.log(`Agent: ${card.name}`);
console.log(`Skills: ${card.skills.map((s) => s.name).join(', ')}`);Sending Messages
const task = await client.sendMessage({
role: 'user',
parts: [{ type: 'text', text: 'Research quantum computing trends' }],
});
console.log(task.status.state);
for (const artifact of task.artifacts) {
for (const part of artifact.parts) {
if (part.type === 'text') console.log(part.text);
}
}Streaming
for await (const event of client.sendMessageStream({
role: 'user',
parts: [{ type: 'text', text: 'Analyze market trends' }],
})) {
if (event.type === 'status-update') {
console.log(`Status: ${event.status.state}`);
} else if (event.type === 'artifact-update') {
console.log(`Artifact: ${event.artifact.id}`);
}
}Artifact events arrive before the final status, and the stream ends on a terminal state or input-required. The client timeout is an idle timeout between events; pass { signal } as the third argument to cancel (the server aborts the agent run when the client disconnects).
Send Configuration
| Option | Effect |
|---|---|
blocking: false | Return the task immediately in working state |
historyLength | Return only the last N history messages (0 = none); also on getTask |
acceptedOutputModes | Only return artifacts with these MIME types |
timeout | Maximum agent run time in ms |
pushNotificationConfig | Register a webhook before the task starts executing |
const task = await client.sendMessage(
{ role: 'user', parts: [{ type: 'text', text: 'Long running analysis' }] },
{ blocking: false, pushNotificationConfig: { webhookUrl: 'https://my-app.com/hooks/a2a' } }
);Task Management
const task = await client.getTask('task_abc123', 5);
await client.cancelTask('task_abc123');asTool() Bridge
Wrap any remote A2A agent as a local Cogitator tool:
const client = new A2AClient('https://research-agent.example.com');
const card = await client.agentCard();
const remoteTool = client.asToolFromCard(card, {
name: 'remote_researcher',
description: 'Delegate research to remote agent',
});
const orchestrator = new Agent({
name: 'orchestrator',
model: 'openai/gpt-6.1-sol',
instructions: 'Use the remote_researcher for information gathering.',
tools: [remoteTool],
});
const result = await cogitator.run(orchestrator, {
input: 'Write a report on AI trends',
});The orchestrator agent calls the remote researcher via A2A protocol transparently -- it looks like a regular tool call with a single task string parameter. asToolFromCard names and describes the tool from the card unless you override them; client.asTool({ name?, description?, timeout? }) does the same without a card (default name a2a_remote_agent). The tool is marked with the external side effect and forwards the run's abort signal.
The tool result is { output, success, error?, taskId?, state? }; when the remote agent asks a question (state: 'input-required'), output holds the question and taskId lets you continue the task.
Multi-Turn Conversations
A2A supports multi-turn conversations where follow-up messages continue an existing task. Each task belongs to a contextId that groups related tasks into a conversation.
Client-Side
Use continueTask to send follow-up messages to an existing task:
const client = new A2AClient('https://remote-agent.example.com');
const task = await client.sendMessage({
role: 'user',
parts: [{ type: 'text', text: 'Research quantum computing' }],
});
const followUp = await client.continueTask(task.id, 'Now compare it with classical computing');
console.log(task.contextId === followUp.contextId); // trueServer-Side
On the server, multi-turn is handled automatically. When a message includes a taskId, the server appends the new message to the existing task history and re-runs the agent with the task transcript as context, so follow-ups work even without a memory adapter. Runs are threaded by contextId, artifacts accumulate across turns, and a task that is still running cannot be continued concurrently.
Listing Tasks
Query stored tasks with filtering and pagination via the tasks/list method:
const client = new A2AClient('https://remote-agent.example.com');
const allTasks = await client.listTasks();
const conversationTasks = await client.listTasks({
contextId: 'ctx_abc123',
});
const completedTasks = await client.listTasks({
state: 'completed',
limit: 10,
offset: 0,
});Tasks are returned sorted by timestamp (newest first). The TaskFilter supports:
| Field | Type | Description |
|---|---|---|
contextId | string | Filter by conversation context |
state | TaskState | Filter by task state |
limit | number | Max number of tasks to return |
offset | number | Number of tasks to skip |
Token Streaming
SSE streaming now includes TokenStreamEvent alongside status and artifact updates, giving clients real-time character-by-character output:
for await (const event of client.sendMessageStream({
role: 'user',
parts: [{ type: 'text', text: 'Write a poem about the ocean' }],
})) {
switch (event.type) {
case 'token':
process.stdout.write(event.token);
break;
case 'status-update':
console.log(`\nStatus: ${event.status.state}`);
break;
case 'artifact-update':
console.log(`\nArtifact: ${event.artifact.id}`);
break;
}
}The A2AStreamEvent union type includes three event types:
| Event Type | Description |
|---|---|
status-update | Task state changed (working, completed) |
artifact-update | New artifact produced by the agent |
token | Individual token from LLM output |
The server emits token events via the onToken callback passed to Cogitator.run().
RedisTaskStore
For production deployments, use RedisTaskStore instead of the default in-memory store:
import { A2AServer, RedisTaskStore } from '@cogitator-ai/a2a';
import { createRedisClient } from '@cogitator-ai/redis';
const redis = await createRedisClient({ url: 'redis://localhost:6379' });
const a2aServer = new A2AServer({
agents: { researcher },
cogitator,
taskStore: new RedisTaskStore({
client: redis,
keyPrefix: 'a2a:task:',
ttl: 86400,
}),
});RedisTaskStore accepts any Redis client that implements the RedisClientLike interface: get, set, del, keys, and optionally setex (TTL), scan (listing without blocking KEYS), mget (batched reads) and eval (atomic task updates). The client from @cogitator-ai/redis (createRedisClient, standalone or cluster) and an ioredis instance (new Redis(url)) fit it directly; other clients such as redis may need a thin wrapper whose methods match these signatures.
Configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
client | RedisClientLike | required | Redis client instance |
keyPrefix | string | a2a:task: | Key prefix for task storage |
ttl | number | none | TTL in seconds (uses SETEX if set) |
Push Notifications
Register webhooks to receive task event notifications instead of polling or holding open SSE connections. When a task's status or artifacts change, the server POSTs the event to all registered webhook URLs.
Registering Webhooks
const client = new A2AClient('https://remote-agent.example.com');
const task = await client.sendMessage({
role: 'user',
parts: [{ type: 'text', text: 'Run a long analysis' }],
});
const config = await client.createPushNotification(task.id, {
webhookUrl: 'https://my-app.com/webhooks/a2a',
authenticationInfo: {
scheme: 'bearer',
credentials: { token: 'my-webhook-secret' },
},
});Managing Webhooks
const configs = await client.listPushNotifications(task.id);
const single = await client.getPushNotification(task.id, config.id!);
await client.deletePushNotification(task.id, config.id!);Server Configuration
Enable push notifications by providing a PushNotificationStore:
import { A2AServer, InMemoryPushNotificationStore } from '@cogitator-ai/a2a';
const a2aServer = new A2AServer({
agents: { researcher },
cogitator,
pushNotificationStore: new InMemoryPushNotificationStore(),
});The server automatically sends A2AStreamEvent payloads to registered webhooks on status and artifact updates. Authentication schemes supported: bearer (token), apiKey (key, optional headerName), basic (username, password), oauth2 (accessToken). Configs can only be created for existing tasks.
Unless allowPrivateUrls: true, delivery refuses loopback, private, link-local, CGNAT, multicast and IPv4-mapped IPv6 targets, validates the address the socket actually connects to (no DNS rebinding) and does not follow redirects.
Agent Card Signing
Sign Agent Cards with HMAC-SHA256 to let clients verify card integrity:
Server-Side
Configure cardSigning on the server to automatically sign all Agent Cards:
const a2aServer = new A2AServer({
agents: { researcher },
cogitator,
cardSigning: {
algorithm: 'hmac-sha256',
secret: process.env.CARD_SIGNING_SECRET!,
},
});Client-Side Verification
const client = new A2AClient('https://remote-agent.example.com');
const isValid = await client.verifyAgentCard(process.env.CARD_SIGNING_SECRET!);Manual Signing and Verification
import { signAgentCard, verifyAgentCardSignature } from '@cogitator-ai/a2a';
const signed = signAgentCard(card, { secret: 'shared-secret' });
console.log(signed.signature); // "hmac-sha256:a1b2c3..."
const valid = verifyAgentCardSignature(signed, 'shared-secret');Extended Agent Card
The agent/extendedCard method provides an authenticated endpoint that returns additional agent details not included in the public Agent Card -- rate limits, pricing, extended skills, and custom metadata.
Server Configuration
const a2aServer: A2AServer = new A2AServer({
agents: { researcher },
cogitator,
extendedCardGenerator: (agentName) => ({
...a2aServer.getAgentCard(agentName),
extendedSkills: [
{
id: 'deep_research',
name: 'deep_research',
description: 'Multi-source deep research',
inputModes: ['text/plain'],
outputModes: ['text/plain', 'application/json'],
},
],
rateLimit: { requestsPerMinute: 60 },
pricing: { model: 'per-request', details: '$0.01 per task' },
metadata: { version: '2.1.0', region: 'us-east-1' },
}),
});Fetching from Client
const client = new A2AClient('https://remote-agent.example.com', {
headers: { Authorization: 'Bearer my-token' },
});
const extendedCard = await client.extendedAgentCard();
console.log(extendedCard.rateLimit);
console.log(extendedCard.pricing);
console.log(extendedCard.extendedSkills);The Agent Card advertises this capability via capabilities.extendedAgentCard: true.
Framework Adapters
All adapters stream only for message/stream (an Accept: text/event-stream header alone does not switch message/send to SSE) and answer JSON-RPC notifications with HTTP 204 and no body. Structurally invalid JSON-RPC requests get -32600 Invalid Request; only unparseable JSON gets -32700 Parse error.
Errors that are neither A2A errors nor CogitatorErrors (in parsing, authentication, routing, streams or agent runs) are logged on the server and answered as -32603 Internal error, without their text; a CogitatorError is answered as -32603 Internal error: <message>. A task that fails that way gets the status message Internal error, and so does the failed status event of a stream.
Express
import { a2aExpress } from '@cogitator-ai/a2a/express';
app.use(a2aExpress(a2aServer));Hono
import { a2aHono } from '@cogitator-ai/a2a/hono';
app.route('/', a2aHono(a2aServer));Fastify
import { a2aFastify } from '@cogitator-ai/a2a/fastify';
fastify.register(a2aFastify(a2aServer));Koa
The Koa middleware reads ctx.request.body, so put a JSON body parser in front of it:
import bodyParser from 'koa-bodyparser';
import { a2aKoa } from '@cogitator-ai/a2a/koa';
app.use(bodyParser());
app.use(a2aKoa(a2aServer));Next.js
a2aNext returns route handlers: GET answers with the Agent Card and POST with JSON-RPC. Export them from the two routes clients expect:
// app/.well-known/agent.json/route.ts → GET
// app/a2a/route.ts → POST (the route at basePath)
import { a2aNext } from '@cogitator-ai/a2a/next';
export const { GET, POST } = a2aNext(a2aServer);Task Store
Tasks are stored in memory by default. InMemoryTaskStore keeps up to maxSize tasks (default 10,000) and evicts the oldest finished task first:
import { A2AServer, InMemoryTaskStore } from '@cogitator-ai/a2a';
const a2aServer = new A2AServer({
agents: { researcher },
cogitator,
taskStore: new InMemoryTaskStore({ maxSize: 50_000 }),
});For production, use RedisTaskStore or implement the TaskStore interface (create, get, update, list, delete). Your list must honour filter.visibleTo: when it is set, return only tasks owned by that user and tasks without an owner (null means a caller without a user), or tasks/list leaks other users' tasks.
Authentication
const a2aServer = new A2AServer({
agents: { researcher },
cogitator,
auth: {
type: 'bearer',
validate: async (token) => {
return token === process.env.A2A_SECRET;
},
},
});validate receives the credential and returns true, false or a caller. Every adapter extracts the credential (Authorization: Bearer ..., or the x-api-key header / headerName for type: 'apiKey') with a2aServer.getAuthToken(); a missing or rejected credential answers a JSON-RPC unauthorized error. The Agent Card itself stays public and advertises the scheme in securitySchemes.
One caller per user
When several users or tenants share the server, return who the caller is instead of true:
auth: {
type: 'bearer',
validate: async (token) => {
const session = await sessions.verify(token);
return session ? { userId: session.userId } : false;
},
},Each task then belongs to the user who created it: another user's tasks/get, tasks/cancel, push-notification calls and attempts to continue the task get Task not found, tasks/list returns only their own tasks (plus tasks without an owner), and a contextId holding someone else's tasks is refused. Runs carry the userId, so the agent's threads and memory are scoped to the user as well (multi-user). The owner is kept in the task's metadata on the server and never sent to clients. Returning true admits the caller without a user: such callers share the tasks that have no owner.
Clients include credentials via headers:
const client = new A2AClient('https://remote-agent.example.com', {
headers: {
Authorization: 'Bearer my-secret-token',
},
});A2A Protocol Methods
| Method | Description |
|---|---|
message/send | Send a message (blocking, or background with blocking: false) |
message/stream | Send a message with SSE streaming (incl. token events) |
tasks/get | Retrieve a task by ID |
tasks/cancel | Cancel a running task |
tasks/list | List tasks with filtering and pagination |
tasks/pushNotification/create | Register a webhook for task events |
tasks/pushNotification/get | Get a push notification config |
tasks/pushNotification/list | List push notification configs for a task |
tasks/pushNotification/delete | Remove a push notification config |
agent/extendedCard | Fetch extended Agent Card (authenticated) |
Task States
| State | Description |
|---|---|
working | Agent is processing |
completed | Task finished successfully |
failed | Task encountered an error |
input-required | Agent needs more input |
canceled | Task was canceled |
rejected | Server rejected the task |
Model Context Protocol (MCP)
Full MCP support -- connect to external MCP servers to use their tools, or expose Cogitator tools as an MCP server.
Reflection Engine
Enable agents to reflect on their own outputs, extract insights from past executions, and continuously improve through self-evaluation loops.