Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 75 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,37 @@ The `A2AClient` makes it easy to communicate with any A2A-compliant agent.

```typescript
// client.ts
import { A2AClient, SendMessageSuccessResponse } from "@a2a-js/sdk/client";
import { A2AClient, A2AClientError, isMessage, withResultType } from "@a2a-js/sdk/client";
import { Message, MessageSendParams } from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";

async function run() {
// Create a client pointing to the agent's Agent Card URL.
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");

const sendParams: MessageSendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [{ kind: "text", text: "Hi there!" }],
kind: "message",
},
};

const response = await client.sendMessage(sendParams);
try {
// Create a client pointing to the agent's Agent Card URL.
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");

const sendParams: MessageSendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [{ kind: "text", text: "Hi there!" }],
kind: "message",
},
};

if ("error" in response) {
console.error("Error:", response.error.message);
} else {
const result = (response as SendMessageSuccessResponse).result as Message;
console.log("Agent response:", result.parts[0].text); // "Hello, world!"
// Type-safe response handling - no manual error checking needed
const response = await client.sendMessage(sendParams);

// Type guards for automatic type inference
if (isMessage(response.result)) {
console.log("Agent response:", response.result.parts[0].text); // "Hello, world!"
}
} catch (error) {
if (error instanceof A2AClientError) {
console.error(`A2A Error (${error.rpcError.code}): ${error.rpcError.message}`);
} else {
console.error("Unexpected error:", error);
}
}
}

Expand Down Expand Up @@ -212,33 +219,48 @@ The client sends a message and receives a `Task` object as the result.

```typescript
// client.ts
import { A2AClient, SendMessageSuccessResponse } from "@a2a-js/sdk/client";
import { Message, MessageSendParams, Task } from "@a2a-js/sdk";
// ... other imports ...

const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");
import { A2AClient, A2AClientError, isTask, isMessage, withResultType } from "@a2a-js/sdk/client";
import { MessageSendParams } from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";

const response = await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "Do something." }], kind: "message" } });
async function run() {
try {
const client = await A2AClient.fromCardUrl("http://localhost:4000/.well-known/agent-card.json");

const response = await client.sendMessage({
message: {
messageId: uuidv4(),
role: "user",
parts: [{ kind: "text", text: "Do something." }],
kind: "message"
}
});

if ("error" in response) {
console.error("Error:", response.error.message);
} else {
const result = (response as SendMessageSuccessResponse).result;
// Pattern-based result processing with automatic type inference
withResultType(response.result, {
task: (task) => {
console.log(`Task [${task.id}] completed with status: ${task.status.state}`);

// Check if the agent's response is a Task or a direct Message.
if (result.kind === "task") {
const task = result as Task;
console.log(`Task [${task.id}] completed with status: ${task.status.state}`);
if (task.artifacts && task.artifacts.length > 0) {
console.log(`Artifact found: ${task.artifacts[0].name}`);
console.log(`Content: ${task.artifacts[0].parts[0].text}`);
}
},
message: (message) => {
console.log("Received direct message:", message.parts[0].text);
}
});

if (task.artifacts && task.artifacts.length > 0) {
console.log(`Artifact found: ${task.artifacts[0].name}`);
console.log(`Content: ${task.artifacts[0].parts[0].text}`);
} catch (error) {
if (error instanceof A2AClientError) {
console.error(`A2A Error (${error.rpcError.code}): ${error.rpcError.message}`);
} else {
console.error("Unexpected error:", error);
}
} else {
const message = result as Message;
console.log("Received direct message:", message.parts[0].text);
}
}

await run();
```

-----
Expand Down Expand Up @@ -278,7 +300,20 @@ const client = await A2AClient.fromCardUrl(
);

// Now, all requests made by this client instance will include the X-Request-ID header.
await client.sendMessage({ message: { messageId: uuidv4(), role: "user", parts: [{ kind: "text", text: "A message requiring custom headers." }], kind: "message" } });
try {
await client.sendMessage({
message: {
messageId: uuidv4(),
role: "user",
parts: [{ kind: "text", text: "A message requiring custom headers." }],
kind: "message"
}
});
} catch (error) {
if (error instanceof A2AClientError) {
console.error(`Request failed: ${error.rpcError.message}`);
}
}
```

### Using the Provided `AuthenticationHandler`
Expand Down
90 changes: 51 additions & 39 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,19 @@ import {
SendMessageSuccessResponse,
ListTaskPushNotificationConfigParams,
ListTaskPushNotificationConfigResponse,
ListTaskPushNotificationConfigSuccessResponse,
DeleteTaskPushNotificationConfigResponse,
DeleteTaskPushNotificationConfigSuccessResponse,
DeleteTaskPushNotificationConfigParams
} from '../types.js'; // Assuming schema.ts is in the same directory or appropriately pathed
import { AGENT_CARD_PATH } from "../constants.js";
import {
parseSuccessResponse,
A2AClientError,
FilterSuccessResponse,
A2ASuccessResponse,
isErrorResponse
} from "./response-utils.js";

// Helper type for the data yielded by streaming methods
type A2AStreamEventData = Message | Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent;
Expand Down Expand Up @@ -215,10 +224,12 @@ export class A2AClient {
* are specified within the `params.configuration` object.
* Optionally, `params.message.contextId` or `params.message.taskId` can be provided.
* @param params The parameters for sending the message, including the message content and configuration.
* @returns A Promise resolving to SendMessageResponse, which can be a Message, Task, or an error.
* @returns A Promise resolving to SendMessageSuccessResponse (Message or Task). Throws A2AClientError for error responses.
* @throws A2AClientError if the response contains an error.
*/
public async sendMessage(params: MessageSendParams): Promise<SendMessageResponse> {
return this._postRpcRequest<MessageSendParams, SendMessageResponse>("message/send", params);
public async sendMessage(params: MessageSendParams): Promise<SendMessageSuccessResponse> {
const response = await this._postRpcRequest<MessageSendParams, SendMessageResponse>("message/send", params);
return parseSuccessResponse(response);
}

/**
Expand Down Expand Up @@ -277,74 +288,86 @@ export class A2AClient {
* Sets or updates the push notification configuration for a given task.
* Requires the agent to support push notifications (`capabilities.pushNotifications: true` in AgentCard).
* @param params Parameters containing the taskId and the TaskPushNotificationConfig.
* @returns A Promise resolving to SetTaskPushNotificationConfigResponse.
* @returns A Promise resolving to SetTaskPushNotificationConfigSuccessResponse. Throws A2AClientError for error responses.
* @throws A2AClientError if the response contains an error.
*/
public async setTaskPushNotificationConfig(params: TaskPushNotificationConfig): Promise<SetTaskPushNotificationConfigResponse> {
public async setTaskPushNotificationConfig(params: TaskPushNotificationConfig): Promise<SetTaskPushNotificationConfigSuccessResponse> {
const agentCard = await this.agentCardPromise;
if (!agentCard.capabilities?.pushNotifications) {
throw new Error("Agent does not support push notifications (AgentCard.capabilities.pushNotifications is not true).");
}
// The 'params' directly matches the structure expected by the RPC method.
return this._postRpcRequest<TaskPushNotificationConfig, SetTaskPushNotificationConfigResponse>(
const response = await this._postRpcRequest<TaskPushNotificationConfig, SetTaskPushNotificationConfigResponse>(
"tasks/pushNotificationConfig/set",
params
);
return parseSuccessResponse(response);
}

/**
* Gets the push notification configuration for a given task.
* @param params Parameters containing the taskId.
* @returns A Promise resolving to GetTaskPushNotificationConfigResponse.
* @returns A Promise resolving to GetTaskPushNotificationConfigSuccessResponse. Throws A2AClientError for error responses.
* @throws A2AClientError if the response contains an error.
*/
public async getTaskPushNotificationConfig(params: TaskIdParams): Promise<GetTaskPushNotificationConfigResponse> {
public async getTaskPushNotificationConfig(params: TaskIdParams): Promise<GetTaskPushNotificationConfigSuccessResponse> {
// The 'params' (TaskIdParams) directly matches the structure expected by the RPC method.
return this._postRpcRequest<TaskIdParams, GetTaskPushNotificationConfigResponse>(
const response = await this._postRpcRequest<TaskIdParams, GetTaskPushNotificationConfigResponse>(
"tasks/pushNotificationConfig/get",
params
);
return parseSuccessResponse(response);
}

/**
* Lists the push notification configurations for a given task.
* @param params Parameters containing the taskId.
* @returns A Promise resolving to ListTaskPushNotificationConfigResponse.
* @returns A Promise resolving to ListTaskPushNotificationConfigSuccessResponse. Throws A2AClientError for error responses.
* @throws A2AClientError if the response contains an error.
*/
public async listTaskPushNotificationConfig(params: ListTaskPushNotificationConfigParams): Promise<ListTaskPushNotificationConfigResponse> {
return this._postRpcRequest<ListTaskPushNotificationConfigParams, ListTaskPushNotificationConfigResponse>(
public async listTaskPushNotificationConfig(params: ListTaskPushNotificationConfigParams): Promise<ListTaskPushNotificationConfigSuccessResponse> {
const response = await this._postRpcRequest<ListTaskPushNotificationConfigParams, ListTaskPushNotificationConfigResponse>(
"tasks/pushNotificationConfig/list",
params
);
return parseSuccessResponse(response);
}

/**
* Deletes the push notification configuration for a given task.
* @param params Parameters containing the taskId and push notification configuration ID.
* @returns A Promise resolving to DeleteTaskPushNotificationConfigResponse.
* @returns A Promise resolving to DeleteTaskPushNotificationConfigSuccessResponse. Throws A2AClientError for error responses.
* @throws A2AClientError if the response contains an error.
*/
public async deleteTaskPushNotificationConfig(params: DeleteTaskPushNotificationConfigParams): Promise<DeleteTaskPushNotificationConfigResponse> {
return this._postRpcRequest<DeleteTaskPushNotificationConfigParams, DeleteTaskPushNotificationConfigResponse>(
public async deleteTaskPushNotificationConfig(params: DeleteTaskPushNotificationConfigParams): Promise<DeleteTaskPushNotificationConfigSuccessResponse> {
const response = await this._postRpcRequest<DeleteTaskPushNotificationConfigParams, DeleteTaskPushNotificationConfigResponse>(
"tasks/pushNotificationConfig/delete",
params
);
return parseSuccessResponse(response);
}


/**
* Retrieves a task by its ID.
* @param params Parameters containing the taskId and optional historyLength.
* @returns A Promise resolving to GetTaskResponse, which contains the Task object or an error.
* @returns A Promise resolving to GetTaskSuccessResponse containing the Task object. Throws A2AClientError for error responses.
* @throws A2AClientError if the response contains an error.
*/
public async getTask(params: TaskQueryParams): Promise<GetTaskResponse> {
return this._postRpcRequest<TaskQueryParams, GetTaskResponse>("tasks/get", params);
public async getTask(params: TaskQueryParams): Promise<GetTaskSuccessResponse> {
const response = await this._postRpcRequest<TaskQueryParams, GetTaskResponse>("tasks/get", params);
return parseSuccessResponse(response);
}

/**
* Cancels a task by its ID.
* @param params Parameters containing the taskId.
* @returns A Promise resolving to CancelTaskResponse, which contains the updated Task object or an error.
* @returns A Promise resolving to CancelTaskSuccessResponse containing the updated Task object. Throws A2AClientError for error responses.
* @throws A2AClientError if the response contains an error.
*/
public async cancelTask(params: TaskIdParams): Promise<CancelTaskResponse> {
return this._postRpcRequest<TaskIdParams, CancelTaskResponse>("tasks/cancel", params);
public async cancelTask(params: TaskIdParams): Promise<CancelTaskSuccessResponse> {
const response = await this._postRpcRequest<TaskIdParams, CancelTaskResponse>("tasks/cancel", params);
return parseSuccessResponse(response);
}

/**
Expand Down Expand Up @@ -507,32 +530,21 @@ export class A2AClient {
// Depending on strictness, this could be an error. For now, it's a warning.
}

if (this.isErrorResponse(a2aStreamResponse)) {
const err = a2aStreamResponse.error as (JSONRPCError | A2AError);
throw new Error(`SSE event contained an error: ${err.message} (Code: ${err.code}) Data: ${JSON.stringify(err.data || {})}`);
}

// Check if 'result' exists, as it's mandatory for successful JSON-RPC responses
if (!('result' in a2aStreamResponse) || typeof (a2aStreamResponse as SendStreamingMessageSuccessResponse).result === 'undefined') {
throw new Error(`SSE event JSON-RPC response is missing 'result' field. Data: ${jsonData}`);
}

const successResponse = a2aStreamResponse as SendStreamingMessageSuccessResponse;
// Use the type-safe parseSuccessResponse to handle error responses and extract result
const successResponse = parseSuccessResponse(a2aStreamResponse);
return successResponse.result as TStreamItem;
} catch (e: any) {
// Catch errors from JSON.parse or if it's an error response that was thrown by this function
if (e.message.startsWith("SSE event contained an error") || e.message.startsWith("SSE event JSON-RPC response is missing 'result' field")) {
throw e; // Re-throw errors already processed/identified by this function
// Re-throw A2AClientError (from parseSuccessResponse) without modification
if (e instanceof A2AClientError) {
throw e;
}
// For other parsing errors or unexpected structures:

// For JSON parsing errors or other unexpected structures:
console.error("Failed to parse SSE event data string or unexpected JSON-RPC structure:", jsonData, e);
throw new Error(`Failed to parse SSE event data: "${jsonData.substring(0, 100)}...". Original error: ${e.message}`);
}
}

isErrorResponse(response: JSONRPCResponse): response is JSONRPCErrorResponse {
return "error" in response;
}

////////////////////////////////////////////////////////////////////////////////
// Functions used to support old A2AClient Constructor to be deprecated soon
Expand Down
2 changes: 2 additions & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
export { A2AClient } from "./client.js";
export type { A2AClientOptions } from "./client.js";
export * from "./auth-handler.js";
export * from "./response-utils.js";
export * from "./type-guards.js";
Loading