Skip to content

a2aproject/a2a-js

A2A JavaScript SDK

License

A2A Logo

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 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.


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.

// 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`);
});

Client: Sending a Message

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();

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.

// 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> => {};
}

Client: Receiving a Task

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);
  }
}

Client Customization

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.

Example: Injecting a Custom Header

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" } });

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 {
  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 }
);

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.

// 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> => {};
}

Client: Consuming a Stream

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();

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.

// 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();
  }
}

License

This project is licensed under the terms of the Apache 2.0 License.

Contributing

See CONTRIBUTING.md for contribution guidelines.

About

Official JavaScript SDK for the Agent2Agent (A2A) Protocol

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks