n8n
Version:
n8n Workflow Automation Tool
272 lines • 9.61 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExecutionRecorder = void 0;
const agents_1 = require("@n8n/agents");
const n8n_workflow_1 = require("n8n-workflow");
function workingMemoryContentFromInput(input) {
if (input && typeof input === 'object' && !Array.isArray(input)) {
const maybe = input.memory;
if (typeof maybe === 'string')
return maybe;
}
return JSON.stringify(input, null, 2);
}
function resolveFromAIInValue(value, llmArgs) {
if (typeof value === 'string')
return resolveFromAIInString(value, llmArgs);
if (Array.isArray(value))
return value.map((v) => resolveFromAIInValue(v, llmArgs));
if (value !== null && typeof value === 'object') {
const out = {};
for (const [k, v] of Object.entries(value)) {
out[k] = resolveFromAIInValue(v, llmArgs);
}
return out;
}
return value;
}
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;
}
});
}
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.workingMemory = 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':
if (chunk.toolName === agents_1.UPDATE_WORKING_MEMORY_TOOL_NAME) {
this.recordWorkingMemoryUpdate(workingMemoryContentFromInput(chunk.input));
}
else {
this.recordToolCall(chunk.toolCallId, chunk.toolName, chunk.input);
}
break;
case 'tool-result':
if (chunk.toolName === agents_1.UPDATE_WORKING_MEMORY_TOOL_NAME) {
break;
}
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,
timeline: this.timeline,
startTime: this.startTime,
duration: Date.now() - this.startTime,
error: this.error,
workingMemory: this.workingMemory,
};
}
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;
}
recordWorkingMemoryUpdate(content) {
this.flushTextBuffer();
this.workingMemory = content;
this.timeline.push({
type: 'working-memory',
content,
timestamp: Date.now(),
});
}
recordToolCall(toolCallId, name, input) {
this.flushTextBuffer();
this.toolCalls.push({ name, input, output: undefined });
const entry = this.registry.get(name);
const llmArgs = input !== null && typeof input === 'object' ? input : {};
const resolvedNodeParameters = entry?.nodeParameters !== undefined
? resolveFromAIInValue(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,
});
}
recordToolResult(toolCallId, name, output, isError) {
const pendingFlat = [...this.toolCalls]
.reverse()
.find((tc) => tc.name === name && tc.output === undefined);
if (pendingFlat) {
pendingFlat.output = output;
}
else {
this.toolCalls.push({ name, input: undefined, output });
}
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 = output;
pendingTimeline.endTime = Date.now();
pendingTimeline.success = !isError;
if (pendingTimeline.kind === 'workflow' && isRecord(output)) {
const execId = output.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,
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(output)) {
const execId = output.executionId;
if (typeof execId === 'string') {
synthesized.workflowExecutionId = execId;
}
}
this.timeline.push(synthesized);
}
}
exports.ExecutionRecorder = ExecutionRecorder;
//# sourceMappingURL=execution-recorder.js.map