/**
* A2A (Agent-to-Agent) JSON-RPC 2.0 Handler
* Processes incoming A2A requests on the /a2a endpoint,
* routing them to the TaskManager for skill execution.
*/
import type {
JsonRpcRequest,
JsonRpcResponse,
JsonRpcError,
Message,
SendMessageParams,
GetTaskParams,
CancelTaskParams,
} from './types.js';
import { A2A_ERRORS } from './types.js';
import { TaskManager } from './task-manager.js';
import { generateAgentCard } from './agent-card.js';
import { loadConfig } from '../types.js';
import { logger } from '../utils/logger.js';
// ─── Interfaces ───
interface APIGatewayEvent {
httpMethod: string;
path: string;
headers: Record<string, string | undefined>;
body: string | null;
isBase64Encoded: boolean;
}
interface A2AResponse {
statusCode: number;
headers: Record<string, string>;
body: string;
}
// ─── CORS ───
const CORS_HEADERS: Record<string, string> = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
// ─── Lazy Singleton TaskManager ───
let taskManager: TaskManager | undefined;
function getTaskManager(): TaskManager {
if (!taskManager) taskManager = new TaskManager();
return taskManager;
}
// ─── JSON-RPC Response Helpers ───
function jsonRpcResponse(id: string | number | null, result: unknown): JsonRpcResponse {
return {
jsonrpc: '2.0',
id,
result,
};
}
function jsonRpcErrorResponse(id: string | number | null, error: JsonRpcError): JsonRpcResponse {
return {
jsonrpc: '2.0',
id,
error,
};
}
function a2aJsonResponse(statusCode: number, rpcResponse: JsonRpcResponse): A2AResponse {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
...CORS_HEADERS,
},
body: JSON.stringify(rpcResponse),
};
}
// ─── Main Handler ───
export async function handleA2ARequest(event: APIGatewayEvent): Promise<A2AResponse> {
// 1. Parse the JSON body
let parsed: unknown;
try {
const rawBody =
event.isBase64Encoded && event.body
? Buffer.from(event.body, 'base64').toString('utf-8')
: event.body;
if (!rawBody) {
return a2aJsonResponse(
200,
jsonRpcErrorResponse(null, A2A_ERRORS.PARSE_ERROR),
);
}
parsed = JSON.parse(rawBody);
} catch {
return a2aJsonResponse(
200,
jsonRpcErrorResponse(null, A2A_ERRORS.PARSE_ERROR),
);
}
// 2. Validate JSON-RPC 2.0 structure
const req = parsed as Record<string, unknown>;
if (
req.jsonrpc !== '2.0' ||
typeof req.method !== 'string' ||
req.id === undefined ||
req.id === null
) {
return a2aJsonResponse(
200,
jsonRpcErrorResponse(
(req.id as string | number | null) ?? null,
A2A_ERRORS.INVALID_REQUEST,
),
);
}
const rpcRequest: JsonRpcRequest = {
jsonrpc: '2.0',
id: req.id as string | number,
method: req.method as string,
params: req.params as Record<string, unknown> | undefined,
};
logger.info('A2A request received', { method: rpcRequest.method, id: rpcRequest.id });
// 3. Route by method
switch (rpcRequest.method) {
case 'message/send':
case 'sendMessage': {
const params = rpcRequest.params as SendMessageParams | undefined;
if (!params?.message) {
return a2aJsonResponse(
400,
jsonRpcErrorResponse(rpcRequest.id, A2A_ERRORS.INVALID_PARAMS),
);
}
const message: Message = params.message;
try {
const manager = getTaskManager();
const task = await manager.processMessage(message);
logger.info('A2A sendMessage completed', {
taskId: task.id,
state: task.status.state,
});
return a2aJsonResponse(200, jsonRpcResponse(rpcRequest.id, task));
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('A2A sendMessage error', { error: errorMessage });
return a2aJsonResponse(
500,
jsonRpcErrorResponse(rpcRequest.id, {
...A2A_ERRORS.INTERNAL_ERROR,
data: errorMessage,
}),
);
}
}
case 'tasks/get':
case 'getTask': {
const params = rpcRequest.params as GetTaskParams | undefined;
const taskId = params?.id ?? 'unknown';
logger.info('A2A getTask — stateless, returning not found', { taskId });
return a2aJsonResponse(
200,
jsonRpcErrorResponse(rpcRequest.id, {
...A2A_ERRORS.TASK_NOT_FOUND,
data: `Task "${taskId}" not found. This gateway is stateless; tasks complete synchronously via sendMessage.`,
}),
);
}
case 'tasks/cancel':
case 'cancelTask': {
const params = rpcRequest.params as CancelTaskParams | undefined;
const taskId = params?.id ?? 'unknown';
logger.info('A2A cancelTask — not cancelable', { taskId });
return a2aJsonResponse(
200,
jsonRpcErrorResponse(rpcRequest.id, {
...A2A_ERRORS.TASK_NOT_CANCELABLE,
data: `Task "${taskId}" cannot be canceled. Tasks complete immediately on this gateway.`,
}),
);
}
default: {
logger.warn('A2A unknown method', { method: rpcRequest.method });
return a2aJsonResponse(
200,
jsonRpcErrorResponse(rpcRequest.id, A2A_ERRORS.METHOD_NOT_FOUND),
);
}
}
}
// ─── OPTIONS Handler ───
export function handleA2AOptions(): A2AResponse {
return {
statusCode: 204,
headers: {
...CORS_HEADERS,
},
body: '',
};
}
// ─── GET Agent Card Handler ───
export function handleA2AGetAgentCard(): A2AResponse {
const config = loadConfig();
const agentCard = generateAgentCard(config);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
...CORS_HEADERS,
},
body: JSON.stringify(agentCard),
};
}