@a2a-js/sdk 0.3.2 → 0.3.4
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 +506 -314
- package/dist/client/index.cjs +171 -71
- package/dist/client/index.d.cts +67 -34
- package/dist/client/index.d.ts +67 -34
- package/dist/client/index.js +171 -71
- package/dist/server/index.cjs +136 -19
- package/dist/server/index.d.cts +40 -4
- package/dist/server/index.d.ts +40 -4
- package/dist/server/index.js +134 -19
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
You can install the A2A SDK using
|
|
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,609 @@ 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
|
-
|
|
34
|
+
-----
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
## Quickstart
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npm install express
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### 1. Define Agent Card
|
|
38
|
+
This example shows how to create a simple "Hello World" agent server and a client to interact with it.
|
|
44
39
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
//
|
|
150
|
-
const
|
|
151
|
-
kind: "
|
|
152
|
-
|
|
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: "
|
|
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(
|
|
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
|
-
//
|
|
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: "
|
|
198
|
-
name: "
|
|
199
|
-
parts: [{ kind: "text", text: `
|
|
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
|
-
//
|
|
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
|
-
###
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
const taskStore: TaskStore = new InMemoryTaskStore();
|
|
234
|
-
const agentExecutor: AgentExecutor = new MyAgentExecutor();
|
|
209
|
+
### Client: Receiving a Task
|
|
235
210
|
|
|
236
|
-
|
|
237
|
-
coderAgentCard,
|
|
238
|
-
taskStore,
|
|
239
|
-
agentExecutor
|
|
240
|
-
);
|
|
211
|
+
The client sends a message and receives a `Task` object as the result.
|
|
241
212
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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-card.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
|
-
|
|
219
|
+
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
|
|
258
220
|
|
|
259
|
-
|
|
221
|
+
const response = await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "Do something." }], kind: "message" } });
|
|
260
222
|
|
|
261
|
-
|
|
223
|
+
if ("error" in response) {
|
|
224
|
+
console.error("Error:", response.error.message);
|
|
225
|
+
} else {
|
|
226
|
+
const result = (response as SendMessageSuccessResponse).result;
|
|
262
227
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
-----
|
|
273
245
|
|
|
274
|
-
##
|
|
246
|
+
## Client Customization
|
|
275
247
|
|
|
276
|
-
|
|
248
|
+
You can provide a custom `fetch` implementation to the `A2AClient` to modify its HTTP request behavior. Common use cases include:
|
|
277
249
|
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
console.error("Error sending message:", sendResponse.error);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
284
|
+
### Using the Provided `AuthenticationHandler`
|
|
330
285
|
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
###
|
|
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
|
|
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 =
|
|
410
|
+
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
|
|
381
411
|
|
|
382
412
|
async function streamTask() {
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
console.log(
|
|
406
|
-
|
|
407
|
-
);
|
|
408
|
-
continue;
|
|
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}`);
|
|
409
432
|
}
|
|
433
|
+
}
|
|
434
|
+
console.log("--- Stream finished ---");
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error("Error during streaming:", error);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
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;
|
|
410
484
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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;
|
|
436
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));
|
|
437
520
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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();
|
|
441
534
|
}
|
|
442
535
|
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## A2A Push Notifications
|
|
539
|
+
|
|
540
|
+
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.
|
|
541
|
+
|
|
542
|
+
### Server-Side Configuration
|
|
543
|
+
|
|
544
|
+
To enable push notifications, your agent card must declare support:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
const movieAgentCard: AgentCard = {
|
|
548
|
+
// ... other properties
|
|
549
|
+
capabilities: {
|
|
550
|
+
streaming: true,
|
|
551
|
+
pushNotifications: true, // Enable push notifications
|
|
552
|
+
stateTransitionHistory: true,
|
|
553
|
+
},
|
|
554
|
+
// ... rest of agent card
|
|
555
|
+
};
|
|
556
|
+
```
|
|
443
557
|
|
|
444
|
-
|
|
558
|
+
When creating the `DefaultRequestHandler`, you can optionally provide custom push notification components:
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import {
|
|
562
|
+
DefaultRequestHandler,
|
|
563
|
+
InMemoryPushNotificationStore,
|
|
564
|
+
DefaultPushNotificationSender
|
|
565
|
+
} from "@a2a-js/sdk/server";
|
|
566
|
+
|
|
567
|
+
// Optional: Custom push notification store and sender
|
|
568
|
+
const pushNotificationStore = new InMemoryPushNotificationStore();
|
|
569
|
+
const pushNotificationSender = new DefaultPushNotificationSender(
|
|
570
|
+
pushNotificationStore,
|
|
571
|
+
{
|
|
572
|
+
timeout: 5000, // 5 second timeout
|
|
573
|
+
tokenHeaderName: 'X-A2A-Notification-Token' // Custom header name
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const requestHandler = new DefaultRequestHandler(
|
|
578
|
+
movieAgentCard,
|
|
579
|
+
taskStore,
|
|
580
|
+
agentExecutor,
|
|
581
|
+
undefined, // eventBusManager (optional)
|
|
582
|
+
pushNotificationStore, // custom store
|
|
583
|
+
pushNotificationSender, // custom sender
|
|
584
|
+
undefined // extendedAgentCard (optional)
|
|
585
|
+
);
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Client-Side Usage
|
|
589
|
+
|
|
590
|
+
Configure push notifications when sending messages:
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
// Configure push notification for a message
|
|
594
|
+
const pushConfig: PushNotificationConfig = {
|
|
595
|
+
id: "my-notification-config", // Optional, defaults to task ID
|
|
596
|
+
url: "https://my-app.com/webhook/task-updates",
|
|
597
|
+
token: "your-auth-token" // Optional authentication token
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const sendParams: MessageSendParams = {
|
|
601
|
+
message: {
|
|
602
|
+
messageId: uuidv4(),
|
|
603
|
+
role: "user",
|
|
604
|
+
parts: [{ kind: "text", text: "Hello, agent!" }],
|
|
605
|
+
kind: "message",
|
|
606
|
+
},
|
|
607
|
+
configuration: {
|
|
608
|
+
blocking: true,
|
|
609
|
+
acceptedOutputModes: ["text/plain"],
|
|
610
|
+
pushNotificationConfig: pushConfig // Add push notification config
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Webhook Endpoint Implementation
|
|
616
|
+
|
|
617
|
+
Your webhook endpoint should expect POST requests with the task data:
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// Example Express.js webhook endpoint
|
|
621
|
+
app.post('/webhook/task-updates', (req, res) => {
|
|
622
|
+
const task = req.body; // The complete task object
|
|
623
|
+
|
|
624
|
+
// Verify the token if provided
|
|
625
|
+
const token = req.headers['x-a2a-notification-token'];
|
|
626
|
+
if (token !== 'your-auth-token') {
|
|
627
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
console.log(`Task ${task.id} status: ${task.status.state}`);
|
|
631
|
+
|
|
632
|
+
// Process the task update
|
|
633
|
+
// ...
|
|
634
|
+
|
|
635
|
+
res.status(200).json({ received: true });
|
|
636
|
+
});
|
|
445
637
|
```
|
|
446
638
|
|
|
447
639
|
## License
|