n8n-nodes-better-ai-agent
Version:
A better AI Agent node for n8n with improved memory management and modern AI SDK integration
722 lines (721 loc) • 34.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BetterAiAgent = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const ai_1 = require("ai");
const openai_1 = require("@ai-sdk/openai");
const anthropic_1 = require("@ai-sdk/anthropic");
const google_1 = require("@ai-sdk/google");
const zod_1 = require("zod");
const utils_1 = require("./utils");
const chatArrayMemory_1 = require("./utils/chatArrayMemory");
const sdk_node_1 = require("@opentelemetry/sdk-node");
const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
const langfuse_vercel_1 = require("langfuse-vercel");
// @ts-ignore
const vercel_1 = require("langsmith/vercel");
// Patch console.log once to respect global verbose flag
if (!globalThis.__BAA_LOG_PATCHED) {
const originalLog = console.log.bind(console);
console.log = (...args) => {
if (globalThis.__BAA_VERBOSE) {
originalLog(...args);
}
};
globalThis.__BAA_LOG_PATCHED = true;
}
// --- OpenTelemetry tracing (Langfuse) ---
if (!globalThis.__BAA_OTEL_INITIALIZED) {
try {
// Determine preferred trace exporter
let traceExporter;
let providerName = 'langfuse';
if (process.env.LANGSMITH_TRACING === 'true' || process.env.LANGSMITH_API_KEY) {
traceExporter = new vercel_1.AISDKExporter();
providerName = 'langsmith';
}
else {
traceExporter = new langfuse_vercel_1.LangfuseExporter();
providerName = 'langfuse';
}
const sdk = new sdk_node_1.NodeSDK({
traceExporter,
instrumentations: [(0, auto_instrumentations_node_1.getNodeAutoInstrumentations)()],
});
sdk.start();
globalThis.__BAA_OTEL_INITIALIZED = sdk;
globalThis.__BAA_TRACE_PROVIDER = providerName;
console.log(`✅ OpenTelemetry SDK initialized with ${providerName} exporter`);
}
catch (err) {
console.warn('❌ Failed to initialize OpenTelemetry SDK:', err);
}
}
// Generic helper: pull a numeric or string setting from multiple possible paths on the LangChain model
function readModelSetting(model, key) {
if (!model)
return undefined;
if (model.options && model.options[key] !== undefined)
return model.options[key];
if (model[key] !== undefined)
return model[key];
if (model.clientConfig && model.clientConfig[key] !== undefined)
return model.clientConfig[key];
if (model.kwargs && model.kwargs[key] !== undefined)
return model.kwargs[key];
return undefined;
}
// Helper function to convert n8n model to AI SDK compatible format
function convertN8nModelToAiSdk(n8nModel) {
if (!n8nModel) {
throw new Error('No language model provided');
}
// Debug: Log the model properties to understand the structure
console.log('n8n Model type:', n8nModel.constructor?.name);
console.log('n8n Model properties:', Object.keys(n8nModel));
// Extract model information from the LangChain model
const modelName = n8nModel.modelName || n8nModel.model || 'gpt-4o-mini';
// Check if it's an OpenAI-compatible model (OpenAI, Azure OpenAI, etc.)
if (n8nModel.constructor?.name?.includes('ChatOpenAI') ||
n8nModel.constructor?.name?.includes('OpenAI')) {
// Settings that should be sent with the model invocation (generation parameters)
const modelSettings = {};
// Settings that belong to the provider (transport-level)
const providerSettings = {};
const temp = readModelSetting(n8nModel, 'temperature');
const topP = readModelSetting(n8nModel, 'topP');
const maxTokens = readModelSetting(n8nModel, 'maxTokens');
const freqPen = readModelSetting(n8nModel, 'frequencyPenalty');
const presPen = readModelSetting(n8nModel, 'presencePenalty');
const reasoningEffort = readModelSetting(n8nModel, 'reasoningEffort');
if (temp !== undefined && temp !== 0) {
// User explicitly set a non-zero temperature – use it as is
modelSettings.temperature = temp;
}
else if ((temp === undefined || temp === 0) && (/^o\d/.test(modelName) || /^gpt-4o/.test(modelName))) {
// OpenAI "o" family (o1, o3, o4…) *and* gpt-4o models mandate temperature=1
console.log('Auto-setting temperature=1 for o-family / gpt-4o model');
modelSettings.temperature = 1;
}
if (maxTokens !== undefined)
modelSettings.maxTokens = maxTokens;
if (topP !== undefined)
modelSettings.topP = topP;
if (freqPen !== undefined)
modelSettings.frequencyPenalty = freqPen;
if (presPen !== undefined)
modelSettings.presencePenalty = presPen;
if (reasoningEffort !== undefined)
modelSettings.reasoningEffort = reasoningEffort;
// Extract API key from the LangChain model
const apiKey = n8nModel.openAIApiKey || n8nModel.apiKey;
console.log('OpenAI API Key found:', apiKey ? 'YES (length: ' + apiKey.length + ')' : 'NO');
// Try to get API key from clientConfig if not found directly
let finalApiKey = apiKey;
if (!finalApiKey && n8nModel.clientConfig) {
const clientApiKey = n8nModel.clientConfig.apiKey || n8nModel.clientConfig.openAIApiKey;
console.log('Client config API key:', clientApiKey ? 'YES (length: ' + clientApiKey.length + ')' : 'NO');
if (clientApiKey) {
finalApiKey = clientApiKey;
}
}
// Extract base URL if it exists (for Azure OpenAI, etc.)
if (n8nModel.configuration?.baseURL) {
providerSettings.baseURL = n8nModel.configuration.baseURL;
}
// Use createOpenAI with explicit API key instead of openai()
if (finalApiKey) {
console.log('Using createOpenAI with explicit API key');
const openaiProvider = (0, openai_1.createOpenAI)({
apiKey: finalApiKey,
...providerSettings,
});
return openaiProvider(modelName, modelSettings);
}
else {
console.log('No API key found, using default openai provider');
return (0, openai_1.openai)(modelName, { ...providerSettings, ...modelSettings });
}
}
// Check if it's a Google Generative AI model (Gemini) – case-insensitive to handle variations like ChatGoogleGenerativeAi
const ctorName = n8nModel.constructor?.name?.toLowerCase() || '';
if (ctorName.includes('googlegenerativeai') || ctorName.includes('gemini')) {
const settings = {};
const gemTemp = readModelSetting(n8nModel, 'temperature');
const gemTopP = readModelSetting(n8nModel, 'topP');
if (gemTemp !== undefined && gemTemp !== 0)
settings.temperature = gemTemp;
if (gemTopP !== undefined)
settings.topP = gemTopP;
const apiKey = n8nModel.apiKey || process.env.GOOGLE_AI_API_KEY;
if (!apiKey) {
throw new Error('Google Generative AI API key missing');
}
console.log('Using createGoogleGenerativeAI with explicit API key');
const geminiProvider = (0, google_1.createGoogleGenerativeAI)({ apiKey, ...settings });
const modelName = n8nModel.modelName || 'gemini-pro';
return geminiProvider(modelName);
}
// Check if it's an Anthropic model
if (n8nModel.constructor?.name?.includes('ChatAnthropic') ||
n8nModel.constructor?.name?.includes('Anthropic')) {
const settings = {};
const aTemp = readModelSetting(n8nModel, 'temperature');
const aTopP = readModelSetting(n8nModel, 'topP');
const aMax = readModelSetting(n8nModel, 'maxTokens');
const aReason = readModelSetting(n8nModel, 'reasoningEffort');
if (aTemp !== undefined && aTemp !== 0)
settings.temperature = aTemp;
if (aMax !== undefined)
settings.maxTokens = aMax;
if (aTopP !== undefined)
settings.topP = aTopP;
if (aReason !== undefined)
settings.reasoningEffort = aReason;
// Extract API key for Anthropic
const apiKey = n8nModel.anthropicApiKey || n8nModel.apiKey;
// Use createAnthropic with explicit API key
if (apiKey) {
console.log('Using createAnthropic with explicit API key');
const anthropicProvider = (0, anthropic_1.createAnthropic)({
apiKey: apiKey,
...settings
});
return anthropicProvider(modelName);
}
else {
console.log('No API key found, using default anthropic provider');
return (0, anthropic_1.anthropic)(modelName, settings);
}
}
// Default fallback to OpenAI with a sensible model
throw new Error(`Unsupported or unknown model type: ${n8nModel.constructor?.name}. Please connect a supported language model node or update convertN8nModelToAiSdk to handle this model.`);
}
// Recursively flatten arrays or containers that expose a .tools array (e.g., McpToolkit)
function* flattenTools(toolOrArray) {
if (!toolOrArray)
return;
if (Array.isArray(toolOrArray)) {
for (const t of toolOrArray)
yield* flattenTools(t);
}
else if (toolOrArray.tools && Array.isArray(toolOrArray.tools)) {
// MCP toolkit or similar wrapper
yield* flattenTools(toolOrArray.tools);
}
else {
yield toolOrArray;
}
}
// Helper function to convert n8n tools to AI SDK tools
function convertN8nToolsToAiSdk(n8nTools) {
const tools = {};
const flatTools = Array.from(flattenTools(n8nTools));
console.log('Converting n8n tools to AI SDK format:');
console.log('Number of tools after flatten:', flatTools.length);
for (const n8nTool of flatTools) {
console.log('n8n Tool:', {
name: n8nTool?.name,
description: n8nTool?.description,
schema: n8nTool?.schema,
keys: Object.keys(n8nTool || {})
});
if (n8nTool && n8nTool.name) {
// Create a more robust schema - handle ZodEffects
let toolSchema;
try {
if (n8nTool.schema) {
// Check if it's a ZodEffects and extract the underlying schema
if (n8nTool.schema._def && n8nTool.schema._def.schema) {
console.log('Extracting schema from ZodEffects');
toolSchema = n8nTool.schema._def.schema;
}
else {
toolSchema = n8nTool.schema;
}
}
else {
// Default schema if none provided
toolSchema = zod_1.z.object({
input: zod_1.z.string().describe('Tool input'),
});
}
console.log('Final tool schema:', toolSchema);
}
catch (error) {
console.warn(`Invalid schema for tool ${n8nTool.name}, using default:`, error);
toolSchema = zod_1.z.object({
input: zod_1.z.string().describe('Tool input'),
});
}
tools[n8nTool.name] = (0, ai_1.tool)({
description: n8nTool.description || `Execute ${n8nTool.name}`,
parameters: toolSchema,
execute: async (parameters) => {
console.log(`Executing tool ${n8nTool.name} with parameters:`, parameters);
try {
// Call the n8n tool
const result = await n8nTool.invoke(parameters);
console.log(`Tool ${n8nTool.name} result:`, result);
return result;
}
catch (error) {
console.error(`Tool ${n8nTool.name} execution failed:`, error);
throw error;
}
},
});
console.log(`Successfully converted tool: ${n8nTool.name}`);
}
else {
console.warn('Skipping invalid tool:', n8nTool);
}
}
console.log(`Total tools converted: ${Object.keys(tools).length}`);
return tools;
}
// Helper function to define the inputs based on n8n AI ecosystem
function getInputs() {
const getInputData = (inputs) => {
const displayNames = {
[n8n_workflow_1.NodeConnectionTypes.AiLanguageModel]: 'Chat Model',
[n8n_workflow_1.NodeConnectionTypes.AiMemory]: 'Memory',
[n8n_workflow_1.NodeConnectionTypes.AiTool]: 'Tool',
[n8n_workflow_1.NodeConnectionTypes.AiOutputParser]: 'Output Parser',
};
return inputs.map(({ type, filter, required }) => {
const input = {
type,
displayName: displayNames[type] || type,
required: required || type === n8n_workflow_1.NodeConnectionTypes.AiLanguageModel,
maxConnections: [n8n_workflow_1.NodeConnectionTypes.AiLanguageModel, n8n_workflow_1.NodeConnectionTypes.AiMemory, n8n_workflow_1.NodeConnectionTypes.AiOutputParser].includes(type)
? 1
: undefined,
};
if (filter) {
input.filter = filter;
}
return input;
});
};
const specialInputs = [
{
type: n8n_workflow_1.NodeConnectionTypes.AiLanguageModel,
required: true,
filter: {
nodes: [
'@n8n/n8n-nodes-langchain.lmChatAnthropic',
'@n8n/n8n-nodes-langchain.lmChatAzureOpenAi',
'@n8n/n8n-nodes-langchain.lmChatAwsBedrock',
'@n8n/n8n-nodes-langchain.lmChatMistralCloud',
'@n8n/n8n-nodes-langchain.lmChatOllama',
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
'@n8n/n8n-nodes-langchain.lmChatGroq',
'@n8n/n8n-nodes-langchain.lmChatGoogleVertex',
'@n8n/n8n-nodes-langchain.lmChatGoogleGemini',
'@n8n/n8n-nodes-langchain.lmChatDeepSeek',
'@n8n/n8n-nodes-langchain.lmChatOpenRouter',
'@n8n/n8n-nodes-langchain.lmChatXAiGrok',
'@n8n/n8n-nodes-langchain.code',
],
},
},
{
type: n8n_workflow_1.NodeConnectionTypes.AiMemory,
},
{
type: n8n_workflow_1.NodeConnectionTypes.AiTool,
},
{
type: n8n_workflow_1.NodeConnectionTypes.AiOutputParser,
},
];
return ['main', ...getInputData(specialInputs)];
}
class BetterAiAgent {
description = {
displayName: 'Better AI Agent',
name: 'betterAiAgent',
icon: 'fa:robot',
iconColor: 'black',
group: ['transform'],
version: 16,
description: 'Advanced AI Agent with improved memory management and modern AI SDK (OpenAI Message Format)',
defaults: {
name: 'Better AI Agent',
color: '#1f77b4',
},
inputs: getInputs(),
outputs: ['main'],
properties: [
{
displayName: 'Tip: This node uses modern AI SDK with proper tool call memory management',
name: 'notice_tip',
type: 'notice',
default: '',
},
{
...utils_1.promptTypeOptions,
},
{
...utils_1.textFromPreviousNode,
displayOptions: {
show: { promptType: ['auto'] },
},
},
{
...utils_1.textInput,
displayOptions: {
show: { promptType: ['define'] },
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add Option',
options: [
{
displayName: 'System Message',
name: 'systemMessage',
type: 'string',
default: 'You are a helpful AI assistant. Use the available tools when necessary to help the user accomplish their goals.',
description: 'The system message that defines the agent behavior',
typeOptions: {
rows: 4,
},
},
{
displayName: 'Max Steps',
name: 'maxSteps',
type: 'number',
default: 5,
description: 'Maximum number of tool call steps before stopping',
typeOptions: {
min: 1,
max: 20,
},
},
{
displayName: 'Intermediate Webhook URL',
name: 'intermediateWebhookUrl',
type: 'string',
default: '',
description: 'If set, the node POSTs every partial reply/tool-call as JSON to this URL while the agent is running',
},
{
displayName: 'Verbose Logs',
name: 'verboseLogs',
type: 'boolean',
default: false,
description: 'Enable detailed console logging for debugging',
},
],
},
],
};
async execute() {
const items = this.getInputData();
const returnData = [];
// Determine verbose flag once (from first item options) so logs are suppressed before conversion
const initialOpts = this.getNodeParameter('options', 0, {});
globalThis.__BAA_VERBOSE = !!initialOpts.verboseLogs;
// Get connected components
const connectedModel = await (0, utils_1.getConnectedModel)(this);
const connectedMemory = await (0, utils_1.getConnectedMemory)(this);
const connectedTools = await (0, utils_1.getConnectedTools)(this);
const connectedOutputParser = await (0, utils_1.getConnectedOutputParser)(this);
if (!connectedModel) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No language model connected');
}
// Convert n8n model to AI SDK model
const aiModel = convertN8nModelToAiSdk(connectedModel);
// Convert n8n tools to AI SDK tools
const aiTools = convertN8nToolsToAiSdk(connectedTools);
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
// Get input text
const input = (0, utils_1.getPromptInputByType)({
ctx: this,
i: itemIndex,
inputKey: 'text',
promptTypeKey: 'promptType',
});
if (!input) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No input text provided');
}
// Get options
const options = this.getNodeParameter('options', itemIndex, {});
// Helper to POST intermediate updates without blocking execution
const runId = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const postIntermediate = (payload) => {
if (!options.intermediateWebhookUrl)
return;
try {
const fetchFn = globalThis.fetch;
if (fetchFn) {
void fetchFn(options.intermediateWebhookUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
}
}
catch (err) {
console.warn('❌ Failed to post intermediate webhook:', err);
}
};
// Initialize memory adapter
let memoryAdapter = null;
if (connectedMemory) {
let messageLimit = null;
try {
// BufferWindowMemory instances expose the window size via `k`.
if (typeof connectedMemory.k === 'number') {
messageLimit = connectedMemory.k;
}
}
catch { }
memoryAdapter = new chatArrayMemory_1.ChatArrayMemory(connectedMemory, messageLimit);
}
// Load previous messages (if any)
let messages = [];
if (memoryAdapter) {
try {
messages = await memoryAdapter.load();
console.log(`✅ Loaded ${messages.length} messages from conversation history.`);
}
catch (err) {
console.warn('❌ Failed to load conversation history – starting fresh.', err);
}
}
// Append current user input
messages.push({ role: 'user', content: input });
// If a message limit is defined on the memory adapter, ensure we do not exceed it
if (memoryAdapter && memoryAdapter.maxMessages) {
const mm = memoryAdapter.maxMessages;
if (mm > 0 && messages.length > mm) {
messages = messages.slice(-mm);
}
}
// Generate response with AI SDK - using the pattern from the example
// Note: temperature, maxTokens, etc. come from the connected model, not node parameters
let stepCount = 0;
const genArgs = {
model: aiModel,
maxSteps: options.maxSteps || 5,
messages: messages,
// Provide the system prompt directly to the AI SDK when present
...(options.systemMessage ? { system: options.systemMessage } : {}),
onStepFinish: ({ text, toolCalls }) => {
postIntermediate({
version: 1,
runId,
step: stepCount,
text,
toolCalls,
done: false,
});
stepCount += 1;
},
};
// Extract generation settings from the model and pass them explicitly to generateText
// This prevents AI SDK from using its own defaults (like temperature: 0)
if (aiModel.settings) {
if (aiModel.settings.temperature !== undefined) {
genArgs.temperature = aiModel.settings.temperature;
}
if (aiModel.settings.topP !== undefined) {
genArgs.topP = aiModel.settings.topP;
}
if (aiModel.settings.frequencyPenalty !== undefined) {
genArgs.frequencyPenalty = aiModel.settings.frequencyPenalty;
}
if (aiModel.settings.presencePenalty !== undefined) {
genArgs.presencePenalty = aiModel.settings.presencePenalty;
}
if (aiModel.settings.maxTokens !== undefined) {
genArgs.maxTokens = aiModel.settings.maxTokens;
}
if (aiModel.settings.reasoningEffort !== undefined) {
genArgs.reasoningEffort = aiModel.settings.reasoningEffort;
}
}
if (Object.keys(aiTools).length > 0) {
genArgs.tools = aiTools;
}
// Enable OpenTelemetry tracing for this generation (Langfuse / LangSmith)
let telemetrySettings;
if (globalThis.__BAA_TRACE_PROVIDER === 'langsmith') {
telemetrySettings = vercel_1.AISDKExporter.getSettings({
runId,
metadata: { n8nNodeName: this.getNode().name ?? 'BetterAiAgent' },
});
}
else {
telemetrySettings = {
isEnabled: true,
functionId: runId,
metadata: { n8nNodeName: this.getNode().name ?? 'BetterAiAgent' },
};
}
genArgs.experimental_telemetry = telemetrySettings;
// Wrap generation call in a retry loop so that the agent can recover from
// tool argument validation errors (e.g. AI_TypeValidationError) by feeding the
// error back as a tool result. This allows the language model to attempt to
// re-issue the tool-call with corrected parameters instead of aborting the
// entire node execution.
let result = null;
const maxRetries = Math.max(1, (options.maxSteps || 5));
let retryCount = 0;
// We reuse the same genArgs object but update the messages array in-place on
// every retry so that additional tool-result error messages are available to
// the model.
while (retryCount < maxRetries) {
try {
// Always reference the latest messages array
genArgs.messages = messages;
result = await (0, ai_1.generateText)(genArgs);
break; // success, exit retry loop
}
catch (err) {
// Detect AI SDK validation or execution errors on tool calls. Those
// expose the name "AI_TypeValidationError" (for schema issues) or may
// simply bubble up from the tool execution. In these cases we build a
// synthetic tool-result message that contains the error so the model can
// try again.
const errName = err?.name || '';
const isToolError = errName.startsWith('AI_') || err.cause?.name === 'ZodError';
if (!isToolError || retryCount >= maxRetries - 1) {
// Not a tool error we can recover from OR we exhausted retries –
// rethrow so that n8n's retry mechanism can take over (if enabled)
throw err;
}
console.warn(`⚠️ Tool call failed (attempt ${retryCount + 1}/${maxRetries}):`, err);
// Post an intermediate update so a webhook (if configured) is aware of
// the failure and retry.
postIntermediate({
version: 1,
runId,
step: stepCount,
error: err.message,
done: false,
retry: retryCount + 1,
});
// Add a tool-result message describing the error so the LLM can decide
// how to fix the arguments.
messages.push({
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: `error-${retryCount + 1}`,
result: err.message || String(err),
},
],
});
retryCount += 1;
continue; // try again with updated context
}
}
if (!result) {
throw new Error('Failed to generate a valid response after retries.');
}
// Convert result steps to ChatMessage objects & persist
if (memoryAdapter) {
try {
// Reconstruct the full exchange to be saved
const messagesToSave = [{ role: 'user', content: input }];
// Aggregate toolCalls and toolResults (SDK may expose them only inside steps)
const aggregatedToolCalls = (result.toolCalls && result.toolCalls.length > 0 ? result.toolCalls : []).concat((result.steps || [])
.flatMap((s) => (s.toolCalls ? s.toolCalls : [])));
const aggregatedToolResults = (result.toolResults && result.toolResults.length > 0 ? result.toolResults : []).concat((result.steps || [])
.flatMap((s) => (s.toolResults ? s.toolResults : [])));
// If there were tool calls, save them as separate assistant message
if (aggregatedToolCalls.length > 0) {
messagesToSave.push({
role: 'assistant',
content: aggregatedToolCalls.map((toolCall) => ({
type: 'tool-call',
toolCallId: toolCall.toolCallId || toolCall.id || toolCall.callId,
toolName: toolCall.toolName || toolCall.name,
args: toolCall.args || toolCall.arguments || toolCall.params,
})),
});
}
// If there were tool results, save them
if (aggregatedToolResults.length > 0) {
messagesToSave.push({
role: 'tool',
content: aggregatedToolResults.map((toolResult) => {
// Build tool-result part
const normalize = () => {
const raw = toolResult.result ?? toolResult.data ?? toolResult.output ?? '';
if (typeof raw === 'string') {
const trimmed = raw.trim();
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
return JSON.parse(trimmed);
}
catch { }
}
}
return raw;
};
return {
type: 'tool-result',
toolCallId: toolResult.toolCallId || toolResult.id || toolResult.callId,
toolName: toolResult.toolName || toolResult.name,
result: normalize(),
};
}),
});
}
// If there's a final text response, save it as separate assistant message
if (result.text) {
messagesToSave.push({ role: 'assistant', content: result.text });
}
await memoryAdapter.save(messagesToSave);
console.log(`💾 Saved ${messagesToSave.length} messages (including new turn).`);
}
catch (err) {
console.warn('❌ Failed to save conversation to memory:', err);
}
}
// Prepare output
returnData.push({
json: {
output: result.text,
steps: result.steps || [],
// Include debug information
totalSteps: result.steps?.length || 0,
},
});
}
catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: error.message },
pairedItem: { item: itemIndex },
});
continue;
}
throw error;
}
}
// After processing all items, flush OpenTelemetry spans so that traces are exported promptly (important for short-lived executions such as n8n worker tasks)
try {
const otelSdk = globalThis.__BAA_OTEL_INITIALIZED;
if (otelSdk && typeof otelSdk.forceFlush === 'function') {
await otelSdk.forceFlush();
console.log('💾 OpenTelemetry spans flushed');
}
}
catch (err) {
console.warn('❌ Failed to flush OpenTelemetry spans:', err);
}
return [returnData];
}
}
exports.BetterAiAgent = BetterAiAgent;