A2A JavaScript SDK

A JavaScript library that helps run agentic applications as A2AServers following the Agent2Agent (A2A) Protocol.
Installation
You can install the A2A SDK using npm.
npm install @a2a-js/sdk
For Server Usage
If you plan to use the Express integration (imports from @a2a-js/sdk/server/express) for A2A server, you'll also need to install Express as it's a peer dependency:
npm install express
For gRPC Usage
If you plan to use the GRPC transport (imports from @a2a-js/sdk/server/grpc or @a2a-js/sdk/client/grpc), you must install the required peer dependencies:
npm install @grpc/grpc-js @bufbuild/protobuf
You can also find some samples here.
Compatibility
This SDK implements the A2A Protocol Specification v0.3.0.
| JSON-RPC | ✅ | ✅ |
| HTTP+JSON/REST | ✅ | ✅ |
| GRPC (Node.js only) | ✅ | ✅ |
Quickstart
This example shows how to create a simple "Hello World" agent server and a client to interact with it.
Server: Hello World Agent
The core of an A2A server is the AgentExecutor, which contains your agent's logic.
import express from 'express';
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { v4 as uuidv4 } from 'uuid';
import { AgentCard, Message, AGENT_CARD_PATH } from '@a2a-js/sdk';
import {
AgentExecutor,
RequestContext,
ExecutionEventBus,
DefaultRequestHandler,
InMemoryTaskStore,
} from '@a2a-js/sdk/server';
import { agentCardHandler, jsonRpcHandler, restHandler, UserBuilder } from '@a2a-js/sdk/server/express';
import { grpcService, A2AService } from '@a2a-js/sdk/server/grpc';
const helloAgentCard: AgentCard = {
name: 'Hello Agent',
description: 'A simple agent that says hello.',
protocolVersion: '0.3.0',
version: '0.1.0',
url: 'http://localhost:4000/a2a/jsonrpc',
skills: [{ id: 'chat', name: 'Chat', description: 'Say hello', tags: ['chat'] }],
capabilities: {
pushNotifications: false,
},
defaultInputModes: ['text'],
defaultOutputModes: ['text'],
additionalInterfaces: [
{ url: 'http://localhost:4000/a2a/jsonrpc', transport: 'JSONRPC' },
{ url: 'http://localhost:4000/a2a/rest', transport: 'HTTP+JSON' },
{ url: 'localhost:4001', transport: 'GRPC' },
],
};
class HelloExecutor implements AgentExecutor {
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
const responseMessage: Message = {
kind: 'message',
messageId: uuidv4(),
role: 'agent',
parts: [{ kind: 'text', text: 'Hello, world!' }],
contextId: requestContext.contextId,
};
eventBus.publish(responseMessage);
eventBus.finished();
}
cancelTask = async (): Promise<void> => {};
}
const agentExecutor = new HelloExecutor();
const requestHandler = new DefaultRequestHandler(
helloAgentCard,
new InMemoryTaskStore(),
agentExecutor
);
const app = express();
app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: requestHandler }));
app.use('/a2a/jsonrpc', jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
app.use('/a2a/rest', restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
app.listen(4000, () => {
console.log(`🚀 Server started on http://localhost:4000`);
});
const server = new Server();
server.addService(A2AService, grpcService({
requestHandler,
userBuilder: UserBuilder.noAuthentication,
}));
server.bindAsync(`localhost:4001`, ServerCredentials.createInsecure(), () => {
console.log(`🚀 Server started on localhost:4001`);
});
Client: Sending a Message
The ClientFactory makes it easy to communicate with any A2A-compliant agent.
import { ClientFactory } from '@a2a-js/sdk/client';
import { Message, MessageSendParams, SendMessageSuccessResponse } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
async function run() {
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
const sendParams: MessageSendParams = {
message: {
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'Hi there!' }],
kind: 'message',
},
};
try {
const response = await client.sendMessage(sendParams);
const result = response as Message;
console.log('Agent response:', result.parts[0].text);
} catch(e) {
console.error('Error:', e);
}
}
await run();
gRPC Client: Sending a Message
The ClientFactory has to be created explicitly passing the GrpcTransportFactory.
import { ClientFactory, ClientFactoryOptions } from '@a2a-js/sdk/client';
import { GrpcTransportFactory } from '@a2a-js/sdk/client/grpc';
import { Message, MessageSendParams, SendMessageSuccessResponse } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
async function run() {
const factory = new ClientFactory({
transports: [new GrpcTransportFactory()]
});
const client = await factory.createFromUrl('http://localhost:4000');
const sendParams: MessageSendParams = {
message: {
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'Hi there!' }],
kind: 'message',
},
};
try {
const response = await client.sendMessage(sendParams);
const result = response as Message;
console.log('Agent response:', result.parts[0].text);
} catch(e) {
console.error('Error:', e);
}
}
await run();
A2A Task Support
For operations that are stateful or long-running, agents create a Task. A task has a state (e.g., working, completed) and can produce Artifacts (e.g., files, data).
Server: Creating a Task
This agent creates a task, attaches a file artifact to it, and marks it as complete.
import { Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from '@a2a-js/sdk';
class TaskExecutor implements AgentExecutor {
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
const { taskId, contextId, userMessage, task } = requestContext;
if (!task) {
const initialTask: Task = {
kind: 'task',
id: taskId,
contextId: contextId,
status: {
state: 'submitted',
timestamp: new Date().toISOString(),
},
history: [userMessage],
};
eventBus.publish(initialTask);
}
const artifactUpdate: TaskArtifactUpdateEvent = {
kind: 'artifact-update',
taskId: taskId,
contextId: contextId,
artifact: {
artifactId: 'report-1',
name: 'analysis_report.txt',
parts: [{ kind: 'text', text: `This is the analysis for task ${taskId}.` }],
},
};
eventBus.publish(artifactUpdate);
const finalUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: { state: 'completed', timestamp: new Date().toISOString() },
final: true,
};
eventBus.publish(finalUpdate);
eventBus.finished();
}
cancelTask = async (): Promise<void> => {};
}
Client: Receiving a Task
The client sends a message and receives a Task object as the result.
import { ClientFactory } from '@a2a-js/sdk/client';
import { Message, MessageSendParams, SendMessageSuccessResponse, Task } from '@a2a-js/sdk';
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
try {
const result = await client.sendMessage({
message: {
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'Do something.' }],
kind: 'message',
},
});
if (result.kind === 'task') {
const task = result as Task;
console.log(`Task [${task.id}] completed with status: ${task.status.state}`);
if (task.artifacts && task.artifacts.length > 0) {
console.log(`Artifact found: ${task.artifacts[0].name}`);
console.log(`Content: ${task.artifacts[0].parts[0].text}`);
}
} else {
const message = result as Message;
console.log('Received direct message:', message.parts[0].text);
}
} catch (e) {
console.error('Error:', e);
}
Client Customization
Client can be customized via CallInterceptor's which is a recommended way as it's transport-agnostic.
Common use cases include:
- Request Interception: Log outgoing requests or collect metrics.
- Header Injection: Add custom headers for authentication, tracing, or routing.
- A2A Extensions: Modifying payloads to include protocol extension data.
This example defines a CallInterceptor to update serviceParameters which are passed as HTTP headers.
import { v4 as uuidv4 } from 'uuid';
import { AfterArgs, BeforeArgs, CallInterceptor, ClientFactory, ClientFactoryOptions } from '@a2a-js/sdk/client';
class RequestIdInterceptor implements CallInterceptor {
before(args: BeforeArgs): Promise<void> {
args.options = {
...args.options,
serviceParameters: {
...args.options.serviceParameters,
['X-Request-ID']: uuidv4(),
},
};
return Promise.resolve();
}
after(): Promise<void> {
return Promise.resolve();
}
}
const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
clientConfig: {
interceptors: [new RequestIdInterceptor()]
}
}))
const client = await factory.createFromAgentCardUrl('http://localhost:4000');
await client.sendMessage({
message: {
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'A message requiring custom headers.' }],
kind: 'message',
},
});
Example: Specifying a Timeout
Each client method can be configured with an optional signal field.
import { ClientFactory } from '@a2a-js/sdk/client';
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
await client.sendMessage(
{
message: {
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'A long-running message.' }],
kind: 'message',
},
},
{
signal: AbortSignal.timeout(5000),
}
);
Customizing Transports: Using the Provided AuthenticationHandler
For advanced authentication scenarios, the SDK includes a higher-order function createAuthenticatingFetchWithRetry and an AuthenticationHandler interface. This utility automatically adds authorization headers and can retry requests that fail with authentication errors (e.g., 401 Unauthorized).
Here's how to use it to manage a Bearer token:
import {
ClientFactory,
ClientFactoryOptions,
JsonRpcTransportFactory,
AuthenticationHandler,
createAuthenticatingFetchWithRetry,
} from '@a2a-js/sdk/client';
const tokenProvider = {
token: 'initial-stale-token',
getNewToken: async () => {
console.log('Refreshing auth token...');
tokenProvider.token = `new-token-${Date.now()}`;
return tokenProvider.token;
},
};
const handler: AuthenticationHandler = {
headers: async () => ({
Authorization: `Bearer ${tokenProvider.token}`,
}),
shouldRetryWithHeaders: async (req: RequestInit, res: Response) => {
if (res.status === 401) {
const newToken = await tokenProvider.getNewToken();
return { Authorization: `Bearer ${newToken}` };
}
return undefined;
},
};
const authFetch = createAuthenticatingFetchWithRetry(fetch, handler);
const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
transports: [
new JsonRpcTransportFactory({ fetchImpl: authFetch })
]
}))
const client = await factory.createFromUrl('http://localhost:4000');
Streaming
For real-time updates, A2A supports streaming responses over Server-Sent Events (SSE).
Server: Streaming Task Updates
The agent publishes events as it works on the task. The client receives these events in real-time.
class StreamingExecutor implements AgentExecutor {
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
const { taskId, contextId, userMessage, task } = requestContext;
if (!task) {
const initialTask: Task = {
kind: 'task',
id: taskId,
contextId: contextId,
status: {
state: 'submitted',
timestamp: new Date().toISOString(),
},
history: [userMessage],
};
eventBus.publish(initialTask);
}
eventBus.publish({
kind: 'status-update',
taskId,
contextId,
status: { state: 'working', timestamp: new Date().toISOString() },
final: false,
});
await new Promise((resolve) => setTimeout(resolve, 1000));
eventBus.publish({
kind: 'artifact-update',
taskId,
contextId,
artifact: { artifactId: 'result.txt', parts: [{ kind: 'text', text: 'First result.' }] },
});
eventBus.publish({
kind: 'status-update',
taskId,
contextId,
status: { state: 'completed', timestamp: new Date().toISOString() },
final: true,
});
eventBus.finished();
}
cancelTask = async (): Promise<void> => {};
}
Client: Consuming a Stream
The sendMessageStream method returns an AsyncGenerator that yields events as they arrive from the server.
import { ClientFactory } from '@a2a-js/sdk/client';
import { MessageSendParams } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
async function streamTask() {
const streamParams: MessageSendParams = {
message: {
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'Stream me some updates!' }],
kind: 'message',
},
};
try {
const stream = client.sendMessageStream(streamParams);
for await (const event of stream) {
if (event.kind === 'task') {
console.log(`[${event.id}] Task created. Status: ${event.status.state}`);
} else if (event.kind === 'status-update') {
console.log(`[${event.taskId}] Status Updated: ${event.status.state}`);
} else if (event.kind === 'artifact-update') {
console.log(`[${event.taskId}] Artifact Received: ${event.artifact.artifactId}`);
}
}
console.log('--- Stream finished ---');
} catch (error) {
console.error('Error during streaming:', error);
}
}
await streamTask();
Handling Task Cancellation
To support user-initiated cancellations, you must implement the cancelTask method in your AgentExecutor. The executor is responsible for gracefully stopping the ongoing work and publishing a final canceled status event.
A straightforward way to manage this is by maintaining an in-memory set of canceled task IDs. The execute method can then periodically check this set to see if it should terminate its process.
Server: Implementing a Cancellable Executor
This example demonstrates an agent that simulates a multi-step process. In each step of its work, it checks if a cancellation has been requested. If so, it stops the work and updates the task's state accordingly.
import {
AgentExecutor,
RequestContext,
ExecutionEventBus,
TaskStatusUpdateEvent,
} from '@a2a-js/sdk/server';
class CancellableExecutor implements AgentExecutor {
private cancelledTasks = new Set<string>();
public async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {
console.log(`[Executor] Received cancellation request for task: ${taskId}`);
this.cancelledTasks.add(taskId);
}
public async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
const { taskId, contextId } = requestContext;
eventBus.publish({
kind: 'status-update',
taskId,
contextId,
status: { state: 'working', timestamp: new Date().toISOString() },
final: false,
});
for (let i = 0; i < 5; i++) {
if (this.cancelledTasks.has(taskId)) {
console.log(`[Executor] Aborting task ${taskId} due to cancellation.`);
const cancelledUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId: taskId,
contextId: contextId,
status: { state: 'canceled', timestamp: new Date().toISOString() },
final: true,
};
eventBus.publish(cancelledUpdate);
eventBus.finished();
this.cancelledTasks.delete(taskId);
return;
}
console.log(`[Executor] Working on step ${i + 1} for task ${taskId}...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
console.log(`[Executor] Task ${taskId} finished all steps without cancellation.`);
const finalUpdate: TaskStatusUpdateEvent = {
kind: 'status-update',
taskId,
contextId,
status: { state: 'completed', timestamp: new Date().toISOString() },
final: true,
};
eventBus.publish(finalUpdate);
eventBus.finished();
}
}
A2A Push Notifications
For very long-running tasks (e.g., lasting minutes, hours, or even days) or when clients cannot or prefer not to maintain persistent connections (like mobile clients or serverless functions), A2A supports asynchronous updates via push notifications. This mechanism allows the A2A Server to actively notify a client-provided webhook when a significant task update occurs.
Server-Side Configuration
To enable push notifications, your agent card must declare support:
const movieAgentCard: AgentCard = {
capabilities: {
streaming: true,
pushNotifications: true,
stateTransitionHistory: true,
},
};
When creating the DefaultRequestHandler, you can optionally provide custom push notification components:
import {
DefaultRequestHandler,
InMemoryPushNotificationStore,
DefaultPushNotificationSender,
} from '@a2a-js/sdk/server';
const pushNotificationStore = new InMemoryPushNotificationStore();
const pushNotificationSender = new DefaultPushNotificationSender(pushNotificationStore, {
timeout: 5000,
tokenHeaderName: 'X-A2A-Notification-Token',
});
const requestHandler = new DefaultRequestHandler(
movieAgentCard,
taskStore,
agentExecutor,
undefined,
pushNotificationStore,
pushNotificationSender,
undefined
);
Client-Side Usage
Configure push notifications when sending messages:
const pushConfig: PushNotificationConfig = {
id: 'my-notification-config',
url: 'https://my-app.com/webhook/task-updates',
token: 'your-auth-token',
};
const sendParams: MessageSendParams = {
message: {
messageId: uuidv4(),
role: 'user',
parts: [{ kind: 'text', text: 'Hello, agent!' }],
kind: 'message',
},
configuration: {
blocking: true,
acceptedOutputModes: ['text/plain'],
pushNotificationConfig: pushConfig,
},
};
Webhook Endpoint Implementation
Your webhook endpoint should expect POST requests with the task data:
app.post('/webhook/task-updates', (req, res) => {
const task = req.body;
const token = req.headers['x-a2a-notification-token'];
if (token !== 'your-auth-token') {
return res.status(401).json({ error: 'Unauthorized' });
}
console.log(`Task ${task.id} status: ${task.status.state}`);
res.status(200).json({ received: true });
});
License
This project is licensed under the terms of the Apache 2.0 License.
Contributing
See CONTRIBUTING.md for contribution guidelines.