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
JavaScript
;
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;