@mailmodo/a2a 0.3.3 → 0.3.7

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
@@ -1,4 +1,4 @@
1
- # A2A JavaScript SDK
1
+ # A2A JavaScript SDK - Mailmodo Edition
2
2
 
3
3
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
4
4
 
@@ -13,25 +13,62 @@
13
13
 
14
14
  <!-- markdownlint-enable no-inline-html -->
15
15
 
16
+ > **Note**: This is Mailmodo's customized fork of the [official A2A JavaScript SDK](https://github.com/a2aproject/a2a-js). Key differences:
17
+ > - **Dual Module Support**: Supports both **CommonJS** and **ESM** (upstream only supports ESM)
18
+ > - **Backwards Compatibility**: Works with older TypeScript configurations using `typesVersions`
19
+
16
20
  ## Installation
17
21
 
18
- You can install the A2A SDK using `npm`.
22
+ You can install the Mailmodo A2A SDK using `npm`.
19
23
 
20
24
  ```bash
21
- npm install @a2a-js/sdk
25
+ npm install @mailmodo/a2a
22
26
  ```
23
27
 
24
28
  ### For Server Usage
25
29
 
26
- If you plan to use the A2A server functionality (`A2AExpressApp`), you'll also need to install Express as it's a peer dependency:
30
+ If you plan to use the Express integration (imports from `@mailmodo/a2a/server/express`) for A2A server, you'll also need to install Express as it's a peer dependency:
27
31
 
28
32
  ```bash
29
33
  npm install express
30
34
  ```
31
35
 
32
- You can also find JavaScript samples [here](https://github.com/google-a2a/a2a-samples/tree/main/samples/js).
36
+ You can also find some samples [here](https://github.com/mailmodo/a2a-js/tree/main/src/samples).
37
+
38
+ ---
39
+
40
+ ## Module System Compatibility
41
+
42
+ This package supports both **CommonJS** and **ESM** module systems:
43
+
44
+ ### CommonJS (Node.js)
45
+ ```javascript
46
+ const { Task, Message } = require('@mailmodo/a2a');
47
+ const { DefaultRequestHandler } = require('@mailmodo/a2a/server');
48
+ const { A2AExpressApp } = require('@mailmodo/a2a/server/express');
49
+ const { ClientFactory } = require('@mailmodo/a2a/client');
50
+ ```
51
+
52
+ ### ESM (ES Modules)
53
+ ```javascript
54
+ import { Task, Message } from '@mailmodo/a2a';
55
+ import { DefaultRequestHandler } from '@mailmodo/a2a/server';
56
+ import { A2AExpressApp } from '@mailmodo/a2a/server/express';
57
+ import { ClientFactory } from '@mailmodo/a2a/client';
58
+ ```
59
+
60
+
61
+ ---
62
+
63
+ ## Compatibility
33
64
 
34
- -----
65
+ This SDK implements the A2A Protocol Specification [`v0.3.0`](https://a2a-protocol.org/v0.3.0/specification).
66
+
67
+ | Transport | Client | Server |
68
+ | :--- | :---: | :---: |
69
+ | **JSON-RPC** | ✅ | ✅ |
70
+ | **HTTP+JSON/REST** | ✅ | ✅ |
71
+ | **gRPC** | ❌ | ❌ |
35
72
 
36
73
  ## Quickstart
37
74
 
@@ -43,41 +80,46 @@ The core of an A2A server is the `AgentExecutor`, which contains your agent's lo
43
80
 
44
81
  ```typescript
45
82
  // server.ts
46
- import express from "express";
47
- import { v4 as uuidv4 } from "uuid";
48
- import type { AgentCard, Message } from "@a2a-js/sdk";
83
+ import express from 'express';
84
+ import { v4 as uuidv4 } from 'uuid';
85
+ import { AgentCard, Message, AGENT_CARD_PATH } from '@mailmodo/a2a';
49
86
  import {
50
87
  AgentExecutor,
51
88
  RequestContext,
52
89
  ExecutionEventBus,
53
90
  DefaultRequestHandler,
54
91
  InMemoryTaskStore,
55
- } from "@a2a-js/sdk/server";
56
- import { A2AExpressApp } from "@a2a-js/sdk/server/express";
92
+ } from '@mailmodo/a2a/server';
93
+ import { agentCardHandler, jsonRpcHandler, restHandler, UserBuilder } from '@mailmodo/a2a/server/express';
57
94
 
58
95
  // 1. Define your agent's identity card.
59
96
  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 ---
97
+ name: 'Hello Agent',
98
+ description: 'A simple agent that says hello.',
99
+ protocolVersion: '0.3.0',
100
+ version: '0.1.0',
101
+ url: 'http://localhost:4000/a2a/jsonrpc', // The public URL of your agent server
102
+ skills: [{ id: 'chat', name: 'Chat', description: 'Say hello', tags: ['chat'] }],
103
+ capabilities: {
104
+ pushNotifications: false,
105
+ },
106
+ defaultInputModes: ['text'],
107
+ defaultOutputModes: ['text'],
108
+ additionalInterfaces: [
109
+ { url: 'http://localhost:4000/a2a/jsonrpc', transport: 'JSONRPC' }, // Default JSON-RPC transport
110
+ { url: 'http://localhost:4000/a2a/rest', transport: 'HTTP+JSON' }, // HTTP+JSON/REST transport
111
+ ],
67
112
  };
68
113
 
69
114
  // 2. Implement the agent's logic.
70
115
  class HelloExecutor implements AgentExecutor {
71
- async execute(
72
- requestContext: RequestContext,
73
- eventBus: ExecutionEventBus
74
- ): Promise<void> {
116
+ async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
75
117
  // Create a direct message response.
76
118
  const responseMessage: Message = {
77
- kind: "message",
119
+ kind: 'message',
78
120
  messageId: uuidv4(),
79
- role: "agent",
80
- parts: [{ kind: "text", text: "Hello, world!" }],
121
+ role: 'agent',
122
+ parts: [{ kind: 'text', text: 'Hello, world!' }],
81
123
  // Associate the response with the incoming request's context.
82
124
  contextId: requestContext.contextId,
83
125
  };
@@ -86,7 +128,7 @@ class HelloExecutor implements AgentExecutor {
86
128
  eventBus.publish(responseMessage);
87
129
  eventBus.finished();
88
130
  }
89
-
131
+
90
132
  // cancelTask is not needed for this simple, non-stateful agent.
91
133
  cancelTask = async (): Promise<void> => {};
92
134
  }
@@ -99,51 +141,56 @@ const requestHandler = new DefaultRequestHandler(
99
141
  agentExecutor
100
142
  );
101
143
 
102
- const appBuilder = new A2AExpressApp(requestHandler);
103
- const expressApp = appBuilder.setupRoutes(express());
144
+ const app = express();
104
145
 
105
- expressApp.listen(4000, () => {
146
+ app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: requestHandler }));
147
+ app.use('/a2a/jsonrpc', jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
148
+ app.use('/a2a/rest', restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
149
+
150
+ app.listen(4000, () => {
106
151
  console.log(`🚀 Server started on http://localhost:4000`);
107
152
  });
108
153
  ```
109
154
 
110
155
  ### Client: Sending a Message
111
156
 
112
- The `A2AClient` makes it easy to communicate with any A2A-compliant agent.
157
+ The [`ClientFactory`](src/client/factory.ts) makes it easy to communicate with any A2A-compliant agent.
113
158
 
114
159
  ```typescript
115
160
  // 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";
161
+ import { ClientFactory } from '@mailmodo/a2a/client';
162
+ import { Message, MessageSendParams, SendMessageSuccessResponse } from '@mailmodo/a2a';
163
+ import { v4 as uuidv4 } from 'uuid';
119
164
 
120
165
  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");
166
+ const factory = new ClientFactory();
167
+
168
+ // createFromUrl accepts baseUrl and optional path,
169
+ // (the default path is /.well-known/agent-card.json)
170
+ const client = await factory.createFromUrl('http://localhost:4000');
123
171
 
124
172
  const sendParams: MessageSendParams = {
125
173
  message: {
126
174
  messageId: uuidv4(),
127
- role: "user",
128
- parts: [{ kind: "text", text: "Hi there!" }],
129
- kind: "message",
175
+ role: 'user',
176
+ parts: [{ kind: 'text', text: 'Hi there!' }],
177
+ kind: 'message',
130
178
  },
131
179
  };
132
180
 
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!"
181
+ try {
182
+ const response = await client.sendMessage(sendParams);
183
+ const result = response as Message;
184
+ console.log('Agent response:', result.parts[0].text); // "Hello, world!"
185
+ } catch(e) {
186
+ console.error('Error:', e);
140
187
  }
141
188
  }
142
189
 
143
190
  await run();
144
191
  ```
145
192
 
146
- -----
193
+ ---
147
194
 
148
195
  ## A2A `Task` Support
149
196
 
@@ -155,47 +202,47 @@ This agent creates a task, attaches a file artifact to it, and marks it as compl
155
202
 
156
203
  ```typescript
157
204
  // server.ts
158
- import { Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "@a2a-js/sdk";
205
+ import { Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from '@mailmodo/a2a';
159
206
  // ... other imports from the quickstart server ...
160
207
 
161
208
  class TaskExecutor implements AgentExecutor {
162
- async execute(
163
- requestContext: RequestContext,
164
- eventBus: ExecutionEventBus
165
- ): Promise<void> {
166
- const { taskId, contextId } = requestContext;
167
-
168
- // 1. Create and publish the initial task object.
169
- const initialTask: Task = {
170
- kind: "task",
171
- id: taskId,
172
- contextId: contextId,
173
- status: {
174
- state: "submitted",
175
- timestamp: new Date().toISOString(),
176
- },
177
- };
178
- eventBus.publish(initialTask);
209
+ async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
210
+ const { taskId, contextId, userMessage, task } = requestContext;
211
+
212
+ // 1. Create and publish the initial task object if it doesn't exist.
213
+ if (!task) {
214
+ const initialTask: Task = {
215
+ kind: 'task',
216
+ id: taskId,
217
+ contextId: contextId,
218
+ status: {
219
+ state: 'submitted',
220
+ timestamp: new Date().toISOString(),
221
+ },
222
+ history: [userMessage],
223
+ };
224
+ eventBus.publish(initialTask);
225
+ }
179
226
 
180
227
  // 2. Create and publish an artifact.
181
228
  const artifactUpdate: TaskArtifactUpdateEvent = {
182
- kind: "artifact-update",
229
+ kind: 'artifact-update',
183
230
  taskId: taskId,
184
231
  contextId: contextId,
185
232
  artifact: {
186
- artifactId: "report-1",
187
- name: "analysis_report.txt",
188
- parts: [{ kind: "text", text: `This is the analysis for task ${taskId}.` }],
233
+ artifactId: 'report-1',
234
+ name: 'analysis_report.txt',
235
+ parts: [{ kind: 'text', text: `This is the analysis for task ${taskId}.` }],
189
236
  },
190
237
  };
191
238
  eventBus.publish(artifactUpdate);
192
239
 
193
240
  // 3. Publish the final status and mark the event as 'final'.
194
241
  const finalUpdate: TaskStatusUpdateEvent = {
195
- kind: "status-update",
242
+ kind: 'status-update',
196
243
  taskId: taskId,
197
244
  contextId: contextId,
198
- status: { state: "completed", timestamp: new Date().toISOString() },
245
+ status: { state: 'completed', timestamp: new Date().toISOString() },
199
246
  final: true,
200
247
  };
201
248
  eventBus.publish(finalUpdate);
@@ -212,21 +259,28 @@ The client sends a message and receives a `Task` object as the result.
212
259
 
213
260
  ```typescript
214
261
  // client.ts
215
- import { A2AClient, SendMessageSuccessResponse } from "@a2a-js/sdk/client";
216
- import { Message, MessageSendParams, Task } from "@a2a-js/sdk";
262
+ import { ClientFactory } from '@mailmodo/a2a/client';
263
+ import { Message, MessageSendParams, SendMessageSuccessResponse, Task } from '@mailmodo/a2a';
217
264
  // ... other imports ...
218
265
 
219
- const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
266
+ const factory = new ClientFactory();
220
267
 
221
- const response = await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "Do something." }], kind: "message" } });
268
+ // createFromUrl accepts baseUrl and optional path,
269
+ // (the default path is /.well-known/agent-card.json)
270
+ const client = await factory.createFromUrl('http://localhost:4000');
222
271
 
223
- if ("error" in response) {
224
- console.error("Error:", response.error.message);
225
- } else {
226
- const result = (response as SendMessageSuccessResponse).result;
272
+ try {
273
+ const result = await client.sendMessage({
274
+ message: {
275
+ messageId: uuidv4(),
276
+ role: 'user',
277
+ parts: [{ kind: 'text', text: 'Do something.' }],
278
+ kind: 'message',
279
+ },
280
+ });
227
281
 
228
282
  // Check if the agent's response is a Task or a direct Message.
229
- if (result.kind === "task") {
283
+ if (result.kind === 'task') {
230
284
  const task = result as Task;
231
285
  console.log(`Task [${task.id}] completed with status: ${task.status.state}`);
232
286
 
@@ -236,52 +290,99 @@ if ("error" in response) {
236
290
  }
237
291
  } else {
238
292
  const message = result as Message;
239
- console.log("Received direct message:", message.parts[0].text);
293
+ console.log('Received direct message:', message.parts[0].text);
240
294
  }
295
+ } catch (e) {
296
+ console.error('Error:', e);
241
297
  }
242
298
  ```
243
299
 
244
- -----
300
+ ---
245
301
 
246
302
  ## Client Customization
247
303
 
248
- You can provide a custom `fetch` implementation to the `A2AClient` to modify its HTTP request behavior. Common use cases include:
304
+ Client can be customized via [`CallInterceptor`'s](src/client/interceptors.ts) which is a recommended way as it's transport-agnostic.
305
+
306
+ Common use cases include:
249
307
 
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.
308
+ - **Request Interception**: Log outgoing requests or collect metrics.
309
+ - **Header Injection**: Add custom headers for authentication, tracing, or routing.
310
+ - **A2A Extensions**: Modifying payloads to include protocol extension data.
253
311
 
254
312
  ### Example: Injecting a Custom Header
255
313
 
256
- This example creates a `fetch` wrapper that adds a unique `X-Request-ID` to every outgoing request.
314
+ This example defines a `CallInterceptor` to update `serviceParameters` which are passed as HTTP headers.
257
315
 
258
316
  ```typescript
259
- import { A2AClient } from "@a2a-js/sdk/client";
260
- import { v4 as uuidv4 } from "uuid";
261
-
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());
266
-
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
- };
317
+ import { v4 as uuidv4 } from 'uuid';
318
+ import { AfterArgs, BeforeArgs, CallInterceptor, ClientFactory, ClientFactoryOptions } from '@mailmodo/a2a/client';
319
+
320
+ // 1. Define an interceptor
321
+ class RequestIdInterceptor implements CallInterceptor {
322
+ before(args: BeforeArgs): Promise<void> {
323
+ args.options = {
324
+ ...args.options,
325
+ serviceParameters: {
326
+ ...args.options.serviceParameters,
327
+ ['X-Request-ID']: uuidv4(),
328
+ },
329
+ };
330
+ return Promise.resolve();
331
+ }
273
332
 
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
- );
333
+ after(): Promise<void> {
334
+ return Promise.resolve();
335
+ }
336
+ }
337
+
338
+ // 2. Register the interceptor in the client factory
339
+ const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
340
+ clientConfig: {
341
+ interceptors: [new RequestIdInterceptor()]
342
+ }
343
+ }))
344
+ const client = await factory.createFromAgentCardUrl('http://localhost:4000');
345
+
346
+ // Now, all requests made by clients created by this factory will include the X-Request-ID header.
347
+ await client.sendMessage({
348
+ message: {
349
+ messageId: uuidv4(),
350
+ role: 'user',
351
+ parts: [{ kind: 'text', text: 'A message requiring custom headers.' }],
352
+ kind: 'message',
353
+ },
354
+ });
355
+ ```
356
+
357
+ ### Example: Specifying a Timeout
358
+
359
+ Each client method can be configured with an optional `signal` field.
360
+
361
+ ```typescript
362
+ import { ClientFactory } from '@mailmodo/a2a/client';
363
+
364
+ const factory = new ClientFactory();
365
+
366
+ // createFromUrl accepts baseUrl and optional path,
367
+ // (the default path is /.well-known/agent-card.json)
368
+ const client = await factory.createFromUrl('http://localhost:4000');
279
369
 
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" } });
370
+ await client.sendMessage(
371
+ {
372
+ message: {
373
+ messageId: uuidv4(),
374
+ role: 'user',
375
+ parts: [{ kind: 'text', text: 'A long-running message.' }],
376
+ kind: 'message',
377
+ },
378
+ },
379
+ {
380
+ signal: AbortSignal.timeout(5000), // 5 seconds timeout
381
+ }
382
+ );
282
383
  ```
283
384
 
284
- ### Using the Provided `AuthenticationHandler`
385
+ ### Customizing Transports: Using the Provided `AuthenticationHandler`
285
386
 
286
387
  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).
287
388
 
@@ -289,16 +390,18 @@ Here's how to use it to manage a Bearer token:
289
390
 
290
391
  ```typescript
291
392
  import {
292
- A2AClient,
393
+ ClientFactory,
394
+ ClientFactoryOptions,
395
+ JsonRpcTransportFactory,
293
396
  AuthenticationHandler,
294
397
  createAuthenticatingFetchWithRetry,
295
- } from "@a2a-js/sdk/client";
398
+ } from '@mailmodo/a2a/client';
296
399
 
297
400
  // A simple token provider that simulates fetching a new token.
298
401
  const tokenProvider = {
299
- token: "initial-stale-token",
402
+ token: 'initial-stale-token',
300
403
  getNewToken: async () => {
301
- console.log("Refreshing auth token...");
404
+ console.log('Refreshing auth token...');
302
405
  tokenProvider.token = `new-token-${Date.now()}`;
303
406
  return tokenProvider.token;
304
407
  },
@@ -314,7 +417,8 @@ const handler: AuthenticationHandler = {
314
417
  // shouldRetryWithHeaders() is called after a request fails.
315
418
  // It decides if a retry is needed and provides new headers.
316
419
  shouldRetryWithHeaders: async (req: RequestInit, res: Response) => {
317
- if (res.status === 401) { // Unauthorized
420
+ if (res.status === 401) {
421
+ // Unauthorized
318
422
  const newToken = await tokenProvider.getNewToken();
319
423
  // Return new headers to trigger a single retry.
320
424
  return { Authorization: `Bearer ${newToken}` };
@@ -328,14 +432,18 @@ const handler: AuthenticationHandler = {
328
432
  // 2. Create the authenticated fetch function.
329
433
  const authFetch = createAuthenticatingFetchWithRetry(fetch, handler);
330
434
 
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
- );
435
+ // 3. Inject new fetch implementation into a client factory.
436
+ const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
437
+ transports: [
438
+ new JsonRpcTransportFactory({ fetchImpl: authFetch })
439
+ ]
440
+ }))
441
+
442
+ // 4. Clients created from the factory are going to have custom fetch attached.
443
+ const client = await factory.createFromUrl('http://localhost:4000');
336
444
  ```
337
445
 
338
- -----
446
+ ---
339
447
 
340
448
  ## Streaming
341
449
 
@@ -350,44 +458,48 @@ The agent publishes events as it works on the task. The client receives these ev
350
458
  // ... imports ...
351
459
 
352
460
  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
- });
461
+ async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
462
+ const { taskId, contextId, userMessage, task } = requestContext;
463
+
464
+ // 1. Create and publish the initial task object if it doesn't exist.
465
+ if (!task) {
466
+ const initialTask: Task = {
467
+ kind: 'task',
468
+ id: taskId,
469
+ contextId: contextId,
470
+ status: {
471
+ state: 'submitted',
472
+ timestamp: new Date().toISOString(),
473
+ },
474
+ history: [userMessage],
475
+ };
476
+ eventBus.publish(initialTask);
477
+ }
366
478
 
367
479
  // 2. Publish 'working' state.
368
480
  eventBus.publish({
369
- kind: "status-update",
481
+ kind: 'status-update',
370
482
  taskId,
371
483
  contextId,
372
- status: { state: "working", timestamp: new Date().toISOString() },
373
- final: false
484
+ status: { state: 'working', timestamp: new Date().toISOString() },
485
+ final: false,
374
486
  });
375
487
 
376
488
  // 3. Simulate work and publish an artifact.
377
- await new Promise(resolve => setTimeout(resolve, 1000));
489
+ await new Promise((resolve) => setTimeout(resolve, 1000));
378
490
  eventBus.publish({
379
- kind: "artifact-update",
491
+ kind: 'artifact-update',
380
492
  taskId,
381
493
  contextId,
382
- artifact: { artifactId: "result.txt", parts: [{ kind: "text", text: "First result." }] },
494
+ artifact: { artifactId: 'result.txt', parts: [{ kind: 'text', text: 'First result.' }] },
383
495
  });
384
496
 
385
497
  // 4. Publish final 'completed' state.
386
498
  eventBus.publish({
387
- kind: "status-update",
499
+ kind: 'status-update',
388
500
  taskId,
389
501
  contextId,
390
- status: { state: "completed", timestamp: new Date().toISOString() },
502
+ status: { state: 'completed', timestamp: new Date().toISOString() },
391
503
  final: true,
392
504
  });
393
505
  eventBus.finished();
@@ -402,20 +514,24 @@ The `sendMessageStream` method returns an `AsyncGenerator` that yields events as
402
514
 
403
515
  ```typescript
404
516
  // client.ts
405
- import { A2AClient } from "@a2a-js/sdk/client";
406
- import { MessageSendParams } from "@a2a-js/sdk";
407
- import { v4 as uuidv4 } from "uuid";
517
+ import { ClientFactory } from '@mailmodo/a2a/client';
518
+ import { MessageSendParams } from '@mailmodo/a2a';
519
+ import { v4 as uuidv4 } from 'uuid';
408
520
  // ... other imports ...
409
521
 
410
- const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
522
+ const factory = new ClientFactory();
523
+
524
+ // createFromUrl accepts baseUrl and optional path,
525
+ // (the default path is /.well-known/agent-card.json)
526
+ const client = await factory.createFromUrl('http://localhost:4000');
411
527
 
412
528
  async function streamTask() {
413
529
  const streamParams: MessageSendParams = {
414
530
  message: {
415
531
  messageId: uuidv4(),
416
- role: "user",
417
- parts: [{ kind: "text", text: "Stream me some updates!" }],
418
- kind: "message",
532
+ role: 'user',
533
+ parts: [{ kind: 'text', text: 'Stream me some updates!' }],
534
+ kind: 'message',
419
535
  },
420
536
  };
421
537
 
@@ -423,17 +539,17 @@ async function streamTask() {
423
539
  const stream = client.sendMessageStream(streamParams);
424
540
 
425
541
  for await (const event of stream) {
426
- if (event.kind === "task") {
542
+ if (event.kind === 'task') {
427
543
  console.log(`[${event.id}] Task created. Status: ${event.status.state}`);
428
- } else if (event.kind === "status-update") {
544
+ } else if (event.kind === 'status-update') {
429
545
  console.log(`[${event.taskId}] Status Updated: ${event.status.state}`);
430
- } else if (event.kind === "artifact-update") {
546
+ } else if (event.kind === 'artifact-update') {
431
547
  console.log(`[${event.taskId}] Artifact Received: ${event.artifact.artifactId}`);
432
548
  }
433
549
  }
434
- console.log("--- Stream finished ---");
550
+ console.log('--- Stream finished ---');
435
551
  } catch (error) {
436
- console.error("Error during streaming:", error);
552
+ console.error('Error during streaming:', error);
437
553
  }
438
554
  }
439
555
 
@@ -457,7 +573,7 @@ import {
457
573
  RequestContext,
458
574
  ExecutionEventBus,
459
575
  TaskStatusUpdateEvent,
460
- } from "@a2a-js/sdk/server";
576
+ } from '@mailmodo/a2a/server';
461
577
  // ... other imports ...
462
578
 
463
579
  class CancellableExecutor implements AgentExecutor {
@@ -468,26 +584,20 @@ class CancellableExecutor implements AgentExecutor {
468
584
  * When a cancellation is requested, add the taskId to our tracking set.
469
585
  * The `execute` loop will handle the rest.
470
586
  */
471
- public async cancelTask(
472
- taskId: string,
473
- eventBus: ExecutionEventBus,
474
- ): Promise<void> {
587
+ public async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {
475
588
  console.log(`[Executor] Received cancellation request for task: ${taskId}`);
476
589
  this.cancelledTasks.add(taskId);
477
590
  }
478
591
 
479
- public async execute(
480
- requestContext: RequestContext,
481
- eventBus: ExecutionEventBus,
482
- ): Promise<void> {
592
+ public async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
483
593
  const { taskId, contextId } = requestContext;
484
594
 
485
595
  // Start the task
486
596
  eventBus.publish({
487
- kind: "status-update",
597
+ kind: 'status-update',
488
598
  taskId,
489
599
  contextId,
490
- status: { state: "working", timestamp: new Date().toISOString() },
600
+ status: { state: 'working', timestamp: new Date().toISOString() },
491
601
  final: false,
492
602
  });
493
603
 
@@ -497,18 +607,18 @@ class CancellableExecutor implements AgentExecutor {
497
607
  // Before each step, check if the task has been canceled.
498
608
  if (this.cancelledTasks.has(taskId)) {
499
609
  console.log(`[Executor] Aborting task ${taskId} due to cancellation.`);
500
-
610
+
501
611
  // Publish the final 'canceled' status.
502
612
  const cancelledUpdate: TaskStatusUpdateEvent = {
503
- kind: "status-update",
613
+ kind: 'status-update',
504
614
  taskId: taskId,
505
615
  contextId: contextId,
506
- status: { state: "canceled", timestamp: new Date().toISOString() },
616
+ status: { state: 'canceled', timestamp: new Date().toISOString() },
507
617
  final: true,
508
618
  };
509
619
  eventBus.publish(cancelledUpdate);
510
620
  eventBus.finished();
511
-
621
+
512
622
  // Clean up and exit.
513
623
  this.cancelledTasks.delete(taskId);
514
624
  return;
@@ -516,17 +626,17 @@ class CancellableExecutor implements AgentExecutor {
516
626
 
517
627
  // Simulate one step of work.
518
628
  console.log(`[Executor] Working on step ${i + 1} for task ${taskId}...`);
519
- await new Promise(resolve => setTimeout(resolve, 1000));
629
+ await new Promise((resolve) => setTimeout(resolve, 1000));
520
630
  }
521
631
 
522
632
  console.log(`[Executor] Task ${taskId} finished all steps without cancellation.`);
523
633
 
524
634
  // If not canceled, finish the work and publish the completed state.
525
635
  const finalUpdate: TaskStatusUpdateEvent = {
526
- kind: "status-update",
636
+ kind: 'status-update',
527
637
  taskId,
528
638
  contextId,
529
- status: { state: "completed", timestamp: new Date().toISOString() },
639
+ status: { state: 'completed', timestamp: new Date().toISOString() },
530
640
  final: true,
531
641
  };
532
642
  eventBus.publish(finalUpdate);
@@ -535,10 +645,112 @@ class CancellableExecutor implements AgentExecutor {
535
645
  }
536
646
  ```
537
647
 
648
+ ## A2A Push Notifications
649
+
650
+ 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.
651
+
652
+ ### Server-Side Configuration
653
+
654
+ To enable push notifications, your agent card must declare support:
655
+
656
+ ```typescript
657
+ const movieAgentCard: AgentCard = {
658
+ // ... other properties
659
+ capabilities: {
660
+ streaming: true,
661
+ pushNotifications: true, // Enable push notifications
662
+ stateTransitionHistory: true,
663
+ },
664
+ // ... rest of agent card
665
+ };
666
+ ```
667
+
668
+ When creating the `DefaultRequestHandler`, you can optionally provide custom push notification components:
669
+
670
+ ```typescript
671
+ import {
672
+ DefaultRequestHandler,
673
+ InMemoryPushNotificationStore,
674
+ DefaultPushNotificationSender,
675
+ } from '@mailmodo/a2a/server';
676
+
677
+ // Optional: Custom push notification store and sender
678
+ const pushNotificationStore = new InMemoryPushNotificationStore();
679
+ const pushNotificationSender = new DefaultPushNotificationSender(pushNotificationStore, {
680
+ timeout: 5000, // 5 second timeout
681
+ tokenHeaderName: 'X-A2A-Notification-Token', // Custom header name
682
+ });
683
+
684
+ const requestHandler = new DefaultRequestHandler(
685
+ movieAgentCard,
686
+ taskStore,
687
+ agentExecutor,
688
+ undefined, // eventBusManager (optional)
689
+ pushNotificationStore, // custom store
690
+ pushNotificationSender, // custom sender
691
+ undefined // extendedAgentCard (optional)
692
+ );
693
+ ```
694
+
695
+ ### Client-Side Usage
696
+
697
+ Configure push notifications when sending messages:
698
+
699
+ ```typescript
700
+ // Configure push notification for a message
701
+ const pushConfig: PushNotificationConfig = {
702
+ id: 'my-notification-config', // Optional, defaults to task ID
703
+ url: 'https://my-app.com/webhook/task-updates',
704
+ token: 'your-auth-token', // Optional authentication token
705
+ };
706
+
707
+ const sendParams: MessageSendParams = {
708
+ message: {
709
+ messageId: uuidv4(),
710
+ role: 'user',
711
+ parts: [{ kind: 'text', text: 'Hello, agent!' }],
712
+ kind: 'message',
713
+ },
714
+ configuration: {
715
+ blocking: true,
716
+ acceptedOutputModes: ['text/plain'],
717
+ pushNotificationConfig: pushConfig, // Add push notification config
718
+ },
719
+ };
720
+ ```
721
+
722
+ ### Webhook Endpoint Implementation
723
+
724
+ Your webhook endpoint should expect POST requests with the task data:
725
+
726
+ ```typescript
727
+ // Example Express.js webhook endpoint
728
+ app.post('/webhook/task-updates', (req, res) => {
729
+ const task = req.body; // The complete task object
730
+
731
+ // Verify the token if provided
732
+ const token = req.headers['x-a2a-notification-token'];
733
+ if (token !== 'your-auth-token') {
734
+ return res.status(401).json({ error: 'Unauthorized' });
735
+ }
736
+
737
+ console.log(`Task ${task.id} status: ${task.status.state}`);
738
+
739
+ // Process the task update
740
+ // ...
741
+
742
+ res.status(200).json({ received: true });
743
+ });
744
+ ```
745
+
538
746
  ## License
539
747
 
540
- This project is licensed under the terms of the [Apache 2.0 License](https://raw.githubusercontent.com/google-a2a/a2a-python/refs/heads/main/LICENSE).
748
+ This project is licensed under the terms of the [Apache 2.0 License](LICENSE).
749
+
750
+ ## Upstream & Contributing
751
+
752
+ This is a customized fork of the [official A2A JavaScript SDK](https://github.com/a2aproject/a2a-js) maintained by Mailmodo.
541
753
 
542
- ## Contributing
754
+ For contributing to the upstream project, see the [upstream CONTRIBUTING.md](https://github.com/a2aproject/a2a-js/blob/main/CONTRIBUTING.md).
543
755
 
544
- See [CONTRIBUTING.md](https://github.com/google-a2a/a2a-js/blob/main/CONTRIBUTING.md) for contribution guidelines.
756
+ For issues or contributions specific to this Mailmodo edition, please contact the Mailmodo team.