Skip to content

Commit 8da480e

Browse files
authored
docs: update docs for 0.3.6 (#246)
# Description 1. Update README.md, sample agents and sample CLI to use new APIs introduced in #162 (extract middlewares from `A2AExpressApp`) and #198 (transport-agnostic client). 2. Mark `A2AExpressApp` and `A2AClient` as deprecated. 3. Ensure samples and TCK import via index files only to "test" that all required types are exported. 4. Consolidate all `@example` tags to use ` ```ts `.
1 parent 6a3bc0b commit 8da480e

File tree

14 files changed

+156
-109
lines changed

14 files changed

+156
-109
lines changed

README.md

Lines changed: 109 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -45,29 +45,33 @@ The core of an A2A server is the `AgentExecutor`, which contains your agent's lo
4545
// server.ts
4646
import express from 'express';
4747
import { v4 as uuidv4 } from 'uuid';
48-
import type { AgentCard, Message } from '@a2a-js/sdk';
48+
import { AgentCard, Message, AGENT_CARD_PATH } from '@a2a-js/sdk';
4949
import {
5050
AgentExecutor,
5151
RequestContext,
5252
ExecutionEventBus,
5353
DefaultRequestHandler,
5454
InMemoryTaskStore,
5555
} from '@a2a-js/sdk/server';
56-
import { A2AExpressApp } from '@a2a-js/sdk/server/express';
56+
import { agentCardHandler, jsonRpcHandler, restHandler, UserBuilder } from '@a2a-js/sdk/server/express';
5757

5858
// 1. Define your agent's identity card.
5959
const helloAgentCard: AgentCard = {
6060
name: 'Hello Agent',
6161
description: 'A simple agent that says hello.',
6262
protocolVersion: '0.3.0',
6363
version: '0.1.0',
64-
url: 'http://localhost:4000/', // The public URL of your agent server
64+
url: 'http://localhost:4000/a2a/jsonrpc', // The public URL of your agent server
6565
skills: [{ id: 'chat', name: 'Chat', description: 'Say hello', tags: ['chat'] }],
6666
capabilities: {
6767
pushNotifications: false,
6868
},
6969
defaultInputModes: ['text'],
7070
defaultOutputModes: ['text'],
71+
additionalInterfaces: [
72+
{ url: 'http://localhost:4000/a2a/jsonrpc', transport: 'JSONRPC' }, // Default JSON-RPC transport
73+
{ url: 'http://localhost:4000/a2a/rest', transport: 'HTTP+JSON' }, // HTTP+JSON/REST transport
74+
],
7175
};
7276

7377
// 2. Implement the agent's logic.
@@ -100,27 +104,33 @@ const requestHandler = new DefaultRequestHandler(
100104
agentExecutor
101105
);
102106

103-
const appBuilder = new A2AExpressApp(requestHandler);
104-
const expressApp = appBuilder.setupRoutes(express());
107+
const app = express();
105108

106-
expressApp.listen(4000, () => {
109+
app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: requestHandler }));
110+
app.use('/a2a/jsonrpc', jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
111+
app.use('/a2a/rest', restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
112+
113+
app.listen(4000, () => {
107114
console.log(`🚀 Server started on http://localhost:4000`);
108115
});
109116
```
110117

111118
### Client: Sending a Message
112119

113-
The `A2AClient` makes it easy to communicate with any A2A-compliant agent.
120+
The [`ClientFactory`](src/client/factory.ts) makes it easy to communicate with any A2A-compliant agent.
114121

115122
```typescript
116123
// client.ts
117-
import { A2AClient, SendMessageSuccessResponse } from '@a2a-js/sdk/client';
124+
import { ClientFactory, SendMessageSuccessResponse } from '@a2a-js/sdk/client';
118125
import { Message, MessageSendParams } from '@a2a-js/sdk';
119126
import { v4 as uuidv4 } from 'uuid';
120127

121128
async function run() {
122-
// Create a client pointing to the agent's Agent Card URL.
123-
const client = await A2AClient.fromCardUrl('http://localhost:4000/.well-known/agent-card.json');
129+
const factory = new ClientFactory();
130+
131+
// createFromUrl accepts baseUrl and optional path,
132+
// (the default path is /.well-known/agent-card.json)
133+
const client = await factory.createFromUrl('http://localhost:4000');
124134

125135
const sendParams: MessageSendParams = {
126136
message: {
@@ -131,13 +141,12 @@ async function run() {
131141
},
132142
};
133143

134-
const response = await client.sendMessage(sendParams);
135-
136-
if ('error' in response) {
137-
console.error('Error:', response.error.message);
138-
} else {
139-
const result = (response as SendMessageSuccessResponse).result as Message;
144+
try {
145+
const response = await client.sendMessage(sendParams);
146+
const result = response as Message;
140147
console.log('Agent response:', result.parts[0].text); // "Hello, world!"
148+
} catch(e) {
149+
console.error('Error:', e);
141150
}
142151
}
143152

@@ -213,25 +222,25 @@ The client sends a message and receives a `Task` object as the result.
213222

214223
```typescript
215224
// client.ts
216-
import { A2AClient, SendMessageSuccessResponse } from '@a2a-js/sdk/client';
225+
import { ClientFactory, SendMessageSuccessResponse } from '@a2a-js/sdk/client';
217226
import { Message, MessageSendParams, Task } from '@a2a-js/sdk';
218227
// ... other imports ...
219228

220-
const client = await A2AClient.fromCardUrl('http://localhost:4000/.well-known/agent-card.json');
229+
const factory = new ClientFactory();
221230

222-
const response = await client.sendMessage({
223-
message: {
224-
messageId: uuidv4(),
225-
role: 'user',
226-
parts: [{ kind: 'text', text: 'Do something.' }],
227-
kind: 'message',
228-
},
229-
});
231+
// createFromUrl accepts baseUrl and optional path,
232+
// (the default path is /.well-known/agent-card.json)
233+
const client = await factory.createFromUrl('http://localhost:4000');
230234

231-
if ('error' in response) {
232-
console.error('Error:', response.error.message);
233-
} else {
234-
const result = (response as SendMessageSuccessResponse).result;
235+
try {
236+
const result = await client.sendMessage({
237+
message: {
238+
messageId: uuidv4(),
239+
role: 'user',
240+
parts: [{ kind: 'text', text: 'Do something.' }],
241+
kind: 'message',
242+
},
243+
});
235244

236245
// Check if the agent's response is a Task or a direct Message.
237246
if (result.kind === 'task') {
@@ -246,45 +255,58 @@ if ('error' in response) {
246255
const message = result as Message;
247256
console.log('Received direct message:', message.parts[0].text);
248257
}
258+
} catch (e) {
259+
console.error('Error:', e);
249260
}
250261
```
251262

252263
---
253264

254265
## Client Customization
255266

256-
You can provide a custom `fetch` implementation to the `A2AClient` to modify its HTTP request behavior. Common use cases include:
267+
Client can be customized via [`CallInterceptor`'s](src/client/interceptors.ts) which is a recommended way as it's transport-agnostic.
268+
269+
Common use cases include:
257270

258271
- **Request Interception**: Log outgoing requests or collect metrics.
259272
- **Header Injection**: Add custom headers for authentication, tracing, or routing.
260-
- **Retry Mechanisms**: Implement custom logic for retrying failed requests.
273+
- **A2A Extensions**: Modifying payloads to include protocol extension data.
261274

262275
### Example: Injecting a Custom Header
263276

264-
This example creates a `fetch` wrapper that adds a unique `X-Request-ID` to every outgoing request.
277+
This example defines a `CallInterceptor` to update `serviceParameters` which are passed as HTTP headers.
265278

266279
```typescript
267-
import { A2AClient } from '@a2a-js/sdk/client';
268280
import { v4 as uuidv4 } from 'uuid';
281+
import { AfterArgs, BeforeArgs, CallInterceptor, ClientFactory, ClientFactoryOptions } from '@a2a-js/sdk/client';
282+
283+
// 1. Define an interceptor
284+
class RequestIdInterceptor implements CallInterceptor {
285+
before(args: BeforeArgs): Promise<void> {
286+
args.options = {
287+
...args.options,
288+
serviceParameters: {
289+
...args.options.serviceParameters,
290+
['X-Request-ID']: uuidv4(),
291+
},
292+
};
293+
return Promise.resolve();
294+
}
269295

270-
// 1. Create a wrapper around the global fetch function.
271-
const fetchWithCustomHeader: typeof fetch = async (url, init) => {
272-
const headers = new Headers(init?.headers);
273-
headers.set('X-Request-ID', uuidv4());
274-
275-
const newInit = { ...init, headers };
276-
277-
console.log(`Sending request to ${url} with X-Request-ID: ${headers.get('X-Request-ID')}`);
278-
279-
return fetch(url, newInit);
280-
};
296+
after(): Promise<void> {
297+
return Promise.resolve();
298+
}
299+
}
281300

282-
// 2. Provide the custom fetch implementation to the client.
283-
const client = await A2AClient.fromCardUrl('http://localhost:4000/.well-known/agent-card.json', {
284-
fetchImpl: fetchWithCustomHeader,
285-
});
301+
// 2. Register the interceptor in the client factory
302+
const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
303+
clientConfig: {
304+
interceptors: [new RequestIdInterceptor()]
305+
}
306+
}))
307+
const client = await factory.createFromAgentCardUrl('http://localhost:4000');
286308

287-
// Now, all requests made by this client instance will include the X-Request-ID header.
309+
// Now, all requests made by clients created by this factory will include the X-Request-ID header.
288310
await client.sendMessage({
289311
message: {
290312
messageId: uuidv4(),
@@ -297,41 +319,43 @@ await client.sendMessage({
297319

298320
### Example: Specifying a Timeout
299321

300-
This example creates a `fetch` wrapper that sets a timeout for every outgoing request.
322+
Each client method can be configured with an optional `signal` field.
301323

302324
```typescript
303-
import { A2AClient } from '@a2a-js/sdk/client';
325+
import { ClientFactory } from '@a2a-js/sdk/client';
304326

305-
// 1. Create a wrapper around the global fetch function.
306-
const fetchWithTimeout: typeof fetch = async (url, init) => {
307-
return fetch(url, { ...init, signal: AbortSignal.timeout(5000) });
308-
};
327+
const factory = new ClientFactory();
309328

310-
// 2. Provide the custom fetch implementation to the client.
311-
const client = await A2AClient.fromCardUrl('http://localhost:4000/.well-known/agent-card.json', {
312-
fetchImpl: fetchWithTimeout,
313-
});
329+
// createFromUrl accepts baseUrl and optional path,
330+
// (the default path is /.well-known/agent-card.json)
331+
const client = await factory.createFromUrl('http://localhost:4000');
314332

315-
// Now, all requests made by this client instance will have a configured timeout.
316-
await client.sendMessage({
317-
message: {
318-
messageId: uuidv4(),
319-
role: 'user',
320-
parts: [{ kind: 'text', text: 'A message requiring custom headers.' }],
321-
kind: 'message',
333+
await client.sendMessage(
334+
{
335+
message: {
336+
messageId: uuidv4(),
337+
role: 'user',
338+
parts: [{ kind: 'text', text: 'A long-running message.' }],
339+
kind: 'message',
340+
},
322341
},
323-
});
342+
{
343+
signal: AbortSignal.timeout(5000), // 5 seconds timeout
344+
}
345+
);
324346
```
325347

326-
### Using the Provided `AuthenticationHandler`
348+
### Customizing Transports: Using the Provided `AuthenticationHandler`
327349

328350
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).
329351

330352
Here's how to use it to manage a Bearer token:
331353

332354
```typescript
333355
import {
334-
A2AClient,
356+
ClientFactory,
357+
ClientFactoryOptions
358+
JsonRpcTransportFactory,
335359
AuthenticationHandler,
336360
createAuthenticatingFetchWithRetry,
337361
} from '@a2a-js/sdk/client';
@@ -371,10 +395,15 @@ const handler: AuthenticationHandler = {
371395
// 2. Create the authenticated fetch function.
372396
const authFetch = createAuthenticatingFetchWithRetry(fetch, handler);
373397

374-
// 3. Initialize the client with the new fetch implementation.
375-
const client = await A2AClient.fromCardUrl('http://localhost:4000/.well-known/agent-card.json', {
376-
fetchImpl: authFetch,
377-
});
398+
// 3. Inject new fetch implementation into a client factory.
399+
const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
400+
transports: [
401+
new JsonRpcTransportFactory({ fetchImpl: authFetch })
402+
]
403+
}))
404+
405+
// 4. Clients created from the factory are going to have custom fetch attached.
406+
const client = await factory.createFromUrl('http://localhost:4000');
378407
```
379408

380409
---
@@ -448,12 +477,16 @@ The `sendMessageStream` method returns an `AsyncGenerator` that yields events as
448477

449478
```typescript
450479
// client.ts
451-
import { A2AClient } from '@a2a-js/sdk/client';
480+
import { ClientFactory } from '@a2a-js/sdk/client';
452481
import { MessageSendParams } from '@a2a-js/sdk';
453482
import { v4 as uuidv4 } from 'uuid';
454483
// ... other imports ...
455484

456-
const client = await A2AClient.fromCardUrl('http://localhost:4000/.well-known/agent-card.json');
485+
const factory = new ClientFactory();
486+
487+
// createFromUrl accepts baseUrl and optional path,
488+
// (the default path is /.well-known/agent-card.json)
489+
const client = await factory.createFromUrl('http://localhost:4000');
457490

458491
async function streamTask() {
459492
const streamParams: MessageSendParams = {

src/client/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface A2AClientOptions {
3737
/**
3838
* A2AClient is a TypeScript HTTP client for interacting with A2A-compliant agents.
3939
* Only JSON-RPC transport is supported.
40+
* @deprecated Use {@link ClientFactory}
4041
*/
4142
export class A2AClient {
4243
private static emptyOptions?: RequestOptions = undefined;

src/samples/agents/movie-agent/index.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TaskStatusUpdateEvent,
99
TextPart,
1010
Message,
11+
AGENT_CARD_PATH,
1112
} from '../../../index.js';
1213
import {
1314
InMemoryTaskStore,
@@ -17,7 +18,7 @@ import {
1718
ExecutionEventBus,
1819
DefaultRequestHandler,
1920
} from '../../../server/index.js';
20-
import { A2AExpressApp } from '../../../server/express/index.js';
21+
import { agentCardHandler, jsonRpcHandler, UserBuilder } from '../../../server/express/index.js';
2122
import { MessageData } from 'genkit';
2223
import { ai } from './genkit.js';
2324
import { searchMovies, searchPeople } from './tools.js';
@@ -247,8 +248,7 @@ class MovieAgentExecutor implements AgentExecutor {
247248
const movieAgentCard: AgentCard = {
248249
name: 'Movie Agent',
249250
description: 'An agent that can answer questions about movies and actors using TMDB.',
250-
// Adjust the base URL and port as needed. /a2a is the default base in A2AExpressApp
251-
url: 'http://localhost:41241/', // Example: if baseUrl in A2AExpressApp
251+
url: 'http://localhost:41241/',
252252
provider: {
253253
organization: 'A2A Samples',
254254
url: 'https://example.com/a2a-samples', // Added provider URL
@@ -296,13 +296,15 @@ async function main() {
296296
// 3. Create DefaultRequestHandler
297297
const requestHandler = new DefaultRequestHandler(movieAgentCard, taskStore, agentExecutor);
298298

299-
// 4. Create and setup A2AExpressApp
300-
const appBuilder = new A2AExpressApp(requestHandler);
301-
const expressApp = appBuilder.setupRoutes(express());
299+
// 4. Create and setup Express.js app
300+
const app = express();
301+
302+
app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: requestHandler }));
303+
app.use(jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
302304

303305
// 5. Start the server
304306
const PORT = process.env.PORT || 41241;
305-
expressApp.listen(PORT, (err) => {
307+
app.listen(PORT, (err) => {
306308
if (err) {
307309
throw err;
308310
}

0 commit comments

Comments
 (0)