UNPKG

n8n-nodes-a2a-protocol

Version:

Agent2Agent (A2A) Protocol nodes for n8n - Enable agent interoperability, communication, and MCP integration

425 lines (417 loc) 21.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.A2ACallback = void 0; const n8n_workflow_1 = require("n8n-workflow"); const urlUtils_1 = require("../../utils/urlUtils"); class A2ACallback { constructor() { this.description = { displayName: 'A2A Callback', name: 'a2aCallback', icon: 'file:a2a-callback.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Send results back to A2A Server for sync processing', defaults: { name: 'A2A Callback', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], credentials: [], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Send Success Result', value: 'success', description: 'Send successful processing result to A2A Server', action: 'Send successful result', }, { name: 'Send Error Result', value: 'error', description: 'Send error result to A2A Server', action: 'Send error result', }, { name: 'Send Custom Result', value: 'custom', description: 'Send custom result with full control', action: 'Send custom result', }, ], default: 'success', }, // Success Result Configuration { displayName: 'Result Message', name: 'resultMessage', type: 'string', default: 'Task completed successfully by workflow', description: 'Success message to include in the result', displayOptions: { show: { operation: ['success'], }, }, }, { displayName: 'Include Input Data', name: 'includeInputData', type: 'boolean', default: true, description: 'Whether to include the original input data in the result', displayOptions: { show: { operation: ['success'], }, }, }, { displayName: 'Include Processed Data', name: 'includeProcessedData', type: 'boolean', default: true, description: 'Whether to include the processed workflow data in the result', displayOptions: { show: { operation: ['success'], }, }, }, // Error Result Configuration { displayName: 'Error Message', name: 'errorMessage', type: 'string', default: 'An error occurred during workflow processing', description: 'Error message to send to A2A Server', displayOptions: { show: { operation: ['error'], }, }, }, { displayName: 'Error Code', name: 'errorCode', type: 'string', default: 'WORKFLOW_ERROR', description: 'Error code to identify the type of error', displayOptions: { show: { operation: ['error'], }, }, }, // Custom Result Configuration { displayName: 'Custom Status', name: 'customStatus', type: 'options', options: [ { name: 'Completed', value: 'completed', }, { name: 'Failed', value: 'failed', }, { name: 'Partial', value: 'partial', }, ], default: 'completed', description: 'Custom status to send', displayOptions: { show: { operation: ['custom'], }, }, }, { displayName: 'Custom Result Data', name: 'customResult', type: 'json', default: '{\n "processed": true,\n "message": "Custom processing complete"\n}', description: 'Custom result data as JSON', displayOptions: { show: { operation: ['custom'], }, }, }, // Simplified Callback URL Configuration { displayName: 'Callback URL Expression', name: 'callbackUrlExpression', type: 'string', default: '{{ $("A2A Agent Server").item.json._workflow_instructions.callback_url}}', placeholder: '{{ $("A2A Agent Server").item.json._workflow_instructions.callback_url}}', description: 'n8n expression to get the callback URL from the A2A Server node', required: true, }, // Advanced Options { displayName: 'Advanced Options', name: 'advancedOptions', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { displayName: 'Callback URL Field', name: 'callbackUrlField', type: 'string', default: 'a2a_callback_url', description: 'Field name containing the callback URL (default: a2a_callback_url)', }, { displayName: 'Task ID Field', name: 'taskIdField', type: 'string', default: 'task_id', description: 'Field name containing the task ID (default: task_id)', }, { displayName: 'Session ID Field', name: 'sessionIdField', type: 'string', default: 'session_id', description: 'Field name containing the session ID (default: session_id)', }, { displayName: 'Timeout (seconds)', name: 'timeout', type: 'number', default: 10, description: 'Timeout for the callback request in seconds', }, { displayName: 'Include Workflow Metadata', name: 'includeWorkflowMetadata', type: 'boolean', default: true, description: 'Include workflow execution metadata in the callback', }, ], }, ], }; } async execute() { var _a, _b, _c, _d, _e, _f; const items = this.getInputData(); const returnData = []; for (let i = 0; i < items.length; i++) { try { const operation = this.getNodeParameter('operation', i); const advancedOptions = this.getNodeParameter('advancedOptions', i); // Get the callback URL expression (simplified - only one way now) const callbackUrlExpression = this.getNodeParameter('callbackUrlExpression', i); // Get field names (with defaults) const callbackUrlField = advancedOptions.callbackUrlField || 'a2a_callback_url'; const taskIdField = advancedOptions.taskIdField || 'task_id'; const sessionIdField = advancedOptions.sessionIdField || 'session_id'; const timeout = (advancedOptions.timeout || 10) * 1000; // Convert to milliseconds const includeWorkflowMetadata = advancedOptions.includeWorkflowMetadata !== false; // Get callback URL from input data - check multiple locations let callbackUrl = items[i].json[callbackUrlField]; let taskId = items[i].json[taskIdField]; let sessionId = items[i].json[sessionIdField]; try { // Evaluate the expression using n8n's expression resolver callbackUrl = this.evaluateExpression(callbackUrlExpression, i); // Extract task ID from callback URL if needed if (!taskId && callbackUrl) { const taskIdMatch = callbackUrl.match(/\/tasks\/([^\/]+)\/status/); if (taskIdMatch) { taskId = taskIdMatch[1]; } } } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to evaluate callback URL expression "${callbackUrlExpression}": ${error.message}. This usually means the node name in the expression doesn't match the actual node name in your workflow. Common solutions: 1. Check the exact node name in your workflow (including spaces) 2. Try using: {{ $("A2A Agent Server").item.json._workflow_instructions.callback_url}} 3. Or try: {{ $("A2A Server").item.json._workflow_instructions.callback_url}} 4. Or check the debug output above for available node names`, { itemIndex: i }); } // If not found in expression, check nested _workflow_instructions if (!callbackUrl && items[i].json._workflow_instructions) { const workflowInstructions = items[i].json._workflow_instructions; callbackUrl = workflowInstructions.callback_url; // Also try to get task ID and session from workflow instructions or derive from callback URL if (!taskId && callbackUrl) { const taskIdMatch = callbackUrl.match(/\/tasks\/([^\/]+)\/status/); if (taskIdMatch) { taskId = taskIdMatch[1]; } } } // Check for A2A Agent Server._workflow_instructions.callback_url (user specified location) if (!callbackUrl && items[i].json['A2A Agent Server'] && items[i].json['A2A Agent Server']._workflow_instructions) { const serverInstructions = items[i].json['A2A Agent Server']._workflow_instructions; callbackUrl = serverInstructions.callback_url; if (!taskId && callbackUrl) { const taskIdMatch = callbackUrl.match(/\/tasks\/([^\/]+)\/status/); if (taskIdMatch) { taskId = taskIdMatch[1]; } } } // Check for [1].._workflow_instructions.callback_url (user specified location) if (!callbackUrl && Array.isArray(items[i].json) && items[i].json[1] && items[i].json[1]._workflow_instructions) { const arrayInstructions = items[i].json[1]._workflow_instructions; callbackUrl = arrayInstructions.callback_url; if (!taskId && callbackUrl) { const taskIdMatch = callbackUrl.match(/\/tasks\/([^\/]+)\/status/); if (taskIdMatch) { taskId = taskIdMatch[1]; } } } // Check if callback URL is available in output field (some workflows might nest it there) if (!callbackUrl && items[i].json.output) { const outputData = items[i].json.output; if (outputData[callbackUrlField]) { callbackUrl = outputData[callbackUrlField]; taskId = taskId || outputData[taskIdField]; sessionId = sessionId || outputData[sessionIdField]; } // Check for _workflow_instructions in output if (!callbackUrl && outputData._workflow_instructions) { const workflowInstructions = outputData._workflow_instructions; callbackUrl = workflowInstructions.callback_url; // Extract task ID from callback URL if needed if (!taskId && callbackUrl) { const taskIdMatch = callbackUrl.match(/\/tasks\/([^\/]+)\/status/); if (taskIdMatch) { taskId = taskIdMatch[1]; } } } // Check for A2A Agent Server in output if (!callbackUrl && outputData['A2A Agent Server'] && outputData['A2A Agent Server']._workflow_instructions) { const serverInstructions = outputData['A2A Agent Server']._workflow_instructions; callbackUrl = serverInstructions.callback_url; if (!taskId && callbackUrl) { const taskIdMatch = callbackUrl.match(/\/tasks\/([^\/]+)\/status/); if (taskIdMatch) { taskId = taskIdMatch[1]; } } } } if (!callbackUrl) { const defaultCallbackUrl = (0, urlUtils_1.getDefaultCallbackUrl)(urlUtils_1.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR); throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Callback URL not found in expression or any fallback locations. Expression attempted: ${callbackUrlExpression} Available fields: ${Object.keys(items[i].json).join(', ')} Checked locations: 1. Expression: ${callbackUrlExpression} 2. ${callbackUrlField} (flat): ${items[i].json[callbackUrlField] || 'NOT_FOUND'} 3. _workflow_instructions.callback_url: ${((_a = items[i].json._workflow_instructions) === null || _a === void 0 ? void 0 : _a.callback_url) || 'NOT_FOUND'} 4. A2A Agent Server._workflow_instructions.callback_url: ${((_c = (_b = items[i].json['A2A Agent Server']) === null || _b === void 0 ? void 0 : _b._workflow_instructions) === null || _c === void 0 ? void 0 : _c.callback_url) || 'NOT_FOUND'} 5. output.${callbackUrlField}: ${((_d = items[i].json.output) === null || _d === void 0 ? void 0 : _d[callbackUrlField]) || 'NOT_FOUND'} 6. output._workflow_instructions.callback_url: ${((_f = (_e = items[i].json.output) === null || _e === void 0 ? void 0 : _e._workflow_instructions) === null || _f === void 0 ? void 0 : _f.callback_url) || 'NOT_FOUND'} The A2A Server should provide the callback URL in one of these locations. Make sure this node is connected after an A2A Server trigger. Expected callback URL format: ${defaultCallbackUrl}`, { itemIndex: i }); } // Build callback payload based on operation let callbackPayload = { task_id: taskId, session_id: sessionId, completed_at: new Date().toISOString(), callback_source: 'n8n_workflow', a2a_protocol_version: '1.0', }; if (operation === 'success') { const resultMessage = this.getNodeParameter('resultMessage', i); const includeInputData = this.getNodeParameter('includeInputData', i); const includeProcessedData = this.getNodeParameter('includeProcessedData', i); callbackPayload.status = 'completed'; callbackPayload.result = Object.assign(Object.assign(Object.assign({ success: true, message: resultMessage, workflow_completed: true }, (includeInputData && { input_data: { type: items[i].json.type, description: items[i].json.description, input: items[i].json.input, data: items[i].json.data, } })), (includeProcessedData && { processed_data: items[i].json, workflow_output: items[i].json })), (includeWorkflowMetadata && { workflow_metadata: { node_name: this.getNode().name, execution_time: new Date().toISOString(), workflow_id: items[i].json.workflowId, processing_method: 'n8n_workflow_callback' } })); } else if (operation === 'error') { const errorMessage = this.getNodeParameter('errorMessage', i); const errorCode = this.getNodeParameter('errorCode', i); callbackPayload.status = 'failed'; callbackPayload.error = { code: errorCode, message: errorMessage, details: 'Error occurred during workflow processing', timestamp: new Date().toISOString(), }; } else if (operation === 'custom') { const customStatus = this.getNodeParameter('customStatus', i); const customResultStr = this.getNodeParameter('customResult', i); let customResult; try { customResult = JSON.parse(customResultStr); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid JSON in custom result: ${error.message}`, { itemIndex: i }); } callbackPayload.status = customStatus; callbackPayload.result = customResult; } // Make the callback request const response = await fetch(callbackUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(callbackPayload), }); const responseData = await response.json(); if (response.ok) { returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_callback_success: true, a2a_callback_status: response.status, a2a_callback_response: responseData, a2a_callback_sent_at: new Date().toISOString(), operation: operation, callback_url: callbackUrl }), }); } else { returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_callback_success: false, a2a_callback_error: `HTTP ${response.status}: ${responseData.message || 'Unknown error'}`, a2a_callback_response: responseData, operation: operation, callback_url: callbackUrl }), }); } } catch (error) { // Return error information but don't fail the workflow returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_callback_success: false, a2a_callback_error: error.message, operation: this.getNodeParameter('operation', i), error_timestamp: new Date().toISOString() }), }); } } return [returnData]; } } exports.A2ACallback = A2ACallback;