n8n-nodes-a2a-protocol
Version:
Agent2Agent (A2A) Protocol nodes for n8n - Enable agent interoperability, communication, and MCP integration
857 lines (856 loc) • 45.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.A2ARemoteAgent = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const express_1 = __importDefault(require("express"));
const urlUtils_1 = require("../../utils/urlUtils");
class A2ARemoteAgent {
constructor() {
this.description = {
displayName: 'A2A Remote Agent Server',
name: 'a2aRemoteAgent',
icon: 'file:a2a-server.svg',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["agentId"]}}',
description: 'Start A2A (Agent-to-Agent) Server to receive tasks from clients',
defaults: {
name: 'A2A Agent Server',
},
inputs: [],
outputs: ["main" /* NodeConnectionType.Main */],
credentials: [],
properties: [
{
displayName: 'Agent ID',
name: 'agentId',
type: 'string',
default: 'n8n-a2a-agent',
description: 'Unique identifier for this A2A agent',
required: true,
},
{
displayName: 'Port',
name: 'port',
type: 'number',
default: urlUtils_1.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR,
description: '⚠️ Port to listen on for A2A requests. NOTE: Port conflicts will be detected during workflow activation, not save. Run "node validate-a2a-ports.js" to check for conflicts before saving.',
required: true,
},
{
displayName: 'Processing Mode',
name: 'processingMode',
type: 'options',
options: [
{
name: 'Synchronous',
value: 'sync',
description: 'Process tasks synchronously and wait for workflow results',
},
{
name: 'Asynchronous',
value: 'async',
description: 'Accept tasks immediately and process asynchronously',
},
{
name: 'Mixed',
value: 'mixed',
description: 'Support both sync and async based on client request',
},
],
default: 'mixed',
description: 'How to process incoming A2A tasks',
},
{
displayName: 'Enable Workflow Integration',
name: 'enableWorkflowIntegration',
type: 'boolean',
default: true,
description: 'Forward tasks to connected workflow nodes for processing',
},
{
displayName: 'Authentication Mode',
name: 'authMode',
type: 'options',
options: [
{
name: 'None',
value: 'none',
description: 'No authentication required',
},
{
name: 'Bearer Token',
value: 'bearer_token',
description: 'Require Bearer token authentication',
},
{
name: 'API Key',
value: 'api_key',
description: 'Require API key authentication',
},
],
default: 'none',
description: 'Authentication method for A2A requests',
},
{
displayName: 'Auto-Register with Registry',
name: 'autoRegister',
type: 'boolean',
default: true,
description: 'Automatically register this agent with the A2A Registry',
},
{
displayName: 'Registry URL',
name: 'registryUrl',
type: 'string',
default: (0, urlUtils_1.getDefaultRegistryUrl)(),
displayOptions: {
show: {
autoRegister: [true],
},
},
description: 'URL of the A2A Registry Server',
placeholder: (0, urlUtils_1.getDefaultRegistryUrl)(),
required: true,
},
{
displayName: 'Technical Capabilities',
name: 'technicalCapabilities',
type: 'options',
placeholder: 'Add Technical Capability',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'Streaming Support (SSE)',
value: 'streaming',
description: 'Support Server-Sent Events for real-time updates',
},
{
name: 'Push Notifications',
value: 'pushNotifications',
description: 'Support webhook-based push notifications',
},
{
name: 'State Transition History',
value: 'stateTransitionHistory',
description: 'Provide detailed task state transition history',
},
],
default: ['streaming'],
description: 'Technical protocol features this agent supports (A2A spec compliance)',
},
{
displayName: 'Agent Skills',
name: 'skills',
type: 'json',
default: JSON.stringify([
{
id: 'general_processing',
name: 'General Processing',
description: 'General task processing with N8N workflows',
inputModes: ['text', 'json'],
outputModes: ['json', 'text'],
category: 'processing',
enabled: true,
tags: ['processing', 'workflow'],
examples: 'Process data through N8N workflows'
},
{
id: 'workflow_orchestration',
name: 'Workflow Orchestration',
description: 'N8N workflow execution and management',
inputModes: ['json'],
outputModes: ['json'],
category: 'orchestration',
enabled: true,
tags: ['orchestration', 'automation'],
examples: 'Execute and manage complex N8N workflows'
},
{
id: 'data_transformation',
name: 'Data Transformation',
description: 'Transform and manipulate data formats',
inputModes: ['json', 'xml', 'csv'],
outputModes: ['json', 'xml', 'csv'],
category: 'data',
enabled: false,
tags: ['data', 'transformation'],
examples: 'Convert JSON to CSV, transform data structures'
},
{
id: 'ai_assistance',
name: 'AI Assistance',
description: 'AI-powered text processing and generation',
inputModes: ['text', 'json'],
outputModes: ['text', 'json'],
category: 'ai',
enabled: false,
tags: ['ai', 'nlp', 'generation'],
examples: 'Generate text, analyze content, summarize documents'
},
{
id: 'api_integration',
name: 'API Integration',
description: 'Connect and integrate with external APIs',
inputModes: ['json'],
outputModes: ['json'],
category: 'integration',
enabled: false,
tags: ['api', 'integration', 'external'],
examples: 'Call REST APIs, integrate with third-party services'
},
{
id: 'file_processing',
name: 'File Processing',
description: 'Process and manipulate files',
inputModes: ['file', 'binary'],
outputModes: ['file', 'binary', 'json'],
category: 'files',
enabled: false,
tags: ['files', 'processing', 'binary'],
examples: 'Process images, parse documents, convert file formats'
},
{
id: 'notification_services',
name: 'Notification Services',
description: 'Send notifications via email, SMS, chat',
inputModes: ['json', 'text'],
outputModes: ['json'],
category: 'communication',
enabled: false,
tags: ['notifications', 'email', 'sms'],
examples: 'Send email alerts, SMS notifications, chat messages'
},
{
id: 'database_operations',
name: 'Database Operations',
description: 'Perform database queries and operations',
inputModes: ['json', 'sql'],
outputModes: ['json'],
category: 'database',
enabled: false,
tags: ['database', 'sql', 'queries'],
examples: 'Execute SQL queries, manage database records'
}
], null, 2),
description: 'Configure agent skills/functions (A2A spec compliant). Set enabled: true to activate a skill.',
typeOptions: {
rows: 15,
},
},
],
};
}
async trigger() {
const port = this.getNodeParameter('port');
const agentId = this.getNodeParameter('agentId');
const processingMode = this.getNodeParameter('processingMode');
const enableWorkflowIntegration = this.getNodeParameter('enableWorkflowIntegration');
const authMode = this.getNodeParameter('authMode');
const autoRegister = this.getNodeParameter('autoRegister');
const registryUrl = this.getNodeParameter('registryUrl');
const technicalCapabilities = this.getNodeParameter('technicalCapabilities');
const skillsJson = this.getNodeParameter('skills');
// Generate stable node ID for this configuration (without timestamp)
const nodeId = `agent_${agentId}_${port}`;
// ✅ VALIDATE PORT BEFORE STARTING - This will show popup errors that prevent workflow activation
const portValidation = await (0, urlUtils_1.validateNodePort)(port, 'agent', `A2A Agent (${agentId})`, nodeId);
if (!portValidation.isValid && portValidation.errorMessage) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `❌ Port ${port} is already in use. Please choose a different port for A2A Agent (${agentId}).`);
}
// Stop previous server instance if it exists (safe - only closes Express servers)
await (0, urlUtils_1.stopPreviousServerInstance)(nodeId);
// Parse technical capabilities (A2A protocol features)
const agentCapabilities = {};
technicalCapabilities.forEach((cap) => {
agentCapabilities[cap] = true;
});
// Parse and filter skills - only include enabled ones
let agentSkills = [];
try {
const allSkills = JSON.parse(skillsJson);
agentSkills = allSkills.filter((skill) => skill.enabled === true);
}
catch (error) {
// Fallback to default skills if JSON parsing fails
agentSkills = [
{
id: 'general_processing',
name: 'General Processing',
description: 'General task processing with N8N workflows',
inputModes: ['text', 'json'],
outputModes: ['json', 'text'],
category: 'processing',
tags: ['processing', 'workflow'],
examples: 'Process data through N8N workflows'
},
{
id: 'workflow_orchestration',
name: 'Workflow Orchestration',
description: 'N8N workflow execution and management',
inputModes: ['json'],
outputModes: ['json'],
category: 'orchestration',
tags: ['orchestration', 'automation'],
examples: 'Execute and manage complex N8N workflows'
}
];
}
const taskStore = new Map();
const taskQueue = []; // Queue for tasks to emit to workflow
const app = (0, express_1.default)();
app.use(express_1.default.json());
// Process task queue and emit to workflow (this happens in proper trigger context)
const processTaskQueue = () => {
if (enableWorkflowIntegration && taskQueue.length > 0) {
const tasksToEmit = taskQueue.splice(0); // Get all tasks and clear queue
tasksToEmit.forEach(taskData => {
this.emit([this.helpers.returnJsonArray([taskData])]);
});
}
};
// Check for new tasks every 100ms
const taskProcessor = setInterval(processTaskQueue, 100);
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-A2A-*');
if (req.method === 'OPTIONS')
return res.sendStatus(200);
next();
});
// Authentication middleware
if (authMode !== 'none') {
app.use((req, res, next) => {
if (req.path === '/health' || req.path === '/capabilities')
return next();
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
error: 'Authentication required',
auth_mode: authMode,
a2a_protocol_version: '1.0'
});
}
if (authMode === 'bearer_token' && !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Bearer token required',
a2a_protocol_version: '1.0'
});
}
if (authMode === 'api_key' && !authHeader.startsWith('ApiKey ')) {
return res.status(401).json({
error: 'API key required',
a2a_protocol_version: '1.0'
});
}
next();
});
}
// Get dynamic endpoints
const endpoints = (0, urlUtils_1.createAgentEndpoints)(port);
const urlConfig = (0, urlUtils_1.detectInstanceUrl)();
const callbackUrlBase = (0, urlUtils_1.buildUrl)(urlConfig, port, '/tasks');
// Registry registration function
const registerWithRegistry = async () => {
if (!autoRegister) {
return;
}
try {
const agentCard = {
agent_id: agentId,
name: `N8N A2A Agent ${agentId}`,
description: 'N8N-powered A2A agent with workflow integration',
version: '1.0.0',
a2a_protocol_version: '1.0',
capabilities: agentCapabilities,
skills: agentSkills.map((skill) => ({
id: skill.id,
name: skill.name,
description: skill.description,
inputModes: skill.inputModes,
outputModes: skill.outputModes,
category: skill.category,
tags: skill.tags,
examples: skill.examples
})),
supported_protocols: ['a2a-v1'],
supported_modalities: ['text', 'json'],
processing_modes: [processingMode],
workflow_integration: {
enabled: enableWorkflowIntegration,
},
auth_mode: authMode,
endpoint: endpoints.endpoint,
tasks: endpoints.tasks,
health: endpoints.health,
timestamp: new Date().toISOString(),
};
// Ensure proper URL formatting (remove trailing slash if present)
const cleanRegistryUrl = registryUrl.replace(/\/+$/, '');
const registrationUrl = `${cleanRegistryUrl}/v1/agents`;
const response = await fetch(registrationUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(agentCard),
});
let result = null;
// Check if response is JSON
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
result = await response.json();
}
else {
// Not JSON response, get as text
const textResult = await response.text();
result = { error: 'Non-JSON response', content_type: contentType, response_text: textResult };
}
// Add registration event to task queue for workflow
if (enableWorkflowIntegration) {
taskQueue.push({
event_type: 'a2a_server_registered',
agent_id: agentId,
registry_url: registryUrl,
registration_successful: response.ok,
agent_card: agentCard,
registry_response: result,
timestamp: new Date().toISOString(),
});
}
}
catch (error) {
// Registry registration failed - continue without registry
}
};
app.get('/health', (req, res) => res.json({
status: 'healthy',
agent_id: agentId,
agent_type: 'n8n_a2a_agent',
a2a_protocol_version: '1.0',
processing_mode: processingMode,
workflow_integration: enableWorkflowIntegration,
auth_mode: authMode,
active_tasks: taskStore.size,
uptime_seconds: Math.floor(process.uptime()),
timestamp: new Date().toISOString(),
}));
app.get('/capabilities', (req, res) => res.json({
agent_id: agentId,
name: `N8N A2A Agent ${agentId}`,
description: 'N8N-powered A2A agent with workflow integration',
version: '1.0.0',
a2a_protocol_version: '1.0',
capabilities: agentCapabilities,
skills: agentSkills.map((skill) => ({
id: skill.id,
name: skill.name,
description: skill.description,
inputModes: skill.inputModes,
outputModes: skill.outputModes,
category: skill.category,
tags: skill.tags,
examples: skill.examples
})),
supported_protocols: ['a2a-v1'],
supported_modalities: ['text', 'json'],
processing_modes: [processingMode],
workflow_integration: {
enabled: enableWorkflowIntegration,
},
auth_mode: authMode,
endpoint: endpoints.endpoint,
tasks: endpoints.tasks,
health: endpoints.health,
timestamp: new Date().toISOString(),
}));
app.post('/tasks', async (req, res) => {
try {
// Validate JSON-RPC 2.0 request format
if (!req.body.jsonrpc || req.body.jsonrpc !== "2.0") {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid Request",
data: "Missing or invalid jsonrpc field. Expected '2.0'."
},
id: req.body.id || null
});
}
const supportedMethods = ["submitTask", "getTaskStatus", "getCapabilities"];
if (!req.body.method || !supportedMethods.includes(req.body.method)) {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32601,
message: "Method not found",
data: `Unsupported method: ${req.body.method}. Supported methods: ${supportedMethods.join(', ')}.`
},
id: req.body.id || null
});
}
if (!req.body.params) {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32602,
message: "Invalid params",
data: "Missing params object in JSON-RPC request."
},
id: req.body.id || null
});
}
const params = req.body.params;
const rpcId = req.body.id;
const method = req.body.method;
// Handle different JSON-RPC 2.0 methods
if (method === "getTaskStatus") {
// Handle task status request
const taskId = params.task_id;
const task = taskStore.get(taskId);
if (!task) {
return res.status(404).json({
jsonrpc: "2.0",
error: {
code: -32602,
message: "Task not found",
data: `No task found with ID: ${taskId}`
},
id: rpcId
});
}
return res.json({
jsonrpc: "2.0",
result: Object.assign(Object.assign(Object.assign(Object.assign({ task_id: taskId, session_id: task.session_id, status: task.status, agent_id: agentId, created_at: task.created_at, processing_mode: task.processing_mode, task_type: task.task_type, a2a_protocol_version: '1.0' }, (task.workflow_result && { workflow_result: task.workflow_result })), (task.completed_at && { completed_at: task.completed_at })), (task.client_info && { client_info: task.client_info })), { timestamp: new Date().toISOString() }),
id: rpcId
});
}
else if (method === "getCapabilities") {
// Handle capabilities request
return res.json({
jsonrpc: "2.0",
result: {
agent_id: agentId,
name: `N8N A2A Agent ${agentId}`,
description: 'N8N-powered A2A agent with workflow integration',
version: '1.0.0',
a2a_protocol_version: '1.0',
capabilities: agentCapabilities,
skills: agentSkills.map((skill) => ({
id: skill.id,
name: skill.name,
description: skill.description,
inputModes: skill.inputModes,
outputModes: skill.outputModes,
category: skill.category,
tags: skill.tags,
examples: skill.examples
})),
supported_protocols: ['a2a-v1'],
supported_modalities: ['text', 'json'],
processing_modes: [processingMode],
workflow_integration: {
enabled: enableWorkflowIntegration,
},
auth_mode: authMode,
endpoint: endpoints.endpoint,
tasks: endpoints.tasks,
health: endpoints.health,
timestamp: new Date().toISOString(),
},
id: rpcId
});
}
else if (method === "submitTask") {
// Handle task submission (existing logic)
const taskId = `task_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
const sessionId = req.headers['x-a2a-session-id'] || `session_${Date.now()}`;
const correlationId = req.headers['x-a2a-correlation-id'] || taskId;
const requestedMode = params.processing_mode || req.headers['x-a2a-processing-mode'] || processingMode;
// Determine if we should process synchronously
const shouldProcessSync = requestedMode === 'sync' ||
(processingMode === 'sync') ||
(processingMode === 'mixed' && requestedMode === 'sync');
const a2aTask = {
task_id: taskId,
session_id: sessionId,
correlation_id: correlationId,
agent_id: agentId,
created_at: new Date().toISOString(),
processing_mode: requestedMode,
task_type: params.type || 'general',
description: params.description || 'A2A task processing',
input_data: params,
context: params.context || {},
required_capabilities: params.required_capabilities || [],
client_info: {
user_agent: req.headers['user-agent'],
ip_address: req.ip,
a2a_client_id: req.headers['x-a2a-client-id'],
},
status: 'received',
a2a_protocol_version: '1.0',
jsonrpc_id: rpcId,
};
taskStore.set(taskId, a2aTask);
// FORWARD CLIENT'S TASK DATA to next workflow node for processing
if (enableWorkflowIntegration) {
if (shouldProcessSync) {
// SYNC MODE: Emit to workflow and wait for results
// Update task status to processing
a2aTask.status = 'processing';
taskStore.set(taskId, a2aTask);
// Create workflow data with CLIENT DATA AT ROOT LEVEL
const workflowData = Object.assign(Object.assign({
// ESSENTIAL FIELDS - Always available
task_id: taskId, session_id: sessionId }, a2aTask.input_data), {
// CORE A2A FIELDS (without a2a_ prefix for common fields)
task_type: a2aTask.task_type, description: a2aTask.description, context: a2aTask.context, processing_mode: requestedMode, created_at: a2aTask.created_at,
// A2A PROTOCOL METADATA (with a2a_ prefix)
a2a_correlation_id: correlationId, a2a_agent_id: agentId, a2a_session_id: sessionId, a2a_required_capabilities: a2aTask.required_capabilities, a2a_status: 'processing', a2a_callback_url: `${callbackUrlBase}/${taskId}/status`, a2a_protocol_version: '1.0', a2a_sync_mode: true,
// WORKFLOW METADATA
workflow_integration: true, event_type: 'task_received', timestamp: new Date().toISOString(), client_info: a2aTask.client_info,
// SYNC PROCESSING GUIDANCE
_workflow_instructions: {
sync_mode: true,
callback_required: true,
callback_url: `${callbackUrlBase}/${taskId}/status`,
note: 'Sync mode - please callback with results, client is waiting'
} });
// Add to queue for emission in proper trigger context
taskQueue.push(workflowData);
// WAIT FOR WORKFLOW RESULTS (polling approach)
const maxWaitTime = 30000; // 30 seconds max
const pollInterval = 200; // Check every 200ms
const startTime = Date.now();
// Wait for workflow to complete and callback with results
const waitForResults = new Promise((resolve, reject) => {
const checkForResults = () => {
const updatedTask = taskStore.get(taskId);
const elapsedTime = Date.now() - startTime;
if (updatedTask && updatedTask.status === 'completed' && updatedTask.result) {
resolve(updatedTask.result);
}
else if (elapsedTime > maxWaitTime) {
reject(new Error(`Workflow processing timeout after ${maxWaitTime}ms`));
}
else {
// Continue polling
setTimeout(checkForResults, pollInterval);
}
};
// Start polling
setTimeout(checkForResults, pollInterval);
});
try {
// Wait for workflow results
const workflowResult = await waitForResults;
// Update final task status
const finalTask = taskStore.get(taskId);
if (finalTask) {
finalTask.status = 'completed';
finalTask.completed_at = new Date().toISOString();
taskStore.set(taskId, finalTask);
}
// Return JSON-RPC 2.0 workflow results in sync response
res.status(200).json({
jsonrpc: "2.0",
result: {
task_id: taskId,
session_id: sessionId,
correlation_id: correlationId,
status: 'completed',
processing_mode: 'synchronous_workflow',
processing_location: 'n8n_workflow',
processing_time_ms: Date.now() - new Date(a2aTask.created_at).getTime(),
workflow_result: workflowResult, // This is the workflow output!
completed_at: new Date().toISOString(),
a2a_protocol_version: '1.0',
agent_id: agentId,
workflow_processing: {
workflow_completed: true,
workflow_result_available: true,
processing_method: 'workflow_callback'
}
},
id: rpcId
});
}
catch (error) {
// JSON-RPC 2.0 sync processing timeout or error
res.status(408).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Workflow processing timeout",
data: {
task_id: taskId,
session_id: sessionId,
correlation_id: correlationId,
status: 'timeout',
processing_mode: 'synchronous_workflow_timeout',
processing_time_ms: Date.now() - new Date(a2aTask.created_at).getTime(),
a2a_protocol_version: '1.0',
agent_id: agentId,
workflow_processing: {
workflow_timeout: true,
max_wait_time_ms: maxWaitTime,
suggestion: 'Try async mode for long-running workflows'
},
original_error: error.message
}
},
id: rpcId
});
}
}
else {
// ASYNC MODE: Emit to workflow for processing
// Update task status to processing (will be processed by workflow)
a2aTask.status = 'processing';
taskStore.set(taskId, a2aTask);
// Create workflow data with CLIENT DATA AT ROOT LEVEL
const workflowData = Object.assign(Object.assign({
// ESSENTIAL FIELDS - Always available
task_id: taskId, session_id: sessionId }, a2aTask.input_data), {
// CORE A2A FIELDS (without a2a_ prefix for common fields)
task_type: a2aTask.task_type, description: a2aTask.description, context: a2aTask.context, processing_mode: requestedMode, created_at: a2aTask.created_at,
// A2A PROTOCOL METADATA (with a2a_ prefix)
a2a_correlation_id: correlationId, a2a_agent_id: agentId, a2a_session_id: sessionId, a2a_required_capabilities: a2aTask.required_capabilities, a2a_status: 'processing', a2a_callback_url: `${callbackUrlBase}/${taskId}/status`, a2a_protocol_version: '1.0', a2a_sync_mode: false,
// WORKFLOW METADATA
workflow_integration: true, event_type: 'task_received', timestamp: new Date().toISOString(), client_info: a2aTask.client_info,
// ASYNC PROCESSING GUIDANCE
_workflow_instructions: {
sync_mode: false,
callback_optional: true,
callback_url: `${callbackUrlBase}/${taskId}/status`,
note: 'Async mode - callback optional but recommended for status updates'
} });
// Add to queue for emission in proper trigger context
taskQueue.push(workflowData);
// Return JSON-RPC 2.0 immediate accepted response for async mode
res.status(202).json({
jsonrpc: "2.0",
result: {
task_id: taskId,
session_id: sessionId,
correlation_id: correlationId,
status: 'accepted',
message: 'Task accepted and forwarded to N8N workflow for asynchronous processing',
processing_mode: 'asynchronous_workflow',
processing_location: 'n8n_workflow',
status_url: `${endpoints.endpoint}/tasks/${taskId}`,
a2a_protocol_version: '1.0',
agent_id: agentId,
estimated_completion: new Date(Date.now() + 60000).toISOString(),
workflow_callback_info: {
callback_url: `${callbackUrlBase}/${taskId}/status`,
callback_optional: true,
note: 'Workflow can optionally call back to update status/results'
}
},
id: rpcId
});
}
}
} // Close submitTask method
}
catch (error) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Task processing failed",
data: {
original_error: error.message,
a2a_protocol_version: '1.0',
troubleshooting: {
common_causes: [
'Invalid task data format',
'Server configuration error',
'Network connectivity issue'
],
documentation: 'See WORKFLOW-CONFIGURATION-GUIDE.md'
}
}
},
id: req.body.id || null
});
}
});
app.get('/tasks/:taskId', (req, res) => {
const task = taskStore.get(req.params.taskId);
if (!task)
return res.status(404).json({
error: 'Task not found',
task_id: req.params.taskId,
a2a_protocol_version: '1.0'
});
if (enableWorkflowIntegration) {
// Add status request to task queue for workflow
taskQueue.push({
event_type: 'a2a_task_status_requested',
task_id: req.params.taskId,
task_details: task,
workflow_integration: true,
timestamp: new Date().toISOString(),
});
}
res.json(Object.assign(Object.assign(Object.assign(Object.assign({ task_id: req.params.taskId, session_id: task.session_id, status: task.status, agent_id: agentId, created_at: task.created_at, processing_mode: task.processing_mode, task_type: task.task_type, a2a_protocol_version: '1.0' }, (task.result && { result: task.result })), (task.completed_at && { completed_at: task.completed_at })), (task.client_info && { client_info: task.client_info })), { timestamp: new Date().toISOString() }));
});
app.post('/tasks/:taskId/status', (req, res) => {
const task = taskStore.get(req.params.taskId);
if (!task)
return res.status(404).json({
error: 'Task not found',
task_id: req.params.taskId,
a2a_protocol_version: '1.0'
});
// Update task with callback results
task.status = req.body.status || 'completed';
task.result = req.body.result;
task.completed_at = req.body.completed_at || new Date().toISOString();
taskStore.set(req.params.taskId, task);
res.json({
task_id: req.params.taskId,
status: 'callback_received',
message: 'Task status updated successfully',
a2a_protocol_version: '1.0'
});
});
const server = app.listen(port, async () => {
// Store the server reference for this node instance
(0, urlUtils_1.storeActiveServer)(nodeId, server, port, 'agent');
// Wait a moment for registry to be fully ready, then register
setTimeout(async () => {
await registerWithRegistry();
}, 2000); // 2 second delay
});
// Handle server startup errors
server.on('error', async (error) => {
if (error.code === 'EADDRINUSE') {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `❌ Port ${port} is already in use. Please choose a different port for A2A Agent (${agentId}).`);
}
else {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `❌ A2A Agent server error: ${error.message}`);
}
});
return {
closeFunction: async () => {
server.close();
taskStore.clear();
clearInterval(taskProcessor);
taskQueue.length = 0; // Clear task queue
(0, urlUtils_1.cleanupActiveServer)(nodeId); // Clean up stored server reference
},
manualTriggerFunction: async () => {
this.emit([this.helpers.returnJsonArray([Object.assign(Object.assign({ event_type: 'a2a_server_ready', message: 'A2A Server is ready to receive tasks from clients', agent_id: agentId, a2a_protocol_version: '1.0' }, endpoints), { processing_mode: processingMode, workflow_integration: enableWorkflowIntegration, auth_mode: authMode, instructions: 'Send tasks to the /tasks endpoint. Client data will be forwarded to next workflow nodes.', timestamp: new Date().toISOString() })])]);
},
};
}
}
exports.A2ARemoteAgent = A2ARemoteAgent;