@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 +385 -173
- package/dist/a2a_request_handler-1Isk3l-0.d.mts +49 -0
- package/dist/a2a_request_handler-BuP9LgXH.d.ts +49 -0
- package/dist/chunk-7JFJW6P6.mjs +38 -0
- package/dist/chunk-AZGEDUZX.mjs +261 -0
- package/dist/chunk-BNBEZNW7.mjs +122 -0
- package/dist/chunk-PHP7LM4Y.mjs +8 -0
- package/dist/client/index.d.mts +666 -0
- package/dist/client/index.d.ts +666 -0
- package/dist/client/index.js +1605 -0
- package/dist/client/index.mjs +1448 -0
- package/dist/extensions-DvruCIzw.d.mts +2589 -0
- package/dist/extensions-DvruCIzw.d.ts +2589 -0
- package/dist/index.d.mts +5 -2758
- package/dist/index.d.ts +5 -2758
- package/dist/index.js +28 -1637
- package/dist/index.mjs +9 -1628
- package/dist/server/express/index.d.mts +97 -0
- package/dist/server/express/index.d.ts +97 -0
- package/dist/server/express/index.js +994 -0
- package/dist/server/express/index.mjs +646 -0
- package/dist/server/index.d.mts +316 -0
- package/dist/server/index.d.ts +316 -0
- package/dist/server/index.js +1386 -0
- package/dist/server/index.mjs +1070 -0
- package/package.json +51 -22
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# A2A JavaScript SDK
|
|
1
|
+
# A2A JavaScript SDK - Mailmodo Edition
|
|
2
2
|
|
|
3
3
|
[](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
|
|
25
|
+
npm install @mailmodo/a2a
|
|
22
26
|
```
|
|
23
27
|
|
|
24
28
|
### For Server Usage
|
|
25
29
|
|
|
26
|
-
If you plan to use the
|
|
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
|
|
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
|
|
47
|
-
import { v4 as uuidv4 } from
|
|
48
|
-
import
|
|
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
|
|
56
|
-
import {
|
|
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:
|
|
61
|
-
description:
|
|
62
|
-
protocolVersion:
|
|
63
|
-
version:
|
|
64
|
-
url:
|
|
65
|
-
skills: [
|
|
66
|
-
|
|
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:
|
|
119
|
+
kind: 'message',
|
|
78
120
|
messageId: uuidv4(),
|
|
79
|
-
role:
|
|
80
|
-
parts: [{ kind:
|
|
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
|
|
103
|
-
const expressApp = appBuilder.setupRoutes(express());
|
|
144
|
+
const app = express();
|
|
104
145
|
|
|
105
|
-
|
|
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 `
|
|
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 {
|
|
117
|
-
import { Message, MessageSendParams } from
|
|
118
|
-
import { v4 as uuidv4 } from
|
|
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
|
-
|
|
122
|
-
|
|
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:
|
|
128
|
-
parts: [{ kind:
|
|
129
|
-
kind:
|
|
175
|
+
role: 'user',
|
|
176
|
+
parts: [{ kind: 'text', text: 'Hi there!' }],
|
|
177
|
+
kind: 'message',
|
|
130
178
|
},
|
|
131
179
|
};
|
|
132
180
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.
|
|
137
|
-
}
|
|
138
|
-
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
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:
|
|
229
|
+
kind: 'artifact-update',
|
|
183
230
|
taskId: taskId,
|
|
184
231
|
contextId: contextId,
|
|
185
232
|
artifact: {
|
|
186
|
-
artifactId:
|
|
187
|
-
name:
|
|
188
|
-
parts: [{ kind:
|
|
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:
|
|
242
|
+
kind: 'status-update',
|
|
196
243
|
taskId: taskId,
|
|
197
244
|
contextId: contextId,
|
|
198
|
-
status: { state:
|
|
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 {
|
|
216
|
-
import { Message, MessageSendParams, Task } from
|
|
262
|
+
import { ClientFactory } from '@mailmodo/a2a/client';
|
|
263
|
+
import { Message, MessageSendParams, SendMessageSuccessResponse, Task } from '@mailmodo/a2a';
|
|
217
264
|
// ... other imports ...
|
|
218
265
|
|
|
219
|
-
const
|
|
266
|
+
const factory = new ClientFactory();
|
|
220
267
|
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 ===
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
314
|
+
This example defines a `CallInterceptor` to update `serviceParameters` which are passed as HTTP headers.
|
|
257
315
|
|
|
258
316
|
```typescript
|
|
259
|
-
import {
|
|
260
|
-
import {
|
|
261
|
-
|
|
262
|
-
// 1.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
393
|
+
ClientFactory,
|
|
394
|
+
ClientFactoryOptions,
|
|
395
|
+
JsonRpcTransportFactory,
|
|
293
396
|
AuthenticationHandler,
|
|
294
397
|
createAuthenticatingFetchWithRetry,
|
|
295
|
-
} from
|
|
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:
|
|
402
|
+
token: 'initial-stale-token',
|
|
300
403
|
getNewToken: async () => {
|
|
301
|
-
console.log(
|
|
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) {
|
|
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.
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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:
|
|
481
|
+
kind: 'status-update',
|
|
370
482
|
taskId,
|
|
371
483
|
contextId,
|
|
372
|
-
status: { state:
|
|
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:
|
|
491
|
+
kind: 'artifact-update',
|
|
380
492
|
taskId,
|
|
381
493
|
contextId,
|
|
382
|
-
artifact: { artifactId:
|
|
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:
|
|
499
|
+
kind: 'status-update',
|
|
388
500
|
taskId,
|
|
389
501
|
contextId,
|
|
390
|
-
status: { state:
|
|
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 {
|
|
406
|
-
import { MessageSendParams } from
|
|
407
|
-
import { v4 as uuidv4 } from
|
|
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
|
|
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:
|
|
417
|
-
parts: [{ kind:
|
|
418
|
-
kind:
|
|
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 ===
|
|
542
|
+
if (event.kind === 'task') {
|
|
427
543
|
console.log(`[${event.id}] Task created. Status: ${event.status.state}`);
|
|
428
|
-
} else if (event.kind ===
|
|
544
|
+
} else if (event.kind === 'status-update') {
|
|
429
545
|
console.log(`[${event.taskId}] Status Updated: ${event.status.state}`);
|
|
430
|
-
} else if (event.kind ===
|
|
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(
|
|
550
|
+
console.log('--- Stream finished ---');
|
|
435
551
|
} catch (error) {
|
|
436
|
-
console.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
|
|
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:
|
|
597
|
+
kind: 'status-update',
|
|
488
598
|
taskId,
|
|
489
599
|
contextId,
|
|
490
|
-
status: { state:
|
|
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:
|
|
613
|
+
kind: 'status-update',
|
|
504
614
|
taskId: taskId,
|
|
505
615
|
contextId: contextId,
|
|
506
|
-
status: { state:
|
|
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:
|
|
636
|
+
kind: 'status-update',
|
|
527
637
|
taskId,
|
|
528
638
|
contextId,
|
|
529
|
-
status: { state:
|
|
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](
|
|
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
|
-
|
|
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
|
-
|
|
756
|
+
For issues or contributions specific to this Mailmodo edition, please contact the Mailmodo team.
|