UNPKG

@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
"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