Skip to content

a2aproject/a2a-js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

npm install @a2a-js/sdk

You can also find JavaScript samples here.

A2A Server

This directory contains a TypeScript server implementation for the Agent-to-Agent (A2A) communication protocol, built using Express.js.

1. Define Agent Card

import { AgentCard } from "@a2a-js/sdk";

const movieAgentCard: AgentCard = {
  name: 'Movie Agent',
  description: 'An agent that can answer questions about movies and actors using TMDB.',
  // Adjust the base URL and port as needed.
  url: 'http://localhost:41241/',
  provider: {
    organization: 'A2A Agents',
    url: 'https://example.com/a2a-agents' // Added provider URL
  },
  version: '0.0.2', // Incremented version
  capabilities: {
    streaming: true, // Supports streaming
    pushNotifications: false, // Assuming not implemented for this agent yet
    stateTransitionHistory: true, // Agent uses history
  },
  securitySchemes: undefined, // Or define actual security schemes if any
  security: undefined,
  defaultInputModes: ['text/plain'],
  defaultOutputModes: ['text/plain'],
  skills: [
    {
      id: 'general_movie_chat',
      name: 'General Movie Chat',
      description: 'Answer general questions or chat about movies, actors, directors.',
      tags: ['movies', 'actors', 'directors'],
      examples: [
        'Tell me about the plot of Inception.',
        'Recommend a good sci-fi movie.',
        'Who directed The Matrix?',
        'What other movies has Scarlett Johansson been in?',
        'Find action movies starring Keanu Reeves',
        'Which came out first, Jurassic Park or Terminator 2?',
      ],
      inputModes: ['text/plain'], // Explicitly defining for skill
      outputModes: ['text/plain'] // Explicitly defining for skill
    },
  ],
  supportsAuthenticatedExtendedCard: false,
};

2. Define Agent Executor

import {
  InMemoryTaskStore,
  TaskStore,
  A2AExpressApp,
  AgentExecutor,
  RequestContext,
  ExecutionEventBus,
  DefaultRequestHandler,
} from "@a2a-js/sdk";

// 1. Define your agent's logic as a AgentExecutor
class MyAgentExecutor implements AgentExecutor {
  private cancelledTasks = new Set<string>();

  public cancelTask = async (
        taskId: string,
        eventBus: ExecutionEventBus,
    ): Promise<void> => {
        this.cancelledTasks.add(taskId);
        // The execute loop is responsible for publishing the final state
    };

  async execute(
    requestContext: RequestContext,
    eventBus: ExecutionEventBus
  ): Promise<void> {
    const userMessage = requestContext.userMessage;
    const existingTask = requestContext.task;

    // Determine IDs for the task and context, from requestContext.
    const taskId = requestContext.taskId;
    const contextId = requestContext.contextId;

    console.log(
      `[MyAgentExecutor] Processing message ${userMessage.messageId} for task ${taskId} (context: ${contextId})`
    );

    // 1. Publish initial Task event if it's a new task
    if (!existingTask) {
      const initialTask: Task = {
        kind: 'task',
        id: taskId,
        contextId: contextId,
        status: {
          state: 'submitted',
          timestamp: new Date().toISOString(),
        },
        history: [userMessage],
        metadata: userMessage.metadata,
        artifacts: [], // Initialize artifacts array
      };
      eventBus.publish(initialTask);
    }

    // 2. Publish "working" status update
    const workingStatusUpdate: TaskStatusUpdateEvent = {
      kind: 'status-update',
      taskId: taskId,
      contextId: contextId,
      status: {
        state: 'working',
        message: {
          kind: 'message',
          role: 'agent',
          messageId: uuidv4(),
          parts: [{ kind: 'text', text: 'Generating code...' }],
          taskId: taskId,
          contextId: contextId,
        },
        timestamp: new Date().toISOString(),
      },
      final: false,
    };
    eventBus.publish(workingStatusUpdate);

    // Simulate work...
    await new Promise((resolve) => setTimeout(resolve, 1000));

    // Check for request cancellation
    if (this.cancelledTasks.has(taskId)) {
      console.log(`[MyAgentExecutor] Request cancelled for task: ${taskId}`);
      const cancelledUpdate: TaskStatusUpdateEvent = {
        kind: 'status-update',
        taskId: taskId,
        contextId: contextId,
        status: {
          state: 'canceled',
          timestamp: new Date().toISOString(),
        },
        final: true,
      };
      eventBus.publish(cancelledUpdate);
      eventBus.finished();
      return;
    }

    // 3. Publish artifact update
    const artifactUpdate: TaskArtifactUpdateEvent = {
      kind: 'artifact-update',
      taskId: taskId,
      contextId: contextId,
      artifact: {
        artifactId: "artifact-1",
        name: "artifact-1",
        parts: [{ text: `Task ${context.task.id} completed.` }],
      },
      append: false, // Each emission is a complete file snapshot
      lastChunk: true, // True for this file artifact
    };
    eventBus.publish(artifactUpdate);

    // 4. Publish final status update
    const finalUpdate: TaskStatusUpdateEvent = {
      kind: 'status-update',
      taskId: taskId,
      contextId: contextId,
      status: {
        state: 'completed',
        message: {
          kind: 'message',
          role: 'agent',
          messageId: uuidv4(),
          taskId: taskId,
          contextId: contextId,
        },
        timestamp: new Date().toISOString(),
      },
      final: true,
    };
    eventBus.publish(finalUpdate);
    eventBus.finished();
  }
}

3. Start the server

const taskStore: TaskStore = new InMemoryTaskStore();
const agentExecutor: AgentExecutor = new MyAgentExecutor();

const requestHandler = new DefaultRequestHandler(
  coderAgentCard,
  taskStore,
  agentExecutor
);

const appBuilder = new A2AExpressApp(requestHandler);
const expressApp = appBuilder.setupRoutes(express(), '');

const PORT = process.env.CODER_AGENT_PORT || 41242; // Different port for coder agent
expressApp.listen(PORT, () => {
  console.log(`[MyAgent] Server using new framework started on http://localhost:${PORT}`);
  console.log(`[MyAgent] Agent Card: http://localhost:${PORT}/.well-known/agent.json`);
  console.log('[MyAgent] Press Ctrl+C to stop the server');
});

Agent Executor

Developers are expected to implement this interface and provide two methods: execute and cancelTask.

execute

  • This method is provided with a RequestContext and an EventBus to publish execution events.
  • Executor can either respond by publishing a Message or Task.
  • For a task, check if there's an existing task in RequestContext. If not, publish an initial Task event using taskId & contextId from RequestContext.
  • Executor can subsequently publish TaskStatusUpdateEvent or TaskArtifactUpdateEvent.
  • Executor should indicate which is the final event and also call finished() method of event bus.
  • Executor should also check if an ongoing task has been cancelled. If yes, cancel the execution and emit an TaskStatusUpdateEvent with cancelled state.

cancelTask

Executors should implement cancellation mechanism for an ongoing task.

A2A Client

There's a A2AClient class, which provides methods for interacting with an A2A server over HTTP using JSON-RPC.

Key Features:

  • JSON-RPC Communication: Handles sending requests and receiving responses (both standard and streaming via Server-Sent Events) according to the JSON-RPC 2.0 specification.
  • A2A Methods: Implements standard A2A methods like sendMessage, sendMessageStream, getTask, cancelTask, setTaskPushNotificationConfig, getTaskPushNotificationConfig, and resubscribeTask.
  • Error Handling: Provides basic error handling for network issues and JSON-RPC errors.
  • Streaming Support: Manages Server-Sent Events (SSE) for real-time task updates (sendMessageStream, resubscribeTask).
  • Extensibility: Allows providing a custom fetch implementation for different environments (e.g., Node.js).

Basic Usage

import {
  A2AClient,
  Message,
  MessageSendParams,
  Task,
  TaskQueryParams,
  SendMessageResponse,
  GetTaskResponse,
  SendMessageSuccessResponse,
  GetTaskSuccessResponse
} from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";

const client = new A2AClient("http://localhost:41241"); // Replace with your server URL

async function run() {
  const messageId = uuidv4();
  let taskId: string | undefined;

  try {
    // 1. Send a message to the agent.
    const sendParams: MessageSendParams = {
      message: {
        messageId: messageId,
        role: "user",
        parts: [{ kind: "text", text: "Hello, agent!" }],
        kind: "message"
      },
      configuration: {
        blocking: true,
        acceptedOutputModes: ['text/plain']
      }
    };
    
    const sendResponse: SendMessageResponse = await client.sendMessage(sendParams);

    if (sendResponse.error) {
        console.error("Error sending message:", sendResponse.error);
        return;
    }

    // On success, the result can be a Task or a Message. Check which one it is.
    const result = (sendResponse as SendMessageSuccessResponse).result;

    if (result.kind === 'task') {
        // The agent created a task.
        const taskResult = result as Task;
        console.log("Send Message Result (Task):", taskResult);
        taskId = taskResult.id; // Save the task ID for the next call
    } else if (result.kind === 'message') {
        // The agent responded with a direct message.
        const messageResult = result as Message;
        console.log("Send Message Result (Direct Message):", messageResult);
        // No task was created, so we can't get task status.
    }

    // 2. If a task was created, get its status.
    if (taskId) {
        const getParams: TaskQueryParams = { id: taskId };
        const getResponse: GetTaskResponse = await client.getTask(getParams);

        if (getResponse.error) {
            console.error(`Error getting task ${taskId}:`, getResponse.error);
            return;
        }
        
        const getTaskResult = (getResponse as GetTaskSuccessResponse).result;
        console.log("Get Task Result:", getTaskResult);
    }

  } catch (error) {
    console.error("A2A Client Communication Error:", error);
  }
}

run();

Streaming Usage

import {
  A2AClient,
  TaskStatusUpdateEvent,
  TaskArtifactUpdateEvent,
  MessageSendParams,
  Task,
  Message,
} from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";

const client = new A2AClient("http://localhost:41241");

async function streamTask() {
  const messageId = uuidv4();
  try {
    console.log(`\n--- Starting streaming task for message ${messageId} ---`);
    
    // Construct the `MessageSendParams` object.
    const streamParams: MessageSendParams = {
      message: {
        messageId: messageId,
        role: "user",
        parts: [{ kind: "text", text: "Stream me some updates!" }],
        kind: "message"
      },
    };
    
    // Use the `sendMessageStream` method.
    const stream = client.sendMessageStream(streamParams);
    let currentTaskId: string | undefined;

    for await (const event of stream) {
      // The first event is often the Task object itself, establishing the ID.
      if ((event as Task).kind === 'task') {
          currentTaskId = (event as Task).id;
          console.log(`[${currentTaskId}] Task created. Status: ${(event as Task).status.state}`);
          continue;
      }
      
      // Differentiate subsequent stream events.
      if ((event as TaskStatusUpdateEvent).kind === 'status-update') {
        const statusEvent = event as TaskStatusUpdateEvent;
        console.log(
          `[${statusEvent.taskId}] Status Update: ${statusEvent.status.state} - ${
            statusEvent.status.message?.parts[0]?.text ?? ""
          }`
        );
        if (statusEvent.final) {
          console.log(`[${statusEvent.taskId}] Stream marked as final.`);
          break; // Exit loop when server signals completion
        }
      } else if ((event as TaskArtifactUpdateEvent).kind === 'artifact-update') {
        const artifactEvent = event as TaskArtifactUpdateEvent;
        // Use artifact.name or artifact.artifactId for identification
        console.log(
          `[${artifactEvent.taskId}] Artifact Update: ${
            artifactEvent.artifact.name ?? artifactEvent.artifact.artifactId
          } - Part Count: ${artifactEvent.artifact.parts.length}`
        );
      } else {
        // This could be a direct Message response if the agent doesn't create a task.
        console.log("Received direct message response in stream:", event);
      }
    }
    console.log(`--- Streaming for message ${messageId} finished ---`);
  } catch (error) {
    console.error(`Error during streaming for message ${messageId}:`, error);
  }
}

streamTask();

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

Security policy

Stars

Watchers

Forks