@a2a-js/sdk 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  ## Installation
17
17
 
18
- You can install the A2A SDK using either `npm`.
18
+ You can install the A2A SDK using `npm`.
19
19
 
20
20
  ```bash
21
21
  npm install @a2a-js/sdk
@@ -23,7 +23,7 @@ npm install @a2a-js/sdk
23
23
 
24
24
  ### For Server Usage
25
25
 
26
- If you plan to use the A2A server functionality (A2AExpressApp), you'll also need to install Express as it's a peer dependency:
26
+ If you plan to use the A2A server functionality (`A2AExpressApp`), you'll also need to install Express as it's a peer dependency:
27
27
 
28
28
  ```bash
29
29
  npm install express
@@ -31,417 +31,508 @@ npm install express
31
31
 
32
32
  You can also find JavaScript samples [here](https://github.com/google-a2a/a2a-samples/tree/main/samples/js).
33
33
 
34
- ## A2A Server
34
+ -----
35
35
 
36
- This directory contains a TypeScript server implementation for the Agent-to-Agent (A2A) communication protocol, built using Express.js.
36
+ ## Quickstart
37
37
 
38
- **Note:** Express is a peer dependency for server functionality. Make sure to install it separately:
39
- ```bash
40
- npm install express
41
- ```
38
+ This example shows how to create a simple "Hello World" agent server and a client to interact with it.
42
39
 
43
- ### 1. Define Agent Card
44
-
45
- ```typescript
46
- import type { AgentCard } from "@a2a-js/sdk";
47
-
48
- const movieAgentCard: AgentCard = {
49
- name: "Movie Agent",
50
- description:
51
- "An agent that can answer questions about movies and actors using TMDB.",
52
- // Adjust the base URL and port as needed.
53
- url: "http://localhost:41241/",
54
- provider: {
55
- organization: "A2A Agents",
56
- url: "https://example.com/a2a-agents", // Added provider URL
57
- },
58
- protocolVersion: "0.3.0", // A2A protocol this agent supports.
59
- version: "0.0.2", // Incremented version
60
- capabilities: {
61
- streaming: true, // Supports streaming
62
- pushNotifications: false, // Assuming not implemented for this agent yet
63
- stateTransitionHistory: true, // Agent uses history
64
- },
65
- securitySchemes: undefined, // Or define actual security schemes if any
66
- security: undefined,
67
- defaultInputModes: ["text/plain"],
68
- defaultOutputModes: ["text/plain"],
69
- skills: [
70
- {
71
- id: "general_movie_chat",
72
- name: "General Movie Chat",
73
- description:
74
- "Answer general questions or chat about movies, actors, directors.",
75
- tags: ["movies", "actors", "directors"],
76
- examples: [
77
- "Tell me about the plot of Inception.",
78
- "Recommend a good sci-fi movie.",
79
- "Who directed The Matrix?",
80
- "What other movies has Scarlett Johansson been in?",
81
- "Find action movies starring Keanu Reeves",
82
- "Which came out first, Jurassic Park or Terminator 2?",
83
- ],
84
- inputModes: ["text/plain"], // Explicitly defining for skill
85
- outputModes: ["text/plain"], // Explicitly defining for skill
86
- },
87
- ],
88
- supportsAuthenticatedExtendedCard: false,
89
- };
90
- ```
40
+ ### Server: Hello World Agent
91
41
 
92
- ### 2. Define Agent Executor
42
+ The core of an A2A server is the `AgentExecutor`, which contains your agent's logic.
93
43
 
94
44
  ```typescript
45
+ // server.ts
46
+ import express from "express";
47
+ import { v4 as uuidv4 } from "uuid";
48
+ import type { AgentCard, Message } from "@a2a-js/sdk";
95
49
  import {
96
- InMemoryTaskStore,
97
- TaskStore,
98
50
  AgentExecutor,
99
51
  RequestContext,
100
52
  ExecutionEventBus,
101
53
  DefaultRequestHandler,
54
+ InMemoryTaskStore,
102
55
  } from "@a2a-js/sdk/server";
103
56
  import { A2AExpressApp } from "@a2a-js/sdk/server/express";
104
57
 
105
- // 1. Define your agent's logic as a AgentExecutor
106
- class MyAgentExecutor implements AgentExecutor {
107
- private cancelledTasks = new Set<string>();
58
+ // 1. Define your agent's identity card.
59
+ const helloAgentCard: AgentCard = {
60
+ name: "Hello Agent",
61
+ description: "A simple agent that says hello.",
62
+ protocolVersion: "0.3.0",
63
+ version: "0.1.0",
64
+ url: "http://localhost:4000/", // The public URL of your agent server
65
+ skills: [ { id: "chat", name: "Chat", description: "Say hello", tags: ["chat"] } ],
66
+ // --- Other AgentCard fields omitted for brevity ---
67
+ };
108
68
 
109
- public cancelTask = async (
110
- taskId: string,
69
+ // 2. Implement the agent's logic.
70
+ class HelloExecutor implements AgentExecutor {
71
+ async execute(
72
+ requestContext: RequestContext,
111
73
  eventBus: ExecutionEventBus
112
- ): Promise<void> => {
113
- this.cancelledTasks.add(taskId);
114
- // The execute loop is responsible for publishing the final state
74
+ ): Promise<void> {
75
+ // Create a direct message response.
76
+ const responseMessage: Message = {
77
+ kind: "message",
78
+ messageId: uuidv4(),
79
+ role: "agent",
80
+ parts: [{ kind: "text", text: "Hello, world!" }],
81
+ // Associate the response with the incoming request's context.
82
+ contextId: requestContext.contextId,
83
+ };
84
+
85
+ // Publish the message and signal that the interaction is finished.
86
+ eventBus.publish(responseMessage);
87
+ eventBus.finished();
88
+ }
89
+
90
+ // cancelTask is not needed for this simple, non-stateful agent.
91
+ cancelTask = async (): Promise<void> => {};
92
+ }
93
+
94
+ // 3. Set up and run the server.
95
+ const agentExecutor = new HelloExecutor();
96
+ const requestHandler = new DefaultRequestHandler(
97
+ helloAgentCard,
98
+ new InMemoryTaskStore(),
99
+ agentExecutor
100
+ );
101
+
102
+ const appBuilder = new A2AExpressApp(requestHandler);
103
+ const expressApp = appBuilder.setupRoutes(express());
104
+
105
+ expressApp.listen(4000, () => {
106
+ console.log(`🚀 Server started on http://localhost:4000`);
107
+ });
108
+ ```
109
+
110
+ ### Client: Sending a Message
111
+
112
+ The `A2AClient` makes it easy to communicate with any A2A-compliant agent.
113
+
114
+ ```typescript
115
+ // client.ts
116
+ import { A2AClient, SendMessageSuccessResponse } from "@a2a-js/sdk/client";
117
+ import { Message, MessageSendParams } from "@a2a-js/sdk";
118
+ import { v4 as uuidv4 } from "uuid";
119
+
120
+ async function run() {
121
+ // Create a client pointing to the agent's Agent Card URL.
122
+ const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
123
+
124
+ const sendParams: MessageSendParams = {
125
+ message: {
126
+ messageId: uuidv4(),
127
+ role: "user",
128
+ parts: [{ kind: "text", text: "Hi there!" }],
129
+ kind: "message",
130
+ },
115
131
  };
116
132
 
133
+ const response = await client.sendMessage(sendParams);
134
+
135
+ if ("error" in response) {
136
+ console.error("Error:", response.error.message);
137
+ } else {
138
+ const result = (response as SendMessageSuccessResponse).result as Message;
139
+ console.log("Agent response:", result.parts[0].text); // "Hello, world!"
140
+ }
141
+ }
142
+
143
+ await run();
144
+ ```
145
+
146
+ -----
147
+
148
+ ## A2A `Task` Support
149
+
150
+ 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).
151
+
152
+ ### Server: Creating a Task
153
+
154
+ This agent creates a task, attaches a file artifact to it, and marks it as complete.
155
+
156
+ ```typescript
157
+ // server.ts
158
+ import { Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "@a2a-js/sdk";
159
+ // ... other imports from the quickstart server ...
160
+
161
+ class TaskExecutor implements AgentExecutor {
117
162
  async execute(
118
163
  requestContext: RequestContext,
119
164
  eventBus: ExecutionEventBus
120
165
  ): Promise<void> {
121
- const userMessage = requestContext.userMessage;
122
- const existingTask = requestContext.task;
123
-
124
- // Determine IDs for the task and context, from requestContext.
125
- const taskId = requestContext.taskId;
126
- const contextId = requestContext.contextId;
127
-
128
- console.log(
129
- `[MyAgentExecutor] Processing message ${userMessage.messageId} for task ${taskId} (context: ${contextId})`
130
- );
131
-
132
- // 1. Publish initial Task event if it's a new task
133
- if (!existingTask) {
134
- const initialTask: Task = {
135
- kind: "task",
136
- id: taskId,
137
- contextId: contextId,
138
- status: {
139
- state: "submitted",
140
- timestamp: new Date().toISOString(),
141
- },
142
- history: [userMessage],
143
- metadata: userMessage.metadata,
144
- artifacts: [], // Initialize artifacts array
145
- };
146
- eventBus.publish(initialTask);
147
- }
166
+ const { taskId, contextId } = requestContext;
148
167
 
149
- // 2. Publish "working" status update
150
- const workingStatusUpdate: TaskStatusUpdateEvent = {
151
- kind: "status-update",
152
- taskId: taskId,
168
+ // 1. Create and publish the initial task object.
169
+ const initialTask: Task = {
170
+ kind: "task",
171
+ id: taskId,
153
172
  contextId: contextId,
154
173
  status: {
155
- state: "working",
156
- message: {
157
- kind: "message",
158
- role: "agent",
159
- messageId: uuidv4(),
160
- parts: [{ kind: "text", text: "Generating code..." }],
161
- taskId: taskId,
162
- contextId: contextId,
163
- },
174
+ state: "submitted",
164
175
  timestamp: new Date().toISOString(),
165
176
  },
166
- final: false,
167
177
  };
168
- eventBus.publish(workingStatusUpdate);
169
-
170
- // Simulate work...
171
- await new Promise((resolve) => setTimeout(resolve, 1000));
172
-
173
- // Check for request cancellation
174
- if (this.cancelledTasks.has(taskId)) {
175
- console.log(`[MyAgentExecutor] Request cancelled for task: ${taskId}`);
176
- const cancelledUpdate: TaskStatusUpdateEvent = {
177
- kind: "status-update",
178
- taskId: taskId,
179
- contextId: contextId,
180
- status: {
181
- state: "canceled",
182
- timestamp: new Date().toISOString(),
183
- },
184
- final: true,
185
- };
186
- eventBus.publish(cancelledUpdate);
187
- eventBus.finished();
188
- return;
189
- }
178
+ eventBus.publish(initialTask);
190
179
 
191
- // 3. Publish artifact update
180
+ // 2. Create and publish an artifact.
192
181
  const artifactUpdate: TaskArtifactUpdateEvent = {
193
182
  kind: "artifact-update",
194
183
  taskId: taskId,
195
184
  contextId: contextId,
196
185
  artifact: {
197
- artifactId: "artifact-1",
198
- name: "artifact-1",
199
- parts: [{ kind: "text", text: `Task ${taskId} completed.` }],
186
+ artifactId: "report-1",
187
+ name: "analysis_report.txt",
188
+ parts: [{ kind: "text", text: `This is the analysis for task ${taskId}.` }],
200
189
  },
201
- append: false, // Each emission is a complete file snapshot
202
- lastChunk: true, // True for this file artifact
203
190
  };
204
191
  eventBus.publish(artifactUpdate);
205
192
 
206
- // 4. Publish final status update
193
+ // 3. Publish the final status and mark the event as 'final'.
207
194
  const finalUpdate: TaskStatusUpdateEvent = {
208
195
  kind: "status-update",
209
196
  taskId: taskId,
210
197
  contextId: contextId,
211
- status: {
212
- state: "completed",
213
- message: {
214
- kind: "message",
215
- role: "agent",
216
- messageId: uuidv4(),
217
- taskId: taskId,
218
- contextId: contextId,
219
- },
220
- timestamp: new Date().toISOString(),
221
- },
198
+ status: { state: "completed", timestamp: new Date().toISOString() },
222
199
  final: true,
223
200
  };
224
201
  eventBus.publish(finalUpdate);
225
202
  eventBus.finished();
226
203
  }
204
+
205
+ cancelTask = async (): Promise<void> => {};
227
206
  }
228
207
  ```
229
208
 
230
- ### 3. Start the server
231
-
232
- ```typescript
233
- const taskStore: TaskStore = new InMemoryTaskStore();
234
- const agentExecutor: AgentExecutor = new MyAgentExecutor();
209
+ ### Client: Receiving a Task
235
210
 
236
- const requestHandler = new DefaultRequestHandler(
237
- coderAgentCard,
238
- taskStore,
239
- agentExecutor
240
- );
211
+ The client sends a message and receives a `Task` object as the result.
241
212
 
242
- const appBuilder = new A2AExpressApp(requestHandler);
243
- const expressApp = appBuilder.setupRoutes(express(), "");
244
-
245
- const PORT = process.env.CODER_AGENT_PORT || 41242; // Different port for coder agent
246
- expressApp.listen(PORT, () => {
247
- console.log(
248
- `[MyAgent] Server using new framework started on http://localhost:${PORT}`
249
- );
250
- console.log(
251
- `[MyAgent] Agent Card: http://localhost:${PORT}/.well-known/agent.json`
252
- );
253
- console.log("[MyAgent] Press Ctrl+C to stop the server");
254
- });
255
- ```
213
+ ```typescript
214
+ // client.ts
215
+ import { A2AClient, SendMessageSuccessResponse } from "@a2a-js/sdk/client";
216
+ import { Message, MessageSendParams, Task } from "@a2a-js/sdk";
217
+ // ... other imports ...
256
218
 
257
- ### Agent Executor
219
+ const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
258
220
 
259
- Developers are expected to implement this interface and provide two methods: `execute` and `cancelTask`.
221
+ const response = await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "Do something." }], kind: "message" } });
260
222
 
261
- #### `execute`
223
+ if ("error" in response) {
224
+ console.error("Error:", response.error.message);
225
+ } else {
226
+ const result = (response as SendMessageSuccessResponse).result;
262
227
 
263
- - This method is provided with a `RequestContext` and an `EventBus` to publish execution events.
264
- - Executor can either respond by publishing a Message or Task.
265
- - For a task, check if there's an existing task in `RequestContext`. If not, publish an initial Task event using `taskId` & `contextId` from `RequestContext`.
266
- - Executor can subsequently publish `TaskStatusUpdateEvent` or `TaskArtifactUpdateEvent`.
267
- - Executor should indicate which is the `final` event and also call `finished()` method of event bus.
268
- - Executor should also check if an ongoing task has been cancelled. If yes, cancel the execution and emit an `TaskStatusUpdateEvent` with cancelled state.
228
+ // Check if the agent's response is a Task or a direct Message.
229
+ if (result.kind === "task") {
230
+ const task = result as Task;
231
+ console.log(`Task [${task.id}] completed with status: ${task.status.state}`);
269
232
 
270
- #### `cancelTask`
233
+ if (task.artifacts && task.artifacts.length > 0) {
234
+ console.log(`Artifact found: ${task.artifacts[0].name}`);
235
+ console.log(`Content: ${task.artifacts[0].parts[0].text}`);
236
+ }
237
+ } else {
238
+ const message = result as Message;
239
+ console.log("Received direct message:", message.parts[0].text);
240
+ }
241
+ }
242
+ ```
271
243
 
272
- Executors should implement cancellation mechanism for an ongoing task.
244
+ -----
273
245
 
274
- ## A2A Client
246
+ ## Client Customization
275
247
 
276
- There's a `A2AClient` class, which provides methods for interacting with an A2A server over HTTP using JSON-RPC.
248
+ You can provide a custom `fetch` implementation to the `A2AClient` to modify its HTTP request behavior. Common use cases include:
277
249
 
278
- ### Key Features:
250
+ * **Request Interception**: Log outgoing requests or collect metrics.
251
+ * **Header Injection**: Add custom headers for authentication, tracing, or routing.
252
+ * **Retry Mechanisms**: Implement custom logic for retrying failed requests.
279
253
 
280
- - **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.
281
- - **A2A Methods:** Implements standard A2A methods like `sendMessage`, `sendMessageStream`, `getTask`, `cancelTask`, `setTaskPushNotificationConfig`, `getTaskPushNotificationConfig`, and `resubscribeTask`.
282
- - **Error Handling:** Provides basic error handling for network issues and JSON-RPC errors.
283
- - **Streaming Support:** Manages Server-Sent Events (SSE) for real-time task updates (`sendMessageStream`, `resubscribeTask`).
284
- - **Extensibility:** Allows providing a custom `fetch` implementation for different environments (e.g., Node.js).
254
+ ### Example: Injecting a Custom Header
285
255
 
286
- ### Basic Usage
256
+ This example creates a `fetch` wrapper that adds a unique `X-Request-ID` to every outgoing request.
287
257
 
288
258
  ```typescript
289
259
  import { A2AClient } from "@a2a-js/sdk/client";
290
- import type {
291
- Message,
292
- MessageSendParams,
293
- Task,
294
- TaskQueryParams,
295
- SendMessageResponse,
296
- GetTaskResponse,
297
- SendMessageSuccessResponse,
298
- GetTaskSuccessResponse,
299
- } from "@a2a-js/sdk";
300
260
  import { v4 as uuidv4 } from "uuid";
301
261
 
302
- const client = new A2AClient("http://localhost:41241"); // Replace with your server URL
262
+ // 1. Create a wrapper around the global fetch function.
263
+ const fetchWithCustomHeader: typeof fetch = async (url, init) => {
264
+ const headers = new Headers(init?.headers);
265
+ headers.set("X-Request-ID", uuidv4());
303
266
 
304
- async function run() {
305
- const messageId = uuidv4();
306
- let taskId: string | undefined;
267
+ const newInit = { ...init, headers };
268
+
269
+ console.log(`Sending request to ${url} with X-Request-ID: ${headers.get("X-Request-ID")}`);
270
+
271
+ return fetch(url, newInit);
272
+ };
307
273
 
308
- try {
309
- // 1. Send a message to the agent.
310
- const sendParams: MessageSendParams = {
311
- message: {
312
- messageId: messageId,
313
- role: "user",
314
- parts: [{ kind: "text", text: "Hello, agent!" }],
315
- kind: "message",
316
- },
317
- configuration: {
318
- blocking: true,
319
- acceptedOutputModes: ["text/plain"],
320
- },
321
- };
274
+ // 2. Provide the custom fetch implementation to the client.
275
+ const client = await A2AClient.fromCardUrl(
276
+ "http://localhost:4000/.well-known/agent-card.json",
277
+ { fetchImpl: fetchWithCustomHeader }
278
+ );
322
279
 
323
- const sendResponse: SendMessageResponse =
324
- await client.sendMessage(sendParams);
280
+ // Now, all requests made by this client instance will include the X-Request-ID header.
281
+ await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "A message requiring custom headers." }], kind: "message" } });
282
+ ```
325
283
 
326
- if (sendResponse.error) {
327
- console.error("Error sending message:", sendResponse.error);
328
- return;
329
- }
284
+ ### Using the Provided `AuthenticationHandler`
330
285
 
331
- // On success, the result can be a Task or a Message. Check which one it is.
332
- const result = (sendResponse as SendMessageSuccessResponse).result;
333
-
334
- if (result.kind === "task") {
335
- // The agent created a task.
336
- const taskResult = result as Task;
337
- console.log("Send Message Result (Task):", taskResult);
338
- taskId = taskResult.id; // Save the task ID for the next call
339
- } else if (result.kind === "message") {
340
- // The agent responded with a direct message.
341
- const messageResult = result as Message;
342
- console.log("Send Message Result (Direct Message):", messageResult);
343
- // No task was created, so we can't get task status.
344
- }
286
+ 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).
345
287
 
346
- // 2. If a task was created, get its status.
347
- if (taskId) {
348
- const getParams: TaskQueryParams = { id: taskId };
349
- const getResponse: GetTaskResponse = await client.getTask(getParams);
288
+ Here's how to use it to manage a Bearer token:
350
289
 
351
- if (getResponse.error) {
352
- console.error(`Error getting task ${taskId}:`, getResponse.error);
353
- return;
354
- }
290
+ ```typescript
291
+ import {
292
+ A2AClient,
293
+ AuthenticationHandler,
294
+ createAuthenticatingFetchWithRetry,
295
+ } from "@a2a-js/sdk/client";
296
+
297
+ // A simple token provider that simulates fetching a new token.
298
+ const tokenProvider = {
299
+ token: "initial-stale-token",
300
+ getNewToken: async () => {
301
+ console.log("Refreshing auth token...");
302
+ tokenProvider.token = `new-token-${Date.now()}`;
303
+ return tokenProvider.token;
304
+ },
305
+ };
355
306
 
356
- const getTaskResult = (getResponse as GetTaskSuccessResponse).result;
357
- console.log("Get Task Result:", getTaskResult);
307
+ // 1. Implement the AuthenticationHandler interface.
308
+ const handler: AuthenticationHandler = {
309
+ // headers() is called on every request to get the current auth headers.
310
+ headers: async () => ({
311
+ Authorization: `Bearer ${tokenProvider.token}`,
312
+ }),
313
+
314
+ // shouldRetryWithHeaders() is called after a request fails.
315
+ // It decides if a retry is needed and provides new headers.
316
+ shouldRetryWithHeaders: async (req: RequestInit, res: Response) => {
317
+ if (res.status === 401) { // Unauthorized
318
+ const newToken = await tokenProvider.getNewToken();
319
+ // Return new headers to trigger a single retry.
320
+ return { Authorization: `Bearer ${newToken}` };
358
321
  }
359
- } catch (error) {
360
- console.error("A2A Client Communication Error:", error);
322
+
323
+ // Return undefined to not retry for other errors.
324
+ return undefined;
325
+ },
326
+ };
327
+
328
+ // 2. Create the authenticated fetch function.
329
+ const authFetch = createAuthenticatingFetchWithRetry(fetch, handler);
330
+
331
+ // 3. Initialize the client with the new fetch implementation.
332
+ const client = await A2AClient.fromCardUrl(
333
+ "http://localhost:4000/.well-known/agent-card.json",
334
+ { fetchImpl: authFetch }
335
+ );
336
+ ```
337
+
338
+ -----
339
+
340
+ ## Streaming
341
+
342
+ For real-time updates, A2A supports streaming responses over Server-Sent Events (SSE).
343
+
344
+ ### Server: Streaming Task Updates
345
+
346
+ The agent publishes events as it works on the task. The client receives these events in real-time.
347
+
348
+ ```typescript
349
+ // server.ts
350
+ // ... imports ...
351
+
352
+ class StreamingExecutor implements AgentExecutor {
353
+ async execute(
354
+ requestContext: RequestContext,
355
+ eventBus: ExecutionEventBus
356
+ ): Promise<void> {
357
+ const { taskId, contextId } = requestContext;
358
+
359
+ // 1. Publish initial 'submitted' state.
360
+ eventBus.publish({
361
+ kind: "task",
362
+ id: taskId,
363
+ contextId,
364
+ status: { state: "submitted", timestamp: new Date().toISOString() },
365
+ });
366
+
367
+ // 2. Publish 'working' state.
368
+ eventBus.publish({
369
+ kind: "status-update",
370
+ taskId,
371
+ contextId,
372
+ status: { state: "working", timestamp: new Date().toISOString() },
373
+ final: false
374
+ });
375
+
376
+ // 3. Simulate work and publish an artifact.
377
+ await new Promise(resolve => setTimeout(resolve, 1000));
378
+ eventBus.publish({
379
+ kind: "artifact-update",
380
+ taskId,
381
+ contextId,
382
+ artifact: { artifactId: "result.txt", parts: [{ kind: "text", text: "First result." }] },
383
+ });
384
+
385
+ // 4. Publish final 'completed' state.
386
+ eventBus.publish({
387
+ kind: "status-update",
388
+ taskId,
389
+ contextId,
390
+ status: { state: "completed", timestamp: new Date().toISOString() },
391
+ final: true,
392
+ });
393
+ eventBus.finished();
361
394
  }
395
+ cancelTask = async (): Promise<void> => {};
362
396
  }
363
-
364
- run();
365
397
  ```
366
398
 
367
- ### Streaming Usage
399
+ ### Client: Consuming a Stream
400
+
401
+ The `sendMessageStream` method returns an `AsyncGenerator` that yields events as they arrive from the server.
368
402
 
369
403
  ```typescript
404
+ // client.ts
370
405
  import { A2AClient } from "@a2a-js/sdk/client";
371
- import type {
372
- TaskStatusUpdateEvent,
373
- TaskArtifactUpdateEvent,
374
- MessageSendParams,
375
- Task,
376
- Message,
377
- } from "@a2a-js/sdk";
406
+ import { MessageSendParams } from "@a2a-js/sdk";
378
407
  import { v4 as uuidv4 } from "uuid";
408
+ // ... other imports ...
379
409
 
380
- const client = new A2AClient("http://localhost:41241");
410
+ const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
381
411
 
382
412
  async function streamTask() {
383
- const messageId = uuidv4();
384
- try {
385
- console.log(`\n--- Starting streaming task for message ${messageId} ---`);
386
-
387
- // Construct the `MessageSendParams` object.
388
- const streamParams: MessageSendParams = {
389
- message: {
390
- messageId: messageId,
391
- role: "user",
392
- parts: [{ kind: "text", text: "Stream me some updates!" }],
393
- kind: "message",
394
- },
395
- };
413
+ const streamParams: MessageSendParams = {
414
+ message: {
415
+ messageId: uuidv4(),
416
+ role: "user",
417
+ parts: [{ kind: "text", text: "Stream me some updates!" }],
418
+ kind: "message",
419
+ },
420
+ };
396
421
 
397
- // Use the `sendMessageStream` method.
422
+ try {
398
423
  const stream = client.sendMessageStream(streamParams);
399
- let currentTaskId: string | undefined;
400
424
 
401
425
  for await (const event of stream) {
402
- // The first event is often the Task object itself, establishing the ID.
403
- if ((event as Task).kind === "task") {
404
- currentTaskId = (event as Task).id;
405
- console.log(
406
- `[${currentTaskId}] Task created. Status: ${(event as Task).status.state}`
407
- );
408
- continue;
409
- }
410
-
411
- // Differentiate subsequent stream events.
412
- if ((event as TaskStatusUpdateEvent).kind === "status-update") {
413
- const statusEvent = event as TaskStatusUpdateEvent;
414
- console.log(
415
- `[${statusEvent.taskId}] Status Update: ${statusEvent.status.state} - ${
416
- statusEvent.status.message?.parts[0]?.text ?? ""
417
- }`
418
- );
419
- if (statusEvent.final) {
420
- console.log(`[${statusEvent.taskId}] Stream marked as final.`);
421
- break; // Exit loop when server signals completion
422
- }
423
- } else if (
424
- (event as TaskArtifactUpdateEvent).kind === "artifact-update"
425
- ) {
426
- const artifactEvent = event as TaskArtifactUpdateEvent;
427
- // Use artifact.name or artifact.artifactId for identification
428
- console.log(
429
- `[${artifactEvent.taskId}] Artifact Update: ${
430
- artifactEvent.artifact.name ?? artifactEvent.artifact.artifactId
431
- } - Part Count: ${artifactEvent.artifact.parts.length}`
432
- );
433
- } else {
434
- // This could be a direct Message response if the agent doesn't create a task.
435
- console.log("Received direct message response in stream:", event);
426
+ if (event.kind === "task") {
427
+ console.log(`[${event.id}] Task created. Status: ${event.status.state}`);
428
+ } else if (event.kind === "status-update") {
429
+ console.log(`[${event.taskId}] Status Updated: ${event.status.state}`);
430
+ } else if (event.kind === "artifact-update") {
431
+ console.log(`[${event.taskId}] Artifact Received: ${event.artifact.artifactId}`);
436
432
  }
437
433
  }
438
- console.log(`--- Streaming for message ${messageId} finished ---`);
434
+ console.log("--- Stream finished ---");
439
435
  } catch (error) {
440
- console.error(`Error during streaming for message ${messageId}:`, error);
436
+ console.error("Error during streaming:", error);
441
437
  }
442
438
  }
443
439
 
444
- streamTask();
440
+ await streamTask();
441
+ ```
442
+
443
+ ## Handling Task Cancellation
444
+
445
+ 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.
446
+
447
+ 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.
448
+
449
+ ### Server: Implementing a Cancellable Executor
450
+
451
+ 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.
452
+
453
+ ```typescript
454
+ // server.ts
455
+ import {
456
+ AgentExecutor,
457
+ RequestContext,
458
+ ExecutionEventBus,
459
+ TaskStatusUpdateEvent,
460
+ } from "@a2a-js/sdk/server";
461
+ // ... other imports ...
462
+
463
+ class CancellableExecutor implements AgentExecutor {
464
+ // Use a Set to track the IDs of tasks that have been requested to be canceled.
465
+ private cancelledTasks = new Set<string>();
466
+
467
+ /**
468
+ * When a cancellation is requested, add the taskId to our tracking set.
469
+ * The `execute` loop will handle the rest.
470
+ */
471
+ public async cancelTask(
472
+ taskId: string,
473
+ eventBus: ExecutionEventBus,
474
+ ): Promise<void> {
475
+ console.log(`[Executor] Received cancellation request for task: ${taskId}`);
476
+ this.cancelledTasks.add(taskId);
477
+ }
478
+
479
+ public async execute(
480
+ requestContext: RequestContext,
481
+ eventBus: ExecutionEventBus,
482
+ ): Promise<void> {
483
+ const { taskId, contextId } = requestContext;
484
+
485
+ // Start the task
486
+ eventBus.publish({
487
+ kind: "status-update",
488
+ taskId,
489
+ contextId,
490
+ status: { state: "working", timestamp: new Date().toISOString() },
491
+ final: false,
492
+ });
493
+
494
+ // Simulate a multi-step, long-running process
495
+ for (let i = 0; i < 5; i++) {
496
+ // **Cancellation Checkpoint**
497
+ // Before each step, check if the task has been canceled.
498
+ if (this.cancelledTasks.has(taskId)) {
499
+ console.log(`[Executor] Aborting task ${taskId} due to cancellation.`);
500
+
501
+ // Publish the final 'canceled' status.
502
+ const cancelledUpdate: TaskStatusUpdateEvent = {
503
+ kind: "status-update",
504
+ taskId: taskId,
505
+ contextId: contextId,
506
+ status: { state: "canceled", timestamp: new Date().toISOString() },
507
+ final: true,
508
+ };
509
+ eventBus.publish(cancelledUpdate);
510
+ eventBus.finished();
511
+
512
+ // Clean up and exit.
513
+ this.cancelledTasks.delete(taskId);
514
+ return;
515
+ }
516
+
517
+ // Simulate one step of work.
518
+ console.log(`[Executor] Working on step ${i + 1} for task ${taskId}...`);
519
+ await new Promise(resolve => setTimeout(resolve, 1000));
520
+ }
521
+
522
+ console.log(`[Executor] Task ${taskId} finished all steps without cancellation.`);
523
+
524
+ // If not canceled, finish the work and publish the completed state.
525
+ const finalUpdate: TaskStatusUpdateEvent = {
526
+ kind: "status-update",
527
+ taskId,
528
+ contextId,
529
+ status: { state: "completed", timestamp: new Date().toISOString() },
530
+ final: true,
531
+ };
532
+ eventBus.publish(finalUpdate);
533
+ eventBus.finished();
534
+ }
535
+ }
445
536
  ```
446
537
 
447
538
  ## License