@dexwox-labs/a2a-client
Version:
TypeScript client implementation for Google's Agent-to-Agent (A2A) protocol - includes HTTP/WebSocket communication, circuit breakers and error handling
626 lines • 25.2 kB
JavaScript
"use strict";
/**
* @module HttpClient
* @description Low-level HTTP client for communicating with A2A protocol servers
*/
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.A2AHttpClient = void 0;
const a2a_core_1 = require("@dexwox-labs/a2a-core");
const circuit_breaker_1 = require("./utils/circuit-breaker");
const agent_card_resolver_1 = require("./agent-card-resolver");
/**
* Low-level HTTP client for communicating with A2A protocol servers
*
* This class provides the core HTTP communication layer for the A2A SDK,
* handling JSON-RPC requests, streaming, and agent card resolution.
*
* @example
* ```typescript
* const httpClient = new A2AHttpClient({
* baseUrl: 'https://a2a-server.example.com'
* });
*
* // Discover available agents
* const agents = await httpClient.discover();
* console.log('Available agents:', agents);
*
* // Send a message to an agent
* const messageId = await httpClient.sendMessage([
* { type: 'text', content: 'Hello, agent!' }
* ], 'assistant-agent');
* ```
*/
let A2AHttpClient = (() => {
let _classDecorators = [(0, a2a_core_1.TraceClass)('A2AHttpClient')];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
var A2AHttpClient = class {
static { _classThis = this; }
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
A2AHttpClient = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
/** @private Configuration options for the client */
options;
/** @private Circuit breaker for handling failures */
circuitBreaker;
/** @private Resolver for agent card information */
agentCardResolver;
/** @private Default circuit breaker configuration */
static DEFAULT_CIRCUIT_BREAKER_OPTIONS = {
failureThreshold: 3,
successThreshold: 2,
timeout: 10000
};
/**
* Creates a new A2A HTTP client instance
*
* Initializes the HTTP client with the provided options, setting up the circuit breaker
* and agent card resolver. Default values are applied for any missing options.
*
* @param options - Configuration options for the client
*
* @example
* ```typescript
* // Basic configuration
* const client = new A2AHttpClient({
* baseUrl: 'https://a2a-server.example.com'
* });
*
* // Advanced configuration
* const advancedClient = new A2AHttpClient({
* baseUrl: 'https://a2a-server.example.com',
* timeout: 10000,
* headers: {
* 'Authorization': 'Bearer token123',
* 'X-Custom-Header': 'custom-value'
* },
* agentCard: {
* path: '/custom-agent-path.json',
* cacheTtl: 600000 // 10 minutes
* }
* });
* ```
*/
constructor(options) {
this.options = {
timeout: 5000,
...options
};
this.circuitBreaker = new circuit_breaker_1.CircuitBreaker(A2AHttpClient.DEFAULT_CIRCUIT_BREAKER_OPTIONS);
this.agentCardResolver = new agent_card_resolver_1.AgentCardResolver(this.options.baseUrl, {
agentCardPath: this.options.agentCard?.path,
cacheTtl: this.options.agentCard?.cacheTtl,
timeout: this.options.timeout
});
}
/**
* Gets the resolved agent card for the connected server
*
* Retrieves the agent card from the server, which contains metadata about
* the agent's capabilities, name, and other information. Results are cached
* according to the configured TTL.
*
* @returns Promise resolving to the agent card
* @throws {A2ANetworkError} If there's a network issue contacting the server
*
* @example
* ```typescript
* const agentCard = await httpClient.getAgentCard();
* console.log(`Connected to agent: ${agentCard.name}`);
* console.log(`Agent capabilities: ${agentCard.capabilities.join(', ')}`);
* ```
*/
async getAgentCard() {
return this.agentCardResolver.resolve();
}
/**
* Refreshes the agent card cache
*
* Forces a refresh of the cached agent card information, bypassing the TTL.
* This is useful when you know the agent's capabilities may have changed.
*
* @returns Promise resolving to the refreshed agent card
* @throws {A2ANetworkError} If there's a network issue contacting the server
*
* @example
* ```typescript
* // Force a refresh of the agent card
* const refreshedCard = await httpClient.refreshAgentCard();
* console.log('Updated capabilities:', refreshedCard.capabilities);
* ```
*/
async refreshAgentCard() {
return this.agentCardResolver.refresh();
}
/**
* Discovers available agents matching an optional capability filter
*
* This method queries the A2A network for available agents. If a capability
* is specified, only agents that support that capability will be returned.
*
* @param capability - Optional capability filter (e.g., 'text-generation', 'image-generation')
* @returns Promise resolving to an array of matching agent cards
* @throws {A2ANetworkError} If there's a network issue contacting the server
* @throws {A2AValidationError} If the capability filter is invalid
*
* @example
* ```typescript
* // Get all available agents
* const allAgents = await httpClient.discover();
* console.log(`Found ${allAgents.length} agents`);
*
* // Get only agents with text generation capability
* const textAgents = await httpClient.discover('text-generation');
* console.log(`Found ${textAgents.length} text generation agents`);
* ```
*/
async discover(capability) {
const request = {
jsonrpc: '2.0',
method: 'discover',
params: capability ? { capability } : {}
};
const response = await this.sendRequest(request);
return response.result.agents;
}
/**
* Gets task details by ID
*
* Retrieves the current state and details of a task by its ID. This method
* fetches the complete task object including status, input, output, and any
* error information.
*
* @param taskId - The ID of the task to retrieve
* @returns Promise resolving to the complete task object
* @throws {A2ANetworkError} If there's a network issue contacting the server
* @throws {A2AValidationError} If the task ID is invalid or not found
*
* @example
* ```typescript
* try {
* const task = await httpClient.getTask('task-123');
* console.log(`Task status: ${task.status}`);
*
* if (task.status === 'completed') {
* console.log('Task output:', task.output);
* } else if (task.status === 'failed') {
* console.error('Task failed:', task.error);
* }
* } catch (error) {
* console.error('Error retrieving task:', error.message);
* }
* ```
*/
async getTask(taskId) {
const request = {
jsonrpc: '2.0',
method: 'getTask',
params: { taskId }
};
const response = await this.sendRequest(request);
return response.result;
}
/**
* Sends a task for execution
*
* Submits a task to the A2A server for execution. The task can contain
* input data, target agent information, and other parameters needed for
* execution.
*
* @param task - The task object to execute
* @returns Promise resolving to the updated task with initial status
* @throws {A2ANetworkError} If there's a network issue contacting the server
* @throws {A2AValidationError} If the task is invalid
*
* @example
* ```typescript
* const task = {
* id: 'task-' + Date.now(),
* name: 'Weather Analysis',
* agentId: 'weather-agent',
* status: 'submitted',
* input: { location: 'New York', days: 5 },
* createdAt: new Date().toISOString(),
* updatedAt: new Date().toISOString()
* };
*
* const submittedTask = await httpClient.sendTask(task);
* console.log(`Task submitted with ID: ${submittedTask.id}`);
* console.log(`Initial status: ${submittedTask.status}`);
* ```
*/
async sendTask(task) {
const request = {
jsonrpc: '2.0',
method: 'executeTask',
params: { task }
};
const response = await this.sendRequest(request);
return response.result;
}
/**
* Cancels a running task
* @param taskId Task ID to cancel
* @returns Promise resolving to updated Task
* @throws A2AError if cancellation fails
*/
async cancelTask(taskId) {
const request = {
jsonrpc: '2.0',
method: 'cancelTask',
params: { taskId }
};
const response = await this.sendRequest(request);
return response.result;
}
/**
* Gets push notification configuration for a task
* @param taskId Task ID to get config for
* @returns Promise resolving to PushNotificationConfig
* @throws A2AError if config retrieval fails
*/
async getPushNotificationConfig(taskId) {
const params = { taskId };
// Add auth token if configured
if (this.options.pushAuth?.token) {
params.authToken = this.options.pushAuth.token;
}
const request = {
jsonrpc: '2.0',
method: 'getPushNotificationConfig',
params
};
const response = await this.sendRequest(request);
return response.result;
}
/**
* Sets push notification configuration for a task
* @param taskId Task ID to configure
* @param config Push notification configuration
* @returns Promise resolving to updated PushNotificationConfig
* @throws A2AError if configuration fails
*/
async setPushNotificationConfig(taskId, config) {
const params = { taskId, config };
// Add auth token if configured
if (this.options.pushAuth?.token) {
params.authToken = this.options.pushAuth.token;
}
const request = {
jsonrpc: '2.0',
method: 'setPushNotificationConfig',
params
};
const response = await this.sendRequest(request);
return response.result;
}
/**
* Refreshes the push notification auth token
* @returns Promise resolving to new token
* @throws A2AError if refresh fails
*/
async refreshPushAuthToken() {
if (!this.options.pushAuth?.refresh) {
throw {
code: -32003,
message: 'Push notification auth refresh not configured'
};
}
try {
const newToken = await this.options.pushAuth.refresh();
this.options.pushAuth = {
...this.options.pushAuth,
token: newToken
};
return newToken;
}
catch (err) {
throw this.normalizeError(err);
}
}
async createStreamRequest(task) {
const params = { task };
// Add push auth token if configured
if (this.options.pushAuth?.token) {
params.authToken = this.options.pushAuth.token;
}
const request = {
jsonrpc: '2.0',
method: 'executeTask',
params
};
const headers = {
'Content-Type': 'application/json',
...this.options.headers
};
const response = await fetch(this.options.baseUrl, {
method: 'POST',
headers,
body: JSON.stringify(request),
signal: AbortSignal.timeout(this.options.timeout)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.body) {
throw new Error('No response body for streaming');
}
return response;
}
async processStream(reader, onEvent) {
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done)
break;
buffer += decoder.decode(value, { stream: true });
buffer = this.processBuffer(buffer, onEvent);
}
}
processBuffer(buffer, onEvent) {
const lines = buffer.split('\n');
const remainingBuffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
this.processEventLine(line.substring(6).trim(), onEvent);
}
}
return remainingBuffer;
}
processEventLine(data, onEvent) {
if (!data)
return;
try {
const event = JSON.parse(data);
onEvent(event);
}
catch (err) {
console.error('Error parsing SSE event:', err);
}
}
/**
* Streams task execution events via Server-Sent Events (SSE)
* @param task Task to execute
* @param onEvent Callback for processing stream events
* @returns Promise that resolves when stream completes
* @throws A2AError if streaming fails to start
*/
async streamTask(task, onEvent, options) {
try {
const params = { task };
// Add push auth token if configured
if (this.options.pushAuth?.token) {
params.authToken = this.options.pushAuth.token;
}
// Add resubscription info if provided
if (options?.lastEventId) {
params.lastEventId = options.lastEventId;
}
const response = await fetch(this.options.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.options.headers,
...(options?.lastEventId && {
'Last-Event-ID': options.lastEventId
})
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'executeTask',
params
}),
signal: AbortSignal.timeout(this.options.timeout)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.body) {
throw new Error('No response body for streaming');
}
const reader = response.body.getReader();
await this.processStream(reader, onEvent);
}
catch (error) {
if (options?.onResubscribe && this.isRecoverableError(error)) {
const newTask = await this.resubscribeTask(task);
options.onResubscribe(newTask);
return this.streamTask(newTask, onEvent, options);
}
throw error;
}
}
isRecoverableError(error) {
return error instanceof Error &&
(error.message.includes('connection') ||
error.message.includes('timeout'));
}
async resubscribeTask(task) {
const request = {
jsonrpc: '2.0',
method: 'resubscribeTask',
params: { taskId: task.id }
};
const response = await this.sendRequest(request);
return response.result;
}
/**
* Sends a message to an agent
* @param parts Array of message parts (text/file/data)
* @param agentId Target agent ID
* @returns Promise resolving to message ID
* @throws A2AError if message fails to send
*/
/**
* Uploads an artifact to the server
* @param artifact Artifact data to upload
* @returns Promise resolving to artifact ID
* @throws A2AError if upload fails
*/
async uploadArtifact(artifact) {
try {
(0, a2a_core_1.validateArtifact)(artifact);
}
catch (err) {
throw new a2a_core_1.A2AError('Invalid artifact: ' + err.message, err.code || -32000);
}
const request = {
jsonrpc: '2.0',
method: 'uploadArtifact',
params: { artifact }
};
try {
const response = await this.sendRequest(request);
return response.result;
}
catch (err) {
throw this.normalizeError(err);
}
}
/**
* Downloads an artifact from the server
* @param artifactId ID of artifact to download
* @returns Promise resolving to artifact data
* @throws A2AError if download fails
*/
async downloadArtifact(artifactId) {
const request = {
jsonrpc: '2.0',
method: 'downloadArtifact',
params: { artifactId }
};
try {
const response = await this.sendRequest(request);
return response.result;
}
catch (err) {
throw this.normalizeError(err);
}
}
/**
* Sends a message to an agent
*
* This method sends a message composed of one or more message parts to a
* specified agent. Message parts can be text, files, or structured data.
*
* @param parts - Array of message parts to send (text, file, data, etc.)
* @param agentId - ID of the target agent to receive the message
* @returns Promise resolving to the message ID assigned by the server
* @throws {A2ANetworkError} If there's a network issue contacting the server
* @throws {A2AValidationError} If the message parts or agent ID are invalid
*
* @example
* ```typescript
* // Send a simple text message
* const textMessageId = await httpClient.sendMessage([
* { type: 'text', content: 'Hello, agent!' }
* ], 'assistant-agent');
*
* // Send a message with multiple parts
* const multipartMessageId = await httpClient.sendMessage([
* { type: 'text', content: 'Here is the data you requested' },
* {
* type: 'data',
* content: { temperature: 72, humidity: 65 },
* schema: 'weather-data'
* }
* ], 'weather-agent');
* ```
*/
async sendMessage(parts, agentId) {
const request = {
jsonrpc: '2.0',
method: 'sendMessage',
params: { parts, agentId }
};
try {
const response = await this.sendRequest(request);
return response.result;
}
catch (err) {
throw this.normalizeError(err);
}
}
normalizeError(err) {
if (err instanceof Error) {
return new a2a_core_1.A2AError(err.message, -32000, { stack: err.stack });
}
return new a2a_core_1.A2AError('Unknown error occurred', -32000, { originalError: err });
}
/**
* Sends a JSON-RPC request to the A2A server
*
* This is the core method that handles all communication with the A2A server.
* It uses the circuit breaker pattern to prevent cascading failures and
* implements timeout handling.
*
* @param request - JSON-RPC request object to send
* @returns Promise resolving to the JSON-RPC response
* @throws Error if the HTTP request fails or times out
* @template T - Expected response result type
* @internal
*/
async sendRequest(request) {
return this.circuitBreaker.execute(async () => {
const response = await fetch(this.options.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.options.headers
},
body: JSON.stringify(request),
signal: AbortSignal.timeout(this.options.timeout)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
}
static {
__runInitializers(_classThis, _classExtraInitializers);
}
};
return A2AHttpClient = _classThis;
})();
exports.A2AHttpClient = A2AHttpClient;
//# sourceMappingURL=http-client.js.map