n8n
Version:
n8n Workflow Automation Tool
352 lines • 15.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstanceAiTerminalOutcomeService = void 0;
const instance_ai_1 = require("@n8n/instance-ai");
function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function getBackgroundOutcomeResponseId(outcome) {
return `background-outcome:${outcome.id}`;
}
function createTerminalOutcomeAgentTree(outcome, responseId) {
return {
agentId: (0, instance_ai_1.orchestratorAgentId)(outcome.runId),
role: 'orchestrator',
status: outcome.status === 'cancelled'
? 'cancelled'
: outcome.status === 'failed'
? 'error'
: 'completed',
textContent: outcome.userFacingMessage,
reasoning: '',
toolCalls: [],
children: [],
timeline: [{ type: 'text', content: outcome.userFacingMessage, responseId }],
};
}
function appendTerminalOutcomeToAgentTree(tree, outcome, responseId) {
const text = outcome.userFacingMessage.trim();
if (!text)
return { tree, appended: false };
const alreadyInTimeline = tree.timeline.some((entry) => entry.type === 'text' && entry.responseId === responseId);
if (alreadyInTimeline) {
return { tree, appended: false };
}
return {
appended: true,
tree: {
...tree,
textContent: tree.textContent ? `${tree.textContent}\n\n${outcome.userFacingMessage}` : text,
timeline: [
...tree.timeline,
{ type: 'text', content: outcome.userFacingMessage, responseId },
],
},
};
}
class InstanceAiTerminalOutcomeService {
constructor(options) {
this.pendingTerminalOutcomes = new Map();
this.eventBus = options.eventBus;
this.durableLog = options.durableLog;
this.dbSnapshotStorage = options.dbSnapshotStorage;
this.agentMemory = options.agentMemory;
this.telemetry = options.telemetry;
this.logger = options.logger;
this.runState = options.runState;
this.suspendedThreads = options.suspendedThreads;
this.tracing = options.tracing;
this.publishRunFinish = options.publishRunFinish;
this.saveAgentTreeSnapshot = options.saveAgentTreeSnapshot;
}
async evaluateTerminalResponse(threadId, runId, status, options = {}) {
const guard = new instance_ai_1.InstanceAiTerminalResponseGuard({
runId,
rootAgentId: (0, instance_ai_1.orchestratorAgentId)(runId),
messageGroupId: options.messageGroupId,
correlationId: options.correlationId,
});
const decision = guard.evaluateTerminal(await this.getTerminalGuardEvents(threadId, runId, options.messageGroupId), status, {
workSummary: options.workSummary,
errorMessage: options.errorMessage,
errorCode: options.errorCode,
suppressCompletedFallback: options.suppressCompletedFallback,
});
this.handleTerminalResponseDecision(threadId, runId, decision, options.messageGroupId);
return decision;
}
async evaluateWaitingResponse(threadId, runId, confirmationEvent, options = {}) {
const guard = new instance_ai_1.InstanceAiTerminalResponseGuard({
runId,
rootAgentId: (0, instance_ai_1.orchestratorAgentId)(runId),
messageGroupId: options.messageGroupId,
correlationId: options.correlationId,
});
const decision = guard.evaluateWaiting(await this.getTerminalGuardEvents(threadId, runId, options.messageGroupId), confirmationEvent);
this.handleTerminalResponseDecision(threadId, runId, decision, options.messageGroupId);
return decision;
}
async getTerminalGuardEvents(threadId, runId, messageGroupId) {
if (!messageGroupId)
return await this.eventBus.getEventsForRun(threadId, runId);
const groupRunIds = this.runState.getRunIdsForMessageGroup(messageGroupId);
return groupRunIds.length > 0
? await this.eventBus.getEventsForRuns(threadId, groupRunIds)
: await this.eventBus.getEventsForRun(threadId, runId);
}
handleTerminalResponseDecision(threadId, runId, decision, messageGroupId) {
this.telemetry.track('instance_ai_terminal_response_decision', {
thread_id: threadId,
run_id: runId,
message_group_id: messageGroupId,
source: 'terminal_guard',
status: decision.status,
action: decision.action,
reason: decision.reason,
visibility_source: decision.visibilitySource,
});
if (decision.reason === 'completed-after-error') {
this.logger.warn('completed_after_error_event', {
threadId,
runId,
messageGroupId,
});
}
if (decision.reason === 'confirmation-invalid') {
this.logger.warn('invalid_confirmation_payload', {
threadId,
runId,
messageGroupId,
});
}
if (decision.action === 'emit' && decision.event) {
this.eventBus.publish(threadId, decision.event);
}
}
createTerminalOutcomeStorage() {
this.terminalOutcomeStorage ??= new instance_ai_1.TerminalOutcomeStorage(this.agentMemory);
return this.terminalOutcomeStorage;
}
async finishInvalidConfirmationRun(args) {
this.runState.cancelThread(args.threadId);
void this.suspendedThreads.dropPendingConfirmationsForThread(args.threadId);
args.abortController.abort();
await this.tracing.finalizeRunTracing(args.runId, args.tracing, {
status: 'error',
reason: 'invalid_confirmation_payload',
});
this.publishRunFinish(args.threadId, args.runId, 'errored', 'I need your input to continue, but I could not display the prompt. Please try again.');
await this.saveAgentTreeSnapshot(args.threadId, args.runId, args.snapshotStorage);
return {
status: 'error',
reason: 'invalid_confirmation_payload',
metadata: this.tracing.buildMessageTraceMetadata(args.threadId, args.runId, {
status: 'error',
}),
};
}
buildBackgroundTerminalOutcome(task) {
const status = task.status === 'failed' ? 'failed' : task.status === 'cancelled' ? 'cancelled' : 'completed';
const userFacingMessage = status === 'completed'
? `The background ${task.role} task finished.`
: status === 'cancelled'
? `The background ${task.role} task was cancelled.`
: `The background ${task.role} task failed before I could complete that part.`;
return {
id: `${task.messageGroupId ?? task.runId}:${task.taskId}:${status}`,
threadId: task.threadId,
runId: task.runId,
messageGroupId: task.messageGroupId,
correlationId: task.messageGroupId,
taskId: task.taskId,
agentId: task.agentId,
status,
userFacingMessage,
createdAt: new Date().toISOString(),
};
}
async replayUndeliveredTerminalOutcomes(threadId, options = {}) {
const storage = this.createTerminalOutcomeStorage();
const noOutcomes = [];
const persistedOutcomes = await storage.getUndelivered(threadId).catch((error) => {
this.logger.warn('Failed to load undelivered Instance AI terminal outcomes', {
threadId,
error: getErrorMessage(error),
});
return noOutcomes;
});
const inMemoryOutcomes = [...this.pendingTerminalOutcomes.values()].filter((outcome) => outcome.threadId === threadId);
const outcomes = new Map();
for (const outcome of [...persistedOutcomes, ...inMemoryOutcomes]) {
outcomes.set(outcome.id, outcome);
}
const persistedOutcomeIds = new Set(persistedOutcomes.map((outcome) => outcome.id));
const delivery = options.delivery ?? 'snapshot';
for (const outcome of outcomes.values()) {
const responseId = getBackgroundOutcomeResponseId(outcome);
let snapshotDelivered = false;
try {
snapshotDelivered = await this.persistTerminalOutcomeLineToSnapshot(outcome, responseId);
}
catch (error) {
this.logger.warn('Failed to replay Instance AI terminal outcome', {
threadId,
runId: outcome.runId,
taskId: outcome.taskId,
error: getErrorMessage(error),
});
if (delivery === 'event') {
const published = await this.publishTerminalOutcomeLine(outcome, responseId);
this.telemetry.track('instance_ai_terminal_response_decision', {
thread_id: threadId,
run_id: outcome.runId,
message_group_id: outcome.messageGroupId,
task_id: outcome.taskId,
source: 'terminal_outcome_replay',
status: outcome.status,
action: published ? 'replay_event' : 'already-emitted',
visibility_source: 'background-outcome',
});
}
continue;
}
if (!snapshotDelivered)
continue;
let action = 'replay_snapshot';
if (delivery === 'event') {
const published = await this.publishTerminalOutcomeLine(outcome, responseId);
action = published ? 'replay_event' : 'already-emitted';
}
if (persistedOutcomeIds.has(outcome.id)) {
await storage
.markDelivered(threadId, outcome.id, new Date().toISOString())
.catch((error) => {
this.logger.warn('Failed to mark Instance AI terminal outcome as delivered', {
threadId,
runId: outcome.runId,
taskId: outcome.taskId,
error: getErrorMessage(error),
});
});
}
this.pendingTerminalOutcomes.delete(outcome.id);
this.telemetry.track('instance_ai_terminal_response_decision', {
thread_id: threadId,
run_id: outcome.runId,
message_group_id: outcome.messageGroupId,
task_id: outcome.taskId,
source: 'terminal_outcome_replay',
status: outcome.status,
action,
visibility_source: 'background-outcome',
});
}
}
async persistTerminalOutcomeLineToSnapshot(outcome, responseId) {
const snapshot = await this.dbSnapshotStorage.getLatest(outcome.threadId, {
messageGroupId: outcome.messageGroupId,
runId: outcome.runId,
});
if (!snapshot) {
await this.dbSnapshotStorage.save(outcome.threadId, createTerminalOutcomeAgentTree(outcome, responseId), outcome.runId, {
messageGroupId: outcome.messageGroupId,
runIds: [outcome.runId],
});
return true;
}
const { tree } = appendTerminalOutcomeToAgentTree(snapshot.tree, outcome, responseId);
const runIds = new Set(snapshot.runIds ?? [snapshot.runId]);
runIds.add(outcome.runId);
await this.dbSnapshotStorage.updateLast(outcome.threadId, tree, snapshot.runId, {
messageGroupId: snapshot.messageGroupId ?? outcome.messageGroupId,
runIds: [...runIds],
langsmithRunId: snapshot.langsmithRunId,
langsmithTraceId: snapshot.langsmithTraceId,
});
return true;
}
async publishTerminalOutcomeLine(outcome, responseId) {
const alreadyPublished = (await this.eventBus.getEventsForRun(outcome.threadId, outcome.runId)).some((event) => event.responseId === responseId);
if (alreadyPublished)
return false;
this.eventBus.publish(outcome.threadId, {
type: this.durableLog ? 'text-block' : 'text-delta',
runId: outcome.runId,
agentId: (0, instance_ai_1.orchestratorAgentId)(outcome.runId),
responseId,
payload: { text: outcome.userFacingMessage },
});
return true;
}
async recordBackgroundTerminalOutcome(task) {
const outcome = this.buildBackgroundTerminalOutcome(task);
let persisted = false;
try {
await this.createTerminalOutcomeStorage().upsert(task.threadId, outcome);
persisted = true;
}
catch (error) {
this.pendingTerminalOutcomes.set(outcome.id, outcome);
this.logger.warn('Failed to persist Instance AI terminal outcome', {
threadId: task.threadId,
runId: task.runId,
taskId: task.taskId,
error: getErrorMessage(error),
});
this.telemetry.track('instance_ai_terminal_outcome_persistence_failure', {
thread_id: task.threadId,
run_id: task.runId,
task_id: task.taskId,
status: outcome.status,
phase: 'metadata',
});
}
const responseId = getBackgroundOutcomeResponseId(outcome);
const published = await this.publishTerminalOutcomeLine(outcome, responseId);
this.telemetry.track('instance_ai_terminal_response_decision', {
thread_id: task.threadId,
run_id: task.runId,
message_group_id: task.messageGroupId,
task_id: task.taskId,
source: 'background_outcome',
status: outcome.status,
action: published ? 'emit' : 'already-emitted',
visibility_source: 'background-outcome',
});
let snapshotDelivered = false;
try {
snapshotDelivered = await this.persistTerminalOutcomeLineToSnapshot(outcome, responseId);
}
catch (error) {
this.logger.warn('Failed to persist Instance AI terminal outcome line to snapshot', {
threadId: task.threadId,
runId: task.runId,
taskId: task.taskId,
error: getErrorMessage(error),
});
this.telemetry.track('instance_ai_terminal_outcome_persistence_failure', {
thread_id: task.threadId,
run_id: task.runId,
task_id: task.taskId,
status: outcome.status,
phase: 'snapshot',
});
}
if (!persisted || !snapshotDelivered)
return;
try {
await this.createTerminalOutcomeStorage().markDelivered(task.threadId, outcome.id, new Date().toISOString());
this.pendingTerminalOutcomes.delete(outcome.id);
}
catch (error) {
this.logger.warn('Failed to mark Instance AI terminal outcome as delivered', {
threadId: task.threadId,
runId: task.runId,
taskId: task.taskId,
error: getErrorMessage(error),
});
}
}
}
exports.InstanceAiTerminalOutcomeService = InstanceAiTerminalOutcomeService;
//# sourceMappingURL=instance-ai-terminal-outcome.service.js.map