n8n
Version:
n8n Workflow Automation Tool
301 lines • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExecutionRecorder = void 0;
const n8n_workflow_1 = require("n8n-workflow");
function resolveTemplatesInValue(value, llmArgs) {
if (typeof value === 'string')
return resolveTemplatesInString(value, llmArgs);
if (Array.isArray(value))
return value.map((v) => resolveTemplatesInValue(v, llmArgs));
if (value !== null && typeof value === 'object') {
const out = {};
for (const [k, v] of Object.entries(value)) {
out[k] = resolveTemplatesInValue(v, llmArgs);
}
return out;
}
return value;
}
function resolveTemplatesInString(str, llmArgs) {
const afterFromAI = resolveFromAIInString(str, llmArgs);
if (typeof afterFromAI !== 'string')
return afterFromAI;
return resolveJsonRefsInString(afterFromAI, llmArgs);
}
function resolveFromAIInString(str, llmArgs) {
if (!str.includes('$fromAI'))
return str;
let calls;
try {
calls = (0, n8n_workflow_1.extractFromAICalls)(str);
}
catch {
return str;
}
if (calls.length === 0)
return str;
if ((0, n8n_workflow_1.isFromAIOnlyExpression)(str)) {
const call = calls[0];
if (call.key in llmArgs)
return llmArgs[call.key];
if (call.defaultValue !== undefined)
return call.defaultValue;
return str;
}
const pattern = /\$fromAI\s*\([^)]*\)/g;
return str.replace(pattern, (match) => {
try {
const inner = (0, n8n_workflow_1.extractFromAICalls)(match);
if (inner.length === 0)
return match;
const call = inner[0];
const resolved = call.key in llmArgs
? llmArgs[call.key]
: call.defaultValue !== undefined
? call.defaultValue
: undefined;
if (resolved === undefined)
return match;
if (typeof resolved === 'object')
return JSON.stringify(resolved);
return String(resolved);
}
catch {
return match;
}
});
}
const FULL_JSON_REF_PATTERN = /^=\s*\{\{\s*\$json((?:\s*\.\s*[a-zA-Z_$][\w$]*)+)\s*\}\}\s*$/;
const INLINE_JSON_REF_PATTERN = /\{\{\s*\$json((?:\s*\.\s*[a-zA-Z_$][\w$]*)+)\s*\}\}/g;
function resolveJsonRefsInString(str, llmArgs) {
if (!str.startsWith('=') || !str.includes('$json'))
return str;
const fullMatch = str.match(FULL_JSON_REF_PATTERN);
if (fullMatch) {
const resolved = lookupJsonPath(llmArgs, fullMatch[1]);
return resolved === undefined ? str : resolved;
}
let replaced = false;
const out = str.replace(INLINE_JSON_REF_PATTERN, (match, path) => {
const resolved = lookupJsonPath(llmArgs, path);
if (resolved === undefined)
return match;
replaced = true;
if (typeof resolved === 'object')
return JSON.stringify(resolved);
return String(resolved);
});
return replaced ? out : str;
}
function lookupJsonPath(root, dottedPath) {
const segments = dottedPath
.split('.')
.map((s) => s.trim())
.filter((s) => s.length > 0);
let cur = root;
for (const seg of segments) {
if (cur === null || cur === undefined)
return undefined;
if (typeof cur !== 'object')
return undefined;
cur = cur[seg];
}
return cur;
}
function normaliseToolErrorOutput(output) {
if (output instanceof Error) {
return { error: output.message || output.name || 'Tool execution failed' };
}
if (typeof output === 'string') {
return { error: output };
}
return output;
}
function isRecord(v) {
return typeof v === 'object' && v !== null;
}
class ExecutionRecorder {
constructor(registry) {
this.textParts = [];
this.textBuffer = [];
this.model = null;
this.finishReason = 'unknown';
this.usage = null;
this.totalCost = null;
this.toolCalls = [];
this.timeline = [];
this.textStartTime = null;
this._suspended = false;
this.error = null;
this.startTime = Date.now();
this.registry = registry ?? new Map();
}
record(chunk) {
switch (chunk.type) {
case 'text-delta':
if (this.textStartTime === null)
this.textStartTime = Date.now();
this.textParts.push(chunk.delta);
this.textBuffer.push(chunk.delta);
break;
case 'tool-call':
this.recordToolCall(chunk.toolCallId, chunk.toolName, chunk.input);
break;
case 'tool-result':
this.recordToolResult(chunk.toolCallId, chunk.toolName, chunk.output, chunk.isError === true);
break;
case 'finish':
this.flushTextBuffer();
this.finishReason = chunk.finishReason;
if (chunk.usage) {
this.usage = {
promptTokens: chunk.usage.promptTokens,
completionTokens: chunk.usage.completionTokens,
totalTokens: chunk.usage.totalTokens,
};
}
this.model = chunk.model ?? null;
this.totalCost = chunk.totalCost ?? chunk.usage?.cost ?? null;
break;
case 'tool-call-suspended':
this.flushTextBuffer();
this._suspended = true;
this.timeline.push({
type: 'suspension',
toolName: chunk.toolName ?? '',
toolCallId: chunk.toolCallId ?? '',
timestamp: Date.now(),
});
break;
case 'error': {
const errMsg = chunk.error instanceof Error ? chunk.error.message : String(chunk.error);
this.error = errMsg;
break;
}
}
}
get suspended() {
return this._suspended;
}
getMessageRecord() {
this.flushTextBuffer();
return {
assistantResponse: this.textParts.join(''),
model: this.model,
finishReason: this.finishReason,
usage: this.usage,
totalCost: this.totalCost,
toolCalls: this.toolCalls.map(({ toolCallId: _toolCallId, ...toolCall }) => toolCall),
timeline: this.timeline,
startTime: this.startTime,
duration: Date.now() - this.startTime,
error: this.error,
};
}
flushTextBuffer() {
if (this.textBuffer.length === 0)
return;
const content = this.textBuffer.join('');
if (content.trim()) {
const now = Date.now();
this.timeline.push({
type: 'text',
content,
timestamp: this.textStartTime ?? now,
endTime: now,
});
}
this.textBuffer = [];
this.textStartTime = null;
}
recordToolCall(toolCallId, name, input) {
this.flushTextBuffer();
this.toolCalls.push({ name, input, output: undefined, toolCallId });
const entry = this.registry.get(name);
const llmArgs = input !== null && typeof input === 'object' ? input : {};
const resolvedNodeParameters = entry?.nodeParameters !== undefined
? resolveTemplatesInValue(entry.nodeParameters, llmArgs)
: undefined;
this.timeline.push({
type: 'tool-call',
kind: entry?.kind ?? 'tool',
name,
toolCallId,
input,
output: undefined,
startTime: Date.now(),
endTime: 0,
success: false,
workflowId: entry?.workflowId,
workflowName: entry?.workflowName,
triggerType: entry?.triggerType,
nodeType: entry?.nodeType,
nodeTypeVersion: entry?.nodeTypeVersion,
nodeDisplayName: entry?.nodeDisplayName,
nodeParameters: resolvedNodeParameters,
});
}
findOpenToolCall(toolCallId, name) {
if (toolCallId !== '') {
return this.toolCalls.find((tc) => tc.toolCallId === toolCallId && tc.output === undefined);
}
return [...this.toolCalls].reverse().find((tc) => tc.name === name && tc.output === undefined);
}
recordToolResult(toolCallId, name, output, isError) {
const recordedOutput = isError ? normaliseToolErrorOutput(output) : output;
const pendingFlat = this.findOpenToolCall(toolCallId, name);
if (pendingFlat) {
pendingFlat.output = recordedOutput;
}
else {
this.toolCalls.push({ name, input: undefined, output: recordedOutput });
}
const pendingTimeline = [...this.timeline]
.reverse()
.find((e) => e.type === 'tool-call' &&
(toolCallId ? e.toolCallId === toolCallId : e.name === name) &&
e.endTime === 0);
if (pendingTimeline) {
pendingTimeline.output = recordedOutput;
pendingTimeline.endTime = Date.now();
pendingTimeline.success = !isError;
if (pendingTimeline.kind === 'workflow' && isRecord(recordedOutput)) {
const execId = recordedOutput.executionId;
if (typeof execId === 'string') {
pendingTimeline.workflowExecutionId = execId;
}
}
return;
}
this.flushTextBuffer();
const entry = this.registry.get(name);
const now = Date.now();
const synthesized = {
type: 'tool-call',
kind: entry?.kind ?? 'tool',
name,
toolCallId,
input: undefined,
output: recordedOutput,
startTime: now,
endTime: now,
success: !isError,
workflowId: entry?.workflowId,
workflowName: entry?.workflowName,
triggerType: entry?.triggerType,
nodeType: entry?.nodeType,
nodeTypeVersion: entry?.nodeTypeVersion,
nodeDisplayName: entry?.nodeDisplayName,
nodeParameters: entry?.nodeParameters,
};
if (synthesized.kind === 'workflow' && isRecord(recordedOutput)) {
const execId = recordedOutput.executionId;
if (typeof execId === 'string') {
synthesized.workflowExecutionId = execId;
}
}
this.timeline.push(synthesized);
}
}
exports.ExecutionRecorder = ExecutionRecorder;
//# sourceMappingURL=execution-recorder.js.map