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

271 lines (249 loc) 8.51 kB
/** * @module MessageClient * @description Client for sending and receiving messages between A2A agents */ import { EventEmitter } from 'events'; import { MessagePart, JsonRpcRequest, TraceClass, } from '@dexwox-labs/a2a-core'; import { validateMessageParts } from '@dexwox-labs/a2a-core'; import { MessageClientOptions, StreamOptions } from './types'; import { normalizeError, A2ANetworkError, } from './utils/error-handler'; import { sendRequest } from './utils/http-utils'; import { TaskClient } from './task-client'; import { AgentClient } from './agent-client'; /** * Client for sending and receiving messages between A2A agents * * The MessageClient provides methods for sending messages to agents and * streaming real-time communication. It also provides access to related * TaskClient and AgentClient instances for convenience. * * @example * ```typescript * const messageClient = new MessageClient({ baseUrl: 'https://a2a-server.example.com' }); * * // Send a simple text message to an agent * const messageId = await messageClient.sendMessage([ * { type: 'text', content: 'What is the weather in New York?' } * ], 'weather-agent'); * ``` */ @TraceClass() export class MessageClient extends EventEmitter { /** Configuration options for the client */ private readonly options: MessageClientOptions; /** Task client for managing tasks */ public readonly tasks: TaskClient; /** Agent client for discovering and interacting with agents */ public readonly agents: AgentClient; /** * Creates a new MessageClient instance * @param options - Configuration options for the client */ constructor(options: MessageClientOptions) { super(); this.options = { timeout: 5000, ...options }; this.tasks = new TaskClient(this.options); this.agents = new AgentClient(this.options); } /** * Sends a message synchronously to an agent * * This method sends a message to a specified agent and waits for the server to * acknowledge receipt. It validates the message parts before sending and handles * network errors appropriately. * * @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 are invalid * * @example * ```typescript * // Send a text message * const textMessageId = await messageClient.sendMessage([ * { type: 'text', content: 'Hello, agent!' } * ], 'assistant-agent'); * * // Send a message with multiple parts * const multipartMessageId = await messageClient.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: MessagePart[], agentId: string): Promise<string> { validateMessageParts(parts); const request: JsonRpcRequest = { jsonrpc: '2.0', method: 'sendMessage', params: { parts, agentId } }; try { const response = await sendRequest(this.options, request); return response.result as string; } catch (err) { if (err instanceof Error && err.message.includes('Network')) { throw new A2ANetworkError('Failed to send message', { originalError: err, agentId }); } throw normalizeError(err); } } /** * Streams messages to and from an agent * * This method establishes a real-time streaming connection with an agent, * allowing for continuous message exchange with automatic error handling. * * @param parts - Initial message parts to send to the agent * @param agentId - ID of the target agent to stream with * @param options - Configuration options and event handlers for the stream * @param options.onMessage - Callback function for received messages * @param options.onError - Optional callback function for stream errors * @param options.onComplete - Optional callback function when stream completes * @returns Promise that resolves when the stream completes * @throws {A2ANetworkError} If there's a network issue establishing the stream * @throws {A2AValidationError} If the message parts are invalid * * @example * ```typescript * await messageClient.streamMessage( * [{ type: 'text', content: 'Tell me a story about dragons' }], * 'storyteller-agent', * { * onMessage: (data) => console.log('Received:', data), * onError: (error) => console.error('Stream error:', error), * onComplete: () => console.log('Stream completed') * } * ); * ``` */ async streamMessage( parts: MessagePart[], agentId: string, options: StreamOptions & { maxRetries?: number; retryDelay?: number; backoffFactor?: number; maxRetryDelay?: number; heartbeatInterval?: number; heartbeatTimeout?: number; } ): Promise<void> { const { maxRetries = 5, retryDelay = 1000, backoffFactor = 2, maxRetryDelay = 30000, heartbeatInterval = 10000, heartbeatTimeout = 30000, onMessage, onError, onComplete } = options; let retryCount = 0; let isStreaming = true; let lastHeartbeat = Date.now(); const calculateDelay = (attempt: number): number => { const delay = retryDelay * Math.pow(backoffFactor, attempt); return Math.min(delay, maxRetryDelay); }; const handleEvent = (event: MessageEvent) => { try { const data = JSON.parse(event.data); if (data.type === 'heartbeat') { lastHeartbeat = Date.now(); } else { if (data.type === 'taskUpdate' && this.tasks) { this.tasks.handleTaskUpdate(data.task); } onMessage(data); } } catch (err) { onError?.(new Error('Failed to parse SSE event')); } }; const setupEventSource = (): EventSource => { const streamUrl = `${this.options.baseUrl}/stream?agentId=${agentId}`; const eventSource = new EventSource(streamUrl); eventSource.onmessage = handleEvent; eventSource.onerror = (error) => { eventSource.close(); isStreaming = false; if (retryCount < maxRetries) { const delay = calculateDelay(retryCount); retryCount++; setTimeout(doStream, delay); } else { onError?.(new Error('Stream connection failed after retries')); } }; eventSource.addEventListener('close', () => { isStreaming = false; onComplete?.(); }); return eventSource; }; const doStream = async (): Promise<void> => { const request: JsonRpcRequest = { jsonrpc: '2.0', method: 'streamMessage', params: { parts, agentId } }; try { const response = await fetch(this.options.baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.options.headers }, body: JSON.stringify(request) }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.body) throw new Error('No response body for streaming'); const eventSource = setupEventSource(); // Heartbeat monitoring const heartbeatMonitor = setInterval(() => { if (isStreaming && Date.now() - lastHeartbeat > heartbeatTimeout) { eventSource.close(); onError?.(new Error('Heartbeat timeout exceeded')); } }, heartbeatInterval); // Cleanup on completion return new Promise<void>((resolve) => { eventSource.addEventListener('close', () => { clearInterval(heartbeatMonitor); resolve(); }); }); } catch (err) { if (err instanceof Error && err.message.includes('Network')) { throw new A2ANetworkError('Streaming connection failed', { originalError: err, agentId, retryCount }); } throw normalizeError(err); } }; return doStream(); } }