@dexwox-labs/a2a-server
Version:
TypeScript server implementation for Google's Agent-to-Agent (A2A) protocol - includes Express/WebSocket handlers, request validation and queue management
426 lines • 18 kB
JavaScript
"use strict";
/**
* @module RequestHandler
* @description Core request handling and routing for the A2A server
*
* This module provides the main request handler implementation for the A2A server,
* handling all incoming requests, routing them to the appropriate handlers, and
* managing the lifecycle of tasks and messages.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultRequestHandler = void 0;
const express_1 = require("express");
const default_jsonrpc_handler_1 = require("./request-handlers/default-jsonrpc-handler");
const crypto_1 = require("crypto");
const agent_executor_1 = require("./agent-execution/agent-executor");
const task_manager_1 = require("./tasks/task-manager");
const in_memory_task_store_1 = require("./tasks/in-memory-task-store");
const push_service_1 = require("./push-notifications/push-service");
const event_queue_1 = require("./agent-execution/event-queue");
const task_event_manager_1 = require("./agent-execution/task-event-manager");
const in_memory_queue_manager_1 = require("./queue-system/in-memory-queue-manager");
const a2a_core_1 = require("@dexwox-labs/a2a-core");
const request_context_1 = require("./agent-execution/request-context");
/**
* Default implementation of the RequestHandler interface
*
* This class provides the standard implementation of the RequestHandler interface,
* handling all A2A protocol requests including message sending, task management,
* push notifications, and agent discovery.
*
* @example
* ```typescript
* // Create a request handler with available agents
* const agents: AgentCard[] = [
* {
* id: 'assistant-agent',
* name: 'Assistant',
* description: 'A helpful assistant',
* capabilities: ['chat', 'answer-questions']
* }
* ];
*
* const requestHandler = new DefaultRequestHandler(agents);
*
* // Use in an Express app
* app.use('/a2a', requestHandler.router);
* ```
*/
class DefaultRequestHandler extends default_jsonrpc_handler_1.DefaultJsonRpcRequestHandler {
/**
* Creates a new DefaultRequestHandler
*
* @param agents - Array of available agent cards
*/
constructor(agents = []) {
super();
/** Manager for request queues */
this.queueManager = new in_memory_queue_manager_1.InMemoryQueueManager();
/** Manager for tasks */
this.taskManager = new task_manager_1.TaskManager(new in_memory_task_store_1.InMemoryTaskStore());
/** Service for push notifications */
this.pushService = new push_service_1.PushNotificationService();
this.router = (0, express_1.Router)();
this.eventQueue = new event_queue_1.EventQueue();
this.taskEventManager = new task_event_manager_1.TaskEventManager(this.eventQueue);
this.agentExecutor = new agent_executor_1.DefaultAgentExecutor(new task_manager_1.TaskManager(new in_memory_task_store_1.InMemoryTaskStore()), this.taskEventManager);
this.agents = agents;
this.setupRoutes();
}
/**
* Sets up the Express routes for handling A2A protocol requests
*
* @private
*/
setupRoutes() {
this.router.post('/sendMessage', this.handleJsonRpcSendMessage.bind(this));
this.router.post('/streamMessage', this.handleJsonRpcStreamMessage.bind(this));
this.router.get('/tasks/:taskId', this.handleJsonRpcGetTaskStatus.bind(this));
this.router.post('/tasks/:taskId/cancel', this.handleJsonRpcCancelTask.bind(this));
this.router.get('/agents', this.handleJsonRpcDiscoverAgents.bind(this));
}
handleSendMessage(parts, agentId) {
return __awaiter(this, void 0, void 0, function* () {
const task = yield this.taskManager.createTask({
name: 'MessageTask',
agentId,
parts: parts || [],
expectedParts: parts.length,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
try {
yield this.agentExecutor.execute((0, request_context_1.createRequestContext)(task, agentId), this.eventQueue);
}
catch (error) {
yield this.taskManager.updateTaskStatus(task.id, 'failed');
this.taskEventManager.taskFailed(task, this.normalizeError(error));
throw error;
}
this.taskEventManager.taskCreated(task);
for (const part of parts) {
if (part.type === 'file') {
this.taskEventManager.artifactAdded(task, {
id: (0, crypto_1.randomUUID)(),
type: 'file',
content: { data: part.content },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
}
else if (part.type === 'data') {
this.taskEventManager.artifactAdded(task, {
id: (0, crypto_1.randomUUID)(),
type: 'data',
content: part.content,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
}
}
return task.id;
});
}
createArtifactEvent(task, part) {
if (part.type === 'file') {
return {
id: (0, crypto_1.randomUUID)(),
type: part.type,
content: { data: part.content },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {
name: part.name,
size: part.size,
mimeType: part.mimeType
}
};
}
else if (part.type === 'data') {
return {
id: (0, crypto_1.randomUUID)(),
type: part.type,
content: part.content,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {
schema: ('schema' in part) ? part.schema : 'json'
}
};
}
throw new Error(`Cannot create artifact from part type: ${part.type}`);
}
createHeartbeat() {
return {
type: 'heartbeat',
content: new Date().toISOString(),
format: 'plain'
};
}
handleStreamMessage(parts, agentId) {
return __asyncGenerator(this, arguments, function* handleStreamMessage_1() {
const task = yield __await(this.taskManager.createTask({
name: 'StreamTask',
agentId,
parts,
expectedParts: parts.length,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}));
const aggregator = this.taskManager.getAggregator(task.id);
this.taskEventManager.taskCreated(task);
for (const part of parts) {
yield yield __await(part);
if (aggregator) {
aggregator.addPart(part);
if (part.type === 'file' || part.type === 'data') {
try {
this.taskEventManager.artifactAdded(task, this.createArtifactEvent(task, part));
}
catch (err) {
console.error('Failed to create artifact:', err);
}
}
}
// Send heartbeat every 15 seconds
if (Math.random() < 0.066) { // ~1/15 chance per second
const heartbeat = this.createHeartbeat();
yield yield __await(heartbeat);
if (aggregator) {
aggregator.addPart(heartbeat);
}
}
yield __await(new Promise(resolve => setTimeout(resolve, 1000)));
}
aggregator === null || aggregator === void 0 ? void 0 : aggregator.complete();
});
}
/**
* Gets the status of a task
*
* Retrieves the current status and details of a task by its ID.
*
* @param taskId - ID of the task to retrieve
* @returns Promise resolving to the task object
* @throws {A2AError} If the task is not found
*
* @example
* ```typescript
* try {
* const task = await requestHandler.handleGetTaskStatus('task-123');
* console.log('Task status:', task.status);
* console.log('Task result:', task.result);
* } catch (error) {
* console.error('Failed to get task:', error);
* }
* ```
*/
handleGetTaskStatus(taskId) {
return __awaiter(this, void 0, void 0, function* () {
const task = yield this.taskManager.getTask(taskId);
if (!task) {
throw this.normalizeError({ code: -32004, message: 'Task not found' });
}
return task;
});
}
/**
* Cancels a running task
*
* Attempts to cancel a task that is currently in progress. This will notify
* the agent to stop processing and update the task status to 'canceled'.
*
* @param taskId - ID of the task to cancel
* @returns Promise resolving when the task is canceled
* @throws {A2AError} If the task is not found or has no agent ID
*
* @example
* ```typescript
* try {
* await requestHandler.handleCancelTask('task-123');
* console.log('Task canceled successfully');
* } catch (error) {
* console.error('Failed to cancel task:', error);
* }
* ```
*/
handleCancelTask(taskId) {
return __awaiter(this, void 0, void 0, function* () {
const task = yield this.taskManager.getTask(taskId);
if (!task.agentId) {
throw this.normalizeError({ code: -32000, message: 'Task has no agentId' });
}
yield this.agentExecutor.cancel((0, request_context_1.createRequestContext)(task, task.agentId), this.eventQueue);
yield this.taskManager.cancelTask(taskId);
this.taskEventManager.taskUpdated(task);
});
}
/**
* Discovers available agents
*
* Returns a list of available agents, optionally filtered by capability.
*
* @param capability - Optional capability to filter agents by
* @returns Promise resolving to an array of agent cards
*
* @example
* ```typescript
* // Get all agents
* const allAgents = await requestHandler.handleDiscoverAgents();
* console.log('All agents:', allAgents);
*
* // Get agents with a specific capability
* const chatAgents = await requestHandler.handleDiscoverAgents('chat');
* console.log('Chat agents:', chatAgents);
* ```
*/
handleDiscoverAgents(capability) {
return __awaiter(this, void 0, void 0, function* () {
return capability
? this.agents.filter(agent => agent.capabilities.includes(capability))
: this.agents;
});
}
handleTaskResubscription(taskId) {
return __asyncGenerator(this, arguments, function* handleTaskResubscription_1() {
var _a;
const task = yield __await(this.taskManager.getTask(taskId));
if (!task) {
throw this.normalizeError({ code: -32004, message: 'Task not found' });
}
// Yield existing task parts first
const parts = (_a = task.parts) !== null && _a !== void 0 ? _a : [];
for (const part of parts) {
yield yield __await(part);
}
// Track last activity time
let lastActivity = Date.now();
// Then continue streaming new updates (mock implementation)
while (task.status === 'working') {
yield __await(new Promise(resolve => setTimeout(resolve, 1000)));
const update = {
type: 'text',
content: `Task ${taskId} update at ${new Date().toISOString()}`,
format: 'plain'
};
yield yield __await(update);
lastActivity = Date.now();
// Check if we need to send a heartbeat
if (Date.now() - lastActivity > 15000) {
const heartbeat = {
type: 'heartbeat',
content: new Date().toISOString(),
format: 'plain'
};
yield yield __await(heartbeat);
lastActivity = Date.now();
}
}
});
}
/**
* Sets push notification configuration for a task
*
* Configures push notifications for a specific task, including the endpoint
* to send notifications to and which events to notify about.
*
* @param taskId - ID of the task
* @param config - Push notification configuration
* @returns Promise resolving when the configuration is set
* @throws {A2AError} If the task is not found
*
* @example
* ```typescript
* await requestHandler.handleSetPushConfig('task-123', {
* enabled: true,
* endpoint: 'https://webhook.example.com/notifications',
* authToken: 'your-auth-token',
* events: ['taskCompleted', 'taskFailed']
* });
* ```
*/
handleSetPushConfig(taskId, config) {
return __awaiter(this, void 0, void 0, function* () {
yield this.taskManager.getTask(taskId); // Verify task exists
yield this.pushService.setConfig(taskId, config);
});
}
/**
* Gets push notification configuration for a task
*
* Retrieves the current push notification configuration for a specific task.
*
* @param taskId - ID of the task
* @returns Promise resolving to the push notification configuration
* @throws {A2AError} If the task is not found
*
* @example
* ```typescript
* const config = await requestHandler.handleGetPushConfig('task-123');
* console.log('Push notification config:', config);
* console.log('Enabled:', config.enabled);
* console.log('Events:', config.events);
* ```
*/
handleGetPushConfig(taskId) {
return __awaiter(this, void 0, void 0, function* () {
yield this.taskManager.getTask(taskId); // Verify task exists
return this.pushService.getConfig(taskId);
});
}
/**
* Normalizes errors to A2AError format
*
* Converts various error types to the standardized A2AError format used
* throughout the A2A protocol.
*
* @param err - Error to normalize
* @returns Normalized A2AError
*
* @example
* ```typescript
* try {
* // Some operation that might fail
* throw new Error('Something went wrong');
* } catch (error) {
* // Normalize the error to A2AError format
* const normalizedError = requestHandler.normalizeError(error);
* console.error('Normalized error:', normalizedError);
* console.error('Error code:', normalizedError.code);
* }
* ```
*/
normalizeError(err) {
if (err instanceof a2a_core_1.A2AError) {
return 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 });
}
}
exports.DefaultRequestHandler = DefaultRequestHandler;
//# sourceMappingURL=request-handler.js.map