A JavaScript library that helps run agentic applications as A2AServers following the Agent2Agent (A2A) Protocol.
You can install the A2A SDK using npm
.
npm install @a2a-js/sdk
If you plan to use the A2A server functionality (A2AExpressApp
), you'll also need to install Express as it's a peer dependency:
npm install express
You can also find JavaScript samples here.
This example shows how to create a simple "Hello World" agent server and a client to interact with it.
The core of an A2A server is the AgentExecutor
, which contains your agent's logic.
// server.ts
import express from "express";
import { v4 as uuidv4 } from "uuid";
import type { AgentCard, Message } from "@a2a-js/sdk";
import {
AgentExecutor,
RequestContext,
ExecutionEventBus,
DefaultRequestHandler,
InMemoryTaskStore,
} from "@a2a-js/sdk/server";
import { A2AExpressApp } from "@a2a-js/sdk/server/express";
// 1. Define your agent's identity card.
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/", // The public URL of your agent server
skills: [ { id: "chat", name: "Chat", description: "Say hello", tags: ["chat"] } ],
// --- Other AgentCard fields omitted for brevity ---
};
// 2. Implement the agent's logic.
class HelloExecutor implements AgentExecutor {
async execute(
requestContext: RequestContext,
eventBus: ExecutionEventBus
): Promise<void> {
// Create a direct message response.
const responseMessage: Message = {
kind: "message",
messageId: uuidv4(),
role: "agent",
parts: [{ kind: "text", text: "Hello, world!" }],
// Associate the response with the incoming request's context.
contextId: requestContext.contextId,
};
// Publish the message and signal that the interaction is finished.
eventBus.publish(responseMessage);
eventBus.finished();
}
// cancelTask is not needed for this simple, non-stateful agent.
cancelTask = async (): Promise<void> => {};
}
// 3. Set up and run the server.
const agentExecutor = new HelloExecutor();
const requestHandler = new DefaultRequestHandler(
helloAgentCard,
new InMemoryTaskStore(),
agentExecutor
);
const appBuilder = new A2AExpressApp(requestHandler);
const expressApp = appBuilder.setupRoutes(express());
expressApp.listen(4000, () => {
console.log(`🚀 Server started on http://localhost:4000`);
});
The A2AClient
makes it easy to communicate with any A2A-compliant agent.
// client.ts
import { A2AClient, SendMessageSuccessResponse } from "@a2a-js/sdk/client";
import { Message, MessageSendParams } from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";
async function run() {
// Create a client pointing to the agent's Agent Card URL.
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
const sendParams: MessageSendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [{ kind: "text", text: "Hi there!" }],
kind: "message",
},
};
const response = await client.sendMessage(sendParams);
if ("error" in response) {
console.error("Error:", response.error.message);
} else {
const result = (response as SendMessageSuccessResponse).result as Message;
console.log("Agent response:", result.parts[0].text); // "Hello, world!"
}
}
await run();
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).
This agent creates a task, attaches a file artifact to it, and marks it as complete.
// server.ts
import { Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "@a2a-js/sdk";
// ... other imports from the quickstart server ...
class TaskExecutor implements AgentExecutor {
async execute(
requestContext: RequestContext,
eventBus: ExecutionEventBus
): Promise<void> {
const { taskId, contextId } = requestContext;
// 1. Create and publish the initial task object.
const initialTask: Task = {
kind: "task",
id: taskId,
contextId: contextId,
status: {
state: "submitted",
timestamp: new Date().toISOString(),
},
};
eventBus.publish(initialTask);
// 2. Create and publish an artifact.
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);
// 3. Publish the final status and mark the event as 'final'.
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> => {};
}
The client sends a message and receives a Task
object as the result.
// client.ts
import { A2AClient, SendMessageSuccessResponse } from "@a2a-js/sdk/client";
import { Message, MessageSendParams, Task } from "@a2a-js/sdk";
// ... other imports ...
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
const response = await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "Do something." }], kind: "message" } });
if ("error" in response) {
console.error("Error:", response.error.message);
} else {
const result = (response as SendMessageSuccessResponse).result;
// Check if the agent's response is a Task or a direct 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);
}
}
You can provide a custom fetch
implementation to the A2AClient
to modify its HTTP request behavior. Common use cases include:
- Request Interception: Log outgoing requests or collect metrics.
- Header Injection: Add custom headers for authentication, tracing, or routing.
- Retry Mechanisms: Implement custom logic for retrying failed requests.
This example creates a fetch
wrapper that adds a unique X-Request-ID
to every outgoing request.
import { A2AClient } from "@a2a-js/sdk/client";
import { v4 as uuidv4 } from "uuid";
// 1. Create a wrapper around the global fetch function.
const fetchWithCustomHeader: typeof fetch = async (url, init) => {
const headers = new Headers(init?.headers);
headers.set("X-Request-ID", uuidv4());
const newInit = { ...init, headers };
console.log(`Sending request to ${url} with X-Request-ID: ${headers.get("X-Request-ID")}`);
return fetch(url, newInit);
};
// 2. Provide the custom fetch implementation to the client.
const client = await A2AClient.fromCardUrl(
"http://localhost:4000/.well-known/agent-card.json",
{ fetchImpl: fetchWithCustomHeader }
);
// Now, all requests made by this client instance will include the X-Request-ID header.
await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "A message requiring custom headers." }], kind: "message" } });
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 {
A2AClient,
AuthenticationHandler,
createAuthenticatingFetchWithRetry,
} from "@a2a-js/sdk/client";
// A simple token provider that simulates fetching a new token.
const tokenProvider = {
token: "initial-stale-token",
getNewToken: async () => {
console.log("Refreshing auth token...");
tokenProvider.token = `new-token-${Date.now()}`;
return tokenProvider.token;
},
};
// 1. Implement the AuthenticationHandler interface.
const handler: AuthenticationHandler = {
// headers() is called on every request to get the current auth headers.
headers: async () => ({
Authorization: `Bearer ${tokenProvider.token}`,
}),
// shouldRetryWithHeaders() is called after a request fails.
// It decides if a retry is needed and provides new headers.
shouldRetryWithHeaders: async (req: RequestInit, res: Response) => {
if (res.status === 401) { // Unauthorized
const newToken = await tokenProvider.getNewToken();
// Return new headers to trigger a single retry.
return { Authorization: `Bearer ${newToken}` };
}
// Return undefined to not retry for other errors.
return undefined;
},
};
// 2. Create the authenticated fetch function.
const authFetch = createAuthenticatingFetchWithRetry(fetch, handler);
// 3. Initialize the client with the new fetch implementation.
const client = await A2AClient.fromCardUrl(
"http://localhost:4000/.well-known/agent-card.json",
{ fetchImpl: authFetch }
);
For real-time updates, A2A supports streaming responses over Server-Sent Events (SSE).
The agent publishes events as it works on the task. The client receives these events in real-time.
// server.ts
// ... imports ...
class StreamingExecutor implements AgentExecutor {
async execute(
requestContext: RequestContext,
eventBus: ExecutionEventBus
): Promise<void> {
const { taskId, contextId } = requestContext;
// 1. Publish initial 'submitted' state.
eventBus.publish({
kind: "task",
id: taskId,
contextId,
status: { state: "submitted", timestamp: new Date().toISOString() },
});
// 2. Publish 'working' state.
eventBus.publish({
kind: "status-update",
taskId,
contextId,
status: { state: "working", timestamp: new Date().toISOString() },
final: false
});
// 3. Simulate work and publish an artifact.
await new Promise(resolve => setTimeout(resolve, 1000));
eventBus.publish({
kind: "artifact-update",
taskId,
contextId,
artifact: { artifactId: "result.txt", parts: [{ kind: "text", text: "First result." }] },
});
// 4. Publish final 'completed' state.
eventBus.publish({
kind: "status-update",
taskId,
contextId,
status: { state: "completed", timestamp: new Date().toISOString() },
final: true,
});
eventBus.finished();
}
cancelTask = async (): Promise<void> => {};
}
The sendMessageStream
method returns an AsyncGenerator
that yields events as they arrive from the server.
// client.ts
import { A2AClient } from "@a2a-js/sdk/client";
import { MessageSendParams } from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";
// ... other imports ...
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
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();
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.
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.
// server.ts
import {
AgentExecutor,
RequestContext,
ExecutionEventBus,
TaskStatusUpdateEvent,
} from "@a2a-js/sdk/server";
// ... other imports ...
class CancellableExecutor implements AgentExecutor {
// Use a Set to track the IDs of tasks that have been requested to be canceled.
private cancelledTasks = new Set<string>();
/**
* When a cancellation is requested, add the taskId to our tracking set.
* The `execute` loop will handle the rest.
*/
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;
// Start the task
eventBus.publish({
kind: "status-update",
taskId,
contextId,
status: { state: "working", timestamp: new Date().toISOString() },
final: false,
});
// Simulate a multi-step, long-running process
for (let i = 0; i < 5; i++) {
// **Cancellation Checkpoint**
// Before each step, check if the task has been canceled.
if (this.cancelledTasks.has(taskId)) {
console.log(`[Executor] Aborting task ${taskId} due to cancellation.`);
// Publish the final 'canceled' status.
const cancelledUpdate: TaskStatusUpdateEvent = {
kind: "status-update",
taskId: taskId,
contextId: contextId,
status: { state: "canceled", timestamp: new Date().toISOString() },
final: true,
};
eventBus.publish(cancelledUpdate);
eventBus.finished();
// Clean up and exit.
this.cancelledTasks.delete(taskId);
return;
}
// Simulate one step of work.
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.`);
// If not canceled, finish the work and publish the completed state.
const finalUpdate: TaskStatusUpdateEvent = {
kind: "status-update",
taskId,
contextId,
status: { state: "completed", timestamp: new Date().toISOString() },
final: true,
};
eventBus.publish(finalUpdate);
eventBus.finished();
}
}
This project is licensed under the terms of the Apache 2.0 License.
See CONTRIBUTING.md for contribution guidelines.