diff --git a/packages/@n8n/instance-ai/src/__tests__/index.test.ts b/packages/@n8n/instance-ai/src/__tests__/index.test.ts
new file mode 100644
index 00000000000..266aae8a885
--- /dev/null
+++ b/packages/@n8n/instance-ai/src/__tests__/index.test.ts
@@ -0,0 +1,393 @@
+type Callable = (...args: never[]) => unknown;
+type Constructable = new (...args: never[]) => unknown;
+
+const call = (value: unknown) => (value as Callable)();
+const construct = (value: unknown) => new (value as Constructable)();
+
+vi.mock('../tracing/langsmith-tracing', () => ({
+ appendGeneratedWorkflowIdToRootMetadata: () => 'appendGeneratedWorkflowIdToRootMetadata',
+ appendRootRunMetadata: () => 'appendRootRunMetadata',
+ createInstanceAiTraceContext: () => 'createInstanceAiTraceContext',
+ createInternalOperationTraceContext: () => 'createInternalOperationTraceContext',
+ createTraceReplayOnlyContext: () => 'createTraceReplayOnlyContext',
+ continueInstanceAiTraceContext: () => 'continueInstanceAiTraceContext',
+ releaseTraceClient: () => 'releaseTraceClient',
+ submitLangsmithUserFeedback: () => 'submitLangsmithUserFeedback',
+ withCurrentTraceSpan: () => 'withCurrentTraceSpan',
+}));
+
+vi.mock('../tracing/trace-replay', () => {
+ class IdRemapper {
+ remapInput(input: unknown) {
+ return input;
+ }
+ }
+ class TraceIndex {}
+ class TraceWriter {}
+ return {
+ IdRemapper,
+ TraceIndex,
+ TraceWriter,
+ parseTraceJsonl: () => [],
+ PURE_REPLAY_TOOLS: new Set(['web-search']),
+ };
+});
+
+vi.mock('../agent/instance-agent', () => ({ createInstanceAgent: () => 'instance-agent' }));
+vi.mock('../domain-access', () => ({
+ createDomainAccessTracker: () => ({ type: 'domain-access-tracker' }),
+}));
+vi.mock('../agent/sub-agent-factory', () => ({ createSubAgent: () => 'sub-agent' }));
+vi.mock('../tools/web-research/sanitize-web-content', () => ({
+ wrapUntrustedData: (content: string, source: string) =>
+ `${content}`,
+}));
+vi.mock('../tools/orchestration/delegate.tool', () => ({
+ startDetachedDelegateTask: () => 'delegate-task',
+}));
+vi.mock('../tools', () => ({
+ createAllTools: () => ['all-tools'],
+ createOrchestrationTools: () => ['orchestration-tools'],
+}));
+vi.mock('../tools/orchestration/agent-persistence', () => ({
+ SUB_AGENT_RESOURCE_PREFIX: 'instance-ai-subagent',
+ createSubAgentResourceId: (threadId: string, kind: string) =>
+ `instance-ai-subagent:${threadId}:${kind.toLowerCase().replace(/\s+/g, '-')}`,
+ createSubAgentResourceIdPrefix: (threadId: string) => `instance-ai-subagent:${threadId}:`,
+}));
+vi.mock('../memory/title-utils', () => ({
+ truncateToTitle: (title: string) => title,
+ generateTitleForRun: () => 'generated-title',
+}));
+vi.mock('../mcp/mcp-client-manager', () => ({ McpClientManager: class McpClientManager {} }));
+vi.mock('../utils/stream-helpers', () => ({
+ isRecord: (value: unknown) =>
+ value !== null && typeof value === 'object' && !Array.isArray(value),
+ parseSuspension: () => ({
+ toolCallId: 'tool-call-1',
+ requestId: 'request-1',
+ suspendPayload: { requestId: 'request-1' },
+ }),
+ asResumable: (agent: unknown) => agent,
+}));
+vi.mock('../storage', () => ({
+ iterationEntrySchema: { safeParse: () => ({ success: true }) },
+ formatPreviousAttempts: () => 'previous attempts',
+ ThreadIterationLogStorage: class ThreadIterationLogStorage {},
+ ThreadTaskStorage: class ThreadTaskStorage {},
+ PlannedTaskStorage: class PlannedTaskStorage {},
+ getThread: () => ({ id: 'thread-1' }),
+ TerminalOutcomeStorage: class TerminalOutcomeStorage {},
+ patchThread: () => ({ id: 'thread-1' }),
+ WorkflowLoopStorage: class WorkflowLoopStorage {},
+}));
+vi.mock('../stream/map-chunk', () => ({ mapAgentChunkToEvent: () => ({ type: 'event' }) }));
+vi.mock('../skills/runtime-skills', () => ({
+ INSTANCE_AI_SKILLS_DIR: '/instance-ai-skills',
+ hasRuntimeSkills: () => true,
+ loadInstanceAiRuntimeSkillSource: () => 'runtime-skill-source',
+}));
+vi.mock('../skills/materialize-runtime-skills', () => ({
+ SANDBOX_RUNTIME_SKILLS_DIR: '/sandbox-skills',
+ SANDBOX_RUNTIME_SKILL_REGISTRY_FILE: 'registry.json',
+ RUNTIME_SKILL_MANIFEST_FILE: 'manifest.json',
+ RUNTIME_SKILL_MANIFEST_SCHEMA_VERSION: 1,
+ N8N_SKILLS_DIR_ENV: 'N8N_SKILLS_DIR',
+ N8N_SKILL_DIR_ENV: 'N8N_SKILL_DIR',
+ N8N_WORKSPACE_DIR_ENV: 'N8N_WORKSPACE_DIR',
+ createLazyWorkspaceRuntimeSkillSource: () => 'lazy-skill-source',
+ buildRuntimeSkillWorkspaceBundle: () => ({ manifest: [] }),
+ materializeRuntimeSkillsIntoWorkspace: () => undefined,
+ loadPrebakedRuntimeSkillsBundle: () => ({ manifest: [] }),
+}));
+vi.mock('../utils/eval-agents', () => ({
+ SONNET_MODEL: 'sonnet',
+ HAIKU_MODEL: 'haiku',
+ createEvalAgent: () => 'eval-agent',
+ extractText: () => 'text',
+ Tool: class Tool {},
+}));
+vi.mock('../utils/agent-tree', () => ({
+ buildAgentTreeFromEvents: () => ({ agentId: 'root', children: [] }),
+ findAgentNodeInTree: () => ({ agentId: 'root', children: [] }),
+}));
+vi.mock('../workspace/builder-templates-service', () => ({
+ BuilderTemplatesService: class BuilderTemplatesService {},
+ builderTemplatesOptionsFromEnv: () => ({ enabled: true }),
+}));
+vi.mock('../workspace/create-workspace', () => ({
+ createSandbox: () => ({ type: 'sandbox' }),
+ createWorkspace: () => ({ type: 'workspace' }),
+}));
+vi.mock('../workspace/lazy-runtime-workspace', () => ({
+ createLazyRuntimeWorkspace: () => ({ type: 'lazy-workspace' }),
+}));
+vi.mock('../workspace/sandbox-setup', () => ({
+ getWorkspaceRoot: () => '/workspace',
+ setupSandboxWorkspace: () => undefined,
+}));
+vi.mock('../workspace/snapshot-manager', () => ({ SnapshotManager: class SnapshotManager {} }));
+vi.mock('../runtime/background-task-manager', () => ({
+ BackgroundTaskManager: class BackgroundTaskManager {},
+ enrichMessageWithRunningTasks: () => ({ content: 'message' }),
+}));
+vi.mock('../runtime/run-state-registry', () => ({ RunStateRegistry: class RunStateRegistry {} }));
+vi.mock('../runtime/terminal-response-guard', () => ({
+ InstanceAiTerminalResponseGuard: class InstanceAiTerminalResponseGuard {},
+}));
+vi.mock('../runtime/resumable-stream-executor', () => ({
+ executeResumableStream: () => ({ status: 'done' }),
+}));
+vi.mock('../runtime/stream-runner', () => ({
+ resumeAgentRun: () => ({ status: 'resumed' }),
+ streamAgentRun: () => ({ status: 'streamed' }),
+}));
+vi.mock('../runtime/liveness-policy', () => {
+ const INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG = {
+ confirmationTimeoutMs: 1,
+ backgroundTaskIdleTimeoutMs: 1,
+ backgroundTaskMaxLifetimeMs: 1,
+ activeRunIdleTimeoutMs: 0,
+ activeRunMaxLifetimeMs: 1,
+ };
+ class InstanceAiLivenessPolicy {
+ hasEnabledTimeouts() {
+ return true;
+ }
+ }
+ return {
+ INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG,
+ createInstanceAiLivenessPolicyConfig: () => INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG,
+ InstanceAiLivenessPolicy,
+ };
+});
+vi.mock('../workflow-loop', () => ({
+ workflowBuildOutcomeSchema: { safeParse: () => ({ success: true }) },
+ attemptRecordSchema: { safeParse: () => ({ success: false }) },
+ workflowLoopStateSchema: { parse: (value: unknown) => value },
+ verificationResultSchema: { safeParse: () => ({ success: true }) },
+ createWorkItem: () => ({ workItemId: 'work-item-1' }),
+ formatWorkflowLoopGuidance: () => 'guidance',
+ handleBuildOutcome: () => ({ action: { type: 'verify' } }),
+ handleVerificationVerdict: () => ({ action: { type: 'done' } }),
+ formatAttemptHistory: () => 'attempt history',
+ WorkflowTaskCoordinator: class WorkflowTaskCoordinator {},
+}));
+vi.mock('../workflow-loop/runtime', () => ({
+ WorkflowLoopRuntime: class WorkflowLoopRuntime {},
+}));
+vi.mock('../planned-tasks/planned-task-service', () => ({
+ PlannedTaskCoordinator: class PlannedTaskCoordinator {},
+}));
+vi.mock('../planned-tasks/planned-task-permissions', () => ({
+ PLANNED_TASK_PERMISSION_OVERRIDES: { checkpoint: { runWorkflow: 'always_allow' } },
+ applyPlannedTaskPermissions: (context: { permissions: Record }) => ({
+ ...context,
+ permissions: { ...context.permissions, runWorkflow: 'always_allow' },
+ }),
+}));
+vi.mock('../parsers/structured-file-parser', () => ({
+ classifyAttachments: () => [{ index: 0, parseable: true, format: 'csv' }],
+ buildAttachmentManifest: () => '[ATTACHMENTS] parse-file [/ATTACHMENTS]',
+ isStructuredAttachment: () => true,
+ isParseableAttachment: () => true,
+}));
+vi.mock('../parsers/validate-attachments', () => {
+ class UnsupportedAttachmentError extends Error {}
+ return {
+ getParseableAttachmentMimeTypes: () => ['text/csv'],
+ getSupportedAttachmentMimeTypes: () => ['text/csv', 'image/*'],
+ isSupportedAttachmentMimeType: (mimeType: string) =>
+ mimeType === 'text/csv' || mimeType.startsWith('image/'),
+ validateAttachmentMimeTypes: () => {
+ throw new UnsupportedAttachmentError();
+ },
+ UnsupportedAttachmentError,
+ };
+});
+
+describe('@n8n/instance-ai public entrypoint', () => {
+ it('exposes representative lazy exports without invoking them', async () => {
+ const entrypoint = await import('../index');
+
+ expect(entrypoint.MAX_STEPS.ORCHESTRATOR).toBeGreaterThan(0);
+ expect(entrypoint.createAllTools).toEqual(expect.any(Function));
+ expect(entrypoint.createInstanceAgent).toEqual(expect.any(Function));
+ expect(entrypoint.createLazyRuntimeWorkspace).toEqual(expect.any(Function));
+ expect(entrypoint.createWorkItem).toEqual(expect.any(Function));
+ expect(entrypoint.getParseableAttachmentMimeTypes).toEqual(expect.any(Function));
+ expect(entrypoint.mapAgentChunkToEvent).toEqual(expect.any(Function));
+ expect(entrypoint.parseTraceJsonl).toEqual(expect.any(Function));
+ expect(entrypoint.wrapUntrustedData).toEqual(expect.any(Function));
+ expect(entrypoint.BackgroundTaskManager).toEqual(expect.any(Function));
+ expect(entrypoint.IdRemapper).toEqual(expect.any(Function));
+ expect(entrypoint.McpClientManager).toEqual(expect.any(Function));
+ expect(entrypoint.UnsupportedAttachmentError).toEqual(expect.any(Function));
+ expect(entrypoint.WorkflowLoopRuntime).toEqual(expect.any(Function));
+ });
+
+ it('loads lazy functions, classes, and getters through the public barrel', async () => {
+ const entrypoint = await import('../index');
+
+ expect(call(entrypoint.appendGeneratedWorkflowIdToRootMetadata)).toBe(
+ 'appendGeneratedWorkflowIdToRootMetadata',
+ );
+ expect(call(entrypoint.appendRootRunMetadata)).toBe('appendRootRunMetadata');
+ expect(call(entrypoint.createInstanceAiTraceContext)).toBe('createInstanceAiTraceContext');
+ expect(call(entrypoint.createInternalOperationTraceContext)).toBe(
+ 'createInternalOperationTraceContext',
+ );
+ expect(call(entrypoint.createTraceReplayOnlyContext)).toBe('createTraceReplayOnlyContext');
+ expect(call(entrypoint.continueInstanceAiTraceContext)).toBe('continueInstanceAiTraceContext');
+ expect(call(entrypoint.releaseTraceClient)).toBe('releaseTraceClient');
+ expect(call(entrypoint.submitLangsmithUserFeedback)).toBe('submitLangsmithUserFeedback');
+ expect(call(entrypoint.withCurrentTraceSpan)).toBe('withCurrentTraceSpan');
+
+ expect(call(entrypoint.createInstanceAgent)).toBe('instance-agent');
+ expect(call(entrypoint.createDomainAccessTracker)).toEqual({ type: 'domain-access-tracker' });
+ expect(call(entrypoint.createSubAgent)).toBe('sub-agent');
+ expect(entrypoint.wrapUntrustedData('hello', 'https://example.com')).toContain(
+ '',
+ );
+ expect(call(entrypoint.startDetachedDelegateTask)).toBe('delegate-task');
+ expect(call(entrypoint.createAllTools)).toEqual(['all-tools']);
+ expect(call(entrypoint.createOrchestrationTools)).toEqual(['orchestration-tools']);
+
+ expect(entrypoint.PURE_REPLAY_TOOLS.has('web-search')).toBe(true);
+ expect(entrypoint.createSubAgentResourceId('thread-1', 'Research Agent')).toBe(
+ 'instance-ai-subagent:thread-1:research-agent',
+ );
+ expect(entrypoint.createSubAgentResourceIdPrefix('thread-1')).toBe(
+ 'instance-ai-subagent:thread-1:',
+ );
+ expect(entrypoint.SUB_AGENT_RESOURCE_PREFIX).toBe('instance-ai-subagent');
+
+ const remapper = new entrypoint.IdRemapper();
+ expect(remapper.remapInput({ id: 'old-id' })).toEqual({ id: 'old-id' });
+ expect(remapper).toBeInstanceOf(entrypoint.IdRemapper);
+ expect(construct(entrypoint.TraceIndex)).toBeInstanceOf(entrypoint.TraceIndex);
+ expect(construct(entrypoint.TraceWriter)).toBeInstanceOf(entrypoint.TraceWriter);
+ expect(call(entrypoint.parseTraceJsonl)).toEqual([]);
+
+ expect(call(entrypoint.hasRuntimeSkills)).toBe(true);
+ expect(call(entrypoint.loadInstanceAiRuntimeSkillSource)).toBe('runtime-skill-source');
+ expect(call(entrypoint.createLazyWorkspaceRuntimeSkillSource)).toBe('lazy-skill-source');
+ expect(call(entrypoint.buildRuntimeSkillWorkspaceBundle)).toEqual({ manifest: [] });
+ expect(call(entrypoint.materializeRuntimeSkillsIntoWorkspace)).toBeUndefined();
+ expect(call(entrypoint.loadPrebakedRuntimeSkillsBundle)).toEqual({ manifest: [] });
+ expect(entrypoint.INSTANCE_AI_SKILLS_DIR).toBe('/instance-ai-skills');
+ expect(entrypoint.SANDBOX_RUNTIME_SKILLS_DIR).toBe('/sandbox-skills');
+ expect(entrypoint.SANDBOX_RUNTIME_SKILL_REGISTRY_FILE).toBe('registry.json');
+ expect(entrypoint.RUNTIME_SKILL_MANIFEST_FILE).toBe('manifest.json');
+ expect(entrypoint.RUNTIME_SKILL_MANIFEST_SCHEMA_VERSION).toBe(1);
+ expect(entrypoint.N8N_SKILLS_DIR_ENV).toBe('N8N_SKILLS_DIR');
+ expect(entrypoint.N8N_SKILL_DIR_ENV).toBe('N8N_SKILL_DIR');
+ expect(entrypoint.N8N_WORKSPACE_DIR_ENV).toBe('N8N_WORKSPACE_DIR');
+
+ expect(call(entrypoint.formatPreviousAttempts)).toBe('previous attempts');
+ expect(construct(entrypoint.ThreadIterationLogStorage)).toBeInstanceOf(
+ entrypoint.ThreadIterationLogStorage,
+ );
+ expect(construct(entrypoint.ThreadTaskStorage)).toBeInstanceOf(entrypoint.ThreadTaskStorage);
+ expect(construct(entrypoint.PlannedTaskStorage)).toBeInstanceOf(entrypoint.PlannedTaskStorage);
+ expect(call(entrypoint.getThread)).toEqual({ id: 'thread-1' });
+ expect(construct(entrypoint.TerminalOutcomeStorage)).toBeInstanceOf(
+ entrypoint.TerminalOutcomeStorage,
+ );
+ expect(call(entrypoint.patchThread)).toEqual({ id: 'thread-1' });
+ expect(construct(entrypoint.WorkflowLoopStorage)).toBeInstanceOf(
+ entrypoint.WorkflowLoopStorage,
+ );
+ expect(entrypoint.iterationEntrySchema.safeParse({}).success).toBe(true);
+
+ expect(call(entrypoint.truncateToTitle)).toBeUndefined();
+ expect(call(entrypoint.generateTitleForRun)).toBe('generated-title');
+ expect(construct(entrypoint.McpClientManager)).toBeInstanceOf(entrypoint.McpClientManager);
+ expect(call(entrypoint.mapAgentChunkToEvent)).toEqual({ type: 'event' });
+ expect(entrypoint.isRecord({ ok: true })).toBe(true);
+ expect(entrypoint.parseSuspension({})).toEqual({
+ toolCallId: 'tool-call-1',
+ requestId: 'request-1',
+ suspendPayload: { requestId: 'request-1' },
+ });
+ expect(entrypoint.asResumable({ resume: true })).toEqual({ resume: true });
+
+ expect(call(entrypoint.createEvalAgent)).toBe('eval-agent');
+ expect(call(entrypoint.extractText)).toBe('text');
+ expect(construct(entrypoint.Tool)).toBeInstanceOf(entrypoint.Tool);
+ expect(entrypoint.SONNET_MODEL).toBe('sonnet');
+ expect(entrypoint.HAIKU_MODEL).toBe('haiku');
+ expect(call(entrypoint.buildAgentTreeFromEvents)).toEqual({ agentId: 'root', children: [] });
+ expect(call(entrypoint.findAgentNodeInTree)).toEqual({ agentId: 'root', children: [] });
+
+ expect(call(entrypoint.createLazyRuntimeWorkspace)).toEqual({ type: 'lazy-workspace' });
+ expect(call(entrypoint.getWorkspaceRoot)).toBe('/workspace');
+ expect(call(entrypoint.setupSandboxWorkspace)).toBeUndefined();
+ expect(construct(entrypoint.BuilderTemplatesService)).toBeInstanceOf(
+ entrypoint.BuilderTemplatesService,
+ );
+ expect(call(entrypoint.builderTemplatesOptionsFromEnv)).toEqual({ enabled: true });
+ expect(call(entrypoint.createSandbox)).toEqual({ type: 'sandbox' });
+ expect(call(entrypoint.createWorkspace)).toEqual({ type: 'workspace' });
+ expect(construct(entrypoint.SnapshotManager)).toBeInstanceOf(entrypoint.SnapshotManager);
+
+ expect(construct(entrypoint.BackgroundTaskManager)).toBeInstanceOf(
+ entrypoint.BackgroundTaskManager,
+ );
+ expect(call(entrypoint.enrichMessageWithRunningTasks)).toEqual({ content: 'message' });
+ expect(call(entrypoint.enrichMessageWithBackgroundTasks)).toEqual({ content: 'message' });
+ expect(construct(entrypoint.RunStateRegistry)).toBeInstanceOf(entrypoint.RunStateRegistry);
+ expect(construct(entrypoint.InstanceAiTerminalResponseGuard)).toBeInstanceOf(
+ entrypoint.InstanceAiTerminalResponseGuard,
+ );
+ expect(call(entrypoint.executeResumableStream)).toEqual({ status: 'done' });
+ expect(call(entrypoint.resumeAgentRun)).toEqual({ status: 'resumed' });
+ expect(call(entrypoint.streamAgentRun)).toEqual({ status: 'streamed' });
+
+ expect(call(entrypoint.createInstanceAiLivenessPolicyConfig)).toEqual(
+ entrypoint.INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG,
+ );
+ expect(construct(entrypoint.InstanceAiLivenessPolicy)).toBeInstanceOf(
+ entrypoint.InstanceAiLivenessPolicy,
+ );
+ expect(call(entrypoint.createWorkItem)).toEqual({ workItemId: 'work-item-1' });
+ expect(call(entrypoint.formatWorkflowLoopGuidance)).toBe('guidance');
+ expect(call(entrypoint.handleBuildOutcome)).toEqual({ action: { type: 'verify' } });
+ expect(call(entrypoint.handleVerificationVerdict)).toEqual({ action: { type: 'done' } });
+ expect(call(entrypoint.formatAttemptHistory)).toBe('attempt history');
+ expect(construct(entrypoint.WorkflowTaskCoordinator)).toBeInstanceOf(
+ entrypoint.WorkflowTaskCoordinator,
+ );
+ expect(entrypoint.workflowBuildOutcomeSchema.safeParse({}).success).toBe(true);
+ expect(entrypoint.attemptRecordSchema.safeParse({}).success).toBe(false);
+ expect(entrypoint.workflowLoopStateSchema.parse({ workItemId: 'work-item-1' })).toEqual({
+ workItemId: 'work-item-1',
+ });
+ expect(entrypoint.verificationResultSchema.safeParse({}).success).toBe(true);
+ expect(construct(entrypoint.WorkflowLoopRuntime)).toBeInstanceOf(
+ entrypoint.WorkflowLoopRuntime,
+ );
+ expect(construct(entrypoint.PlannedTaskCoordinator)).toBeInstanceOf(
+ entrypoint.PlannedTaskCoordinator,
+ );
+ expect(
+ entrypoint.applyPlannedTaskPermissions({ permissions: {} } as never, 'checkpoint')
+ .permissions!.runWorkflow,
+ ).toBe('always_allow');
+ expect(entrypoint.PLANNED_TASK_PERMISSION_OVERRIDES.checkpoint!.runWorkflow).toBe(
+ 'always_allow',
+ );
+
+ const classified = entrypoint.classifyAttachments([]);
+ expect(classified[0]).toMatchObject({ index: 0, parseable: true, format: 'csv' });
+ expect(entrypoint.buildAttachmentManifest(classified)).toContain('parse-file');
+ expect(entrypoint.isStructuredAttachment({} as never)).toBe(true);
+ expect(entrypoint.isParseableAttachment({} as never)).toBe(true);
+ expect(entrypoint.getParseableAttachmentMimeTypes()).toContain('text/csv');
+ expect(entrypoint.getSupportedAttachmentMimeTypes()).toContain('image/*');
+ expect(entrypoint.isSupportedAttachmentMimeType('image/png')).toBe(true);
+ expect(() => entrypoint.validateAttachmentMimeTypes([])).toThrow(
+ entrypoint.UnsupportedAttachmentError,
+ );
+ });
+});
diff --git a/packages/@n8n/instance-ai/src/index.ts b/packages/@n8n/instance-ai/src/index.ts
index ebffa650f85..daac822adb4 100644
--- a/packages/@n8n/instance-ai/src/index.ts
+++ b/packages/@n8n/instance-ai/src/index.ts
@@ -3,15 +3,39 @@ import './source-map-filter';
import type * as InstanceAgentMod from './agent/instance-agent';
import type * as SubAgentFactoryMod from './agent/sub-agent-factory';
+import type * as DomainAccessMod from './domain-access';
import type * as McpClientManagerMod from './mcp/mcp-client-manager';
import type * as TitleUtilsMod from './memory/title-utils';
+import type * as StructuredFileParserMod from './parsers/structured-file-parser';
+import type * as ValidateAttachmentsMod from './parsers/validate-attachments';
+import type * as PlannedTaskPermissionsMod from './planned-tasks/planned-task-permissions';
+import type * as PlannedTaskServiceMod from './planned-tasks/planned-task-service';
+import type * as BackgroundTaskManagerMod from './runtime/background-task-manager';
+import type * as LivenessPolicyMod from './runtime/liveness-policy';
+import type * as ResumableStreamExecutorMod from './runtime/resumable-stream-executor';
+import type * as RunStateRegistryMod from './runtime/run-state-registry';
+import type * as StreamRunnerMod from './runtime/stream-runner';
+import type * as TerminalResponseGuardMod from './runtime/terminal-response-guard';
import type * as MaterializeRuntimeSkillsMod from './skills/materialize-runtime-skills';
import type * as RuntimeSkillsMod from './skills/runtime-skills';
+import type * as StorageMod from './storage';
+import type * as MapChunkMod from './stream/map-chunk';
+import type * as ToolsMod from './tools';
+import type * as AgentPersistenceMod from './tools/orchestration/agent-persistence';
import type * as DelegateToolMod from './tools/orchestration/delegate.tool';
+import type * as SanitizeWebContentMod from './tools/web-research/sanitize-web-content';
import type * as LangsmithTracingMod from './tracing/langsmith-tracing';
+import type * as TraceReplayMod from './tracing/trace-replay';
+import type * as AgentTreeMod from './utils/agent-tree';
import type * as EvalAgentsMod from './utils/eval-agents';
+import type * as StreamHelpersMod from './utils/stream-helpers';
+import type * as WorkflowLoopMod from './workflow-loop';
+import type * as WorkflowLoopRuntimeMod from './workflow-loop/runtime';
import type * as BuilderTemplatesServiceMod from './workspace/builder-templates-service';
import type * as CreateWorkspaceMod from './workspace/create-workspace';
+import type * as LazyRuntimeWorkspaceMod from './workspace/lazy-runtime-workspace';
+import type * as SandboxSetupMod from './workspace/sandbox-setup';
+import type * as SnapshotManagerMod from './workspace/snapshot-manager';
type LazyFunction = (...args: never[]) => unknown;
type LazyConstructor = abstract new (...args: never[]) => unknown;
@@ -57,19 +81,35 @@ const defineLazyExport = (name: string, load: () => TValue): void => {
const loadLangsmithTracing = lazyModule(
() => require('./tracing/langsmith-tracing') as typeof LangsmithTracingMod,
);
+const loadTraceReplay = lazyModule(
+ () => require('./tracing/trace-replay') as typeof TraceReplayMod,
+);
const loadInstanceAgent = lazyModule(
() => require('./agent/instance-agent') as typeof InstanceAgentMod,
);
+const loadDomainAccess = lazyModule(() => require('./domain-access') as typeof DomainAccessMod);
const loadSubAgentFactory = lazyModule(
() => require('./agent/sub-agent-factory') as typeof SubAgentFactoryMod,
);
+const loadSanitizeWebContent = lazyModule(
+ () => require('./tools/web-research/sanitize-web-content') as typeof SanitizeWebContentMod,
+);
const loadDelegateTool = lazyModule(
() => require('./tools/orchestration/delegate.tool') as typeof DelegateToolMod,
);
+const loadTools = lazyModule(() => require('./tools') as typeof ToolsMod);
+const loadAgentPersistence = lazyModule(
+ () => require('./tools/orchestration/agent-persistence') as typeof AgentPersistenceMod,
+);
const loadTitleUtils = lazyModule(() => require('./memory/title-utils') as typeof TitleUtilsMod);
const loadMcpClientManager = lazyModule(
() => require('./mcp/mcp-client-manager') as typeof McpClientManagerMod,
);
+const loadStreamHelpers = lazyModule(
+ () => require('./utils/stream-helpers') as typeof StreamHelpersMod,
+);
+const loadStorage = lazyModule(() => require('./storage') as typeof StorageMod);
+const loadMapChunk = lazyModule(() => require('./stream/map-chunk') as typeof MapChunkMod);
const loadRuntimeSkills = lazyModule(
() => require('./skills/runtime-skills') as typeof RuntimeSkillsMod,
);
@@ -77,12 +117,56 @@ const loadMaterializeRuntimeSkills = lazyModule(
() => require('./skills/materialize-runtime-skills') as typeof MaterializeRuntimeSkillsMod,
);
const loadEvalAgents = lazyModule(() => require('./utils/eval-agents') as typeof EvalAgentsMod);
+const loadAgentTree = lazyModule(() => require('./utils/agent-tree') as typeof AgentTreeMod);
const loadBuilderTemplatesService = lazyModule(
() => require('./workspace/builder-templates-service') as typeof BuilderTemplatesServiceMod,
);
const loadCreateWorkspace = lazyModule(
() => require('./workspace/create-workspace') as typeof CreateWorkspaceMod,
);
+const loadLazyRuntimeWorkspace = lazyModule(
+ () => require('./workspace/lazy-runtime-workspace') as typeof LazyRuntimeWorkspaceMod,
+);
+const loadSandboxSetup = lazyModule(
+ () => require('./workspace/sandbox-setup') as typeof SandboxSetupMod,
+);
+const loadSnapshotManager = lazyModule(
+ () => require('./workspace/snapshot-manager') as typeof SnapshotManagerMod,
+);
+const loadRunStateRegistry = lazyModule(
+ () => require('./runtime/run-state-registry') as typeof RunStateRegistryMod,
+);
+const loadBackgroundTaskManager = lazyModule(
+ () => require('./runtime/background-task-manager') as typeof BackgroundTaskManagerMod,
+);
+const loadTerminalResponseGuard = lazyModule(
+ () => require('./runtime/terminal-response-guard') as typeof TerminalResponseGuardMod,
+);
+const loadResumableStreamExecutor = lazyModule(
+ () => require('./runtime/resumable-stream-executor') as typeof ResumableStreamExecutorMod,
+);
+const loadStreamRunner = lazyModule(
+ () => require('./runtime/stream-runner') as typeof StreamRunnerMod,
+);
+const loadLivenessPolicy = lazyModule(
+ () => require('./runtime/liveness-policy') as typeof LivenessPolicyMod,
+);
+const loadWorkflowLoop = lazyModule(() => require('./workflow-loop') as typeof WorkflowLoopMod);
+const loadWorkflowLoopRuntime = lazyModule(
+ () => require('./workflow-loop/runtime') as typeof WorkflowLoopRuntimeMod,
+);
+const loadPlannedTaskService = lazyModule(
+ () => require('./planned-tasks/planned-task-service') as typeof PlannedTaskServiceMod,
+);
+const loadPlannedTaskPermissions = lazyModule(
+ () => require('./planned-tasks/planned-task-permissions') as typeof PlannedTaskPermissionsMod,
+);
+const loadStructuredFileParser = lazyModule(
+ () => require('./parsers/structured-file-parser') as typeof StructuredFileParserMod,
+);
+const loadValidateAttachments = lazyModule(
+ () => require('./parsers/validate-attachments') as typeof ValidateAttachmentsMod,
+);
export { MAX_STEPS } from './constants/max-steps';
export type {
@@ -95,9 +179,12 @@ export type {
SerializableAgentState,
Thread,
} from '@n8n/agents';
-export { wrapUntrustedData } from './tools/web-research/sanitize-web-content';
+export const wrapUntrustedData: typeof SanitizeWebContentMod.wrapUntrustedData = lazyFunction(
+ () => loadSanitizeWebContent().wrapUntrustedData,
+);
export type { Logger } from './logger';
-export { createDomainAccessTracker } from './domain-access';
+export const createDomainAccessTracker: typeof DomainAccessMod.createDomainAccessTracker =
+ lazyFunction(() => loadDomainAccess().createDomainAccessTracker);
export type { DomainAccessTracker } from './domain-access';
export type { SubmitLangsmithUserFeedbackOptions } from './tracing/langsmith-tracing';
@@ -130,13 +217,22 @@ export const submitLangsmithUserFeedback: typeof LangsmithTracingMod.submitLangs
export const withCurrentTraceSpan: typeof LangsmithTracingMod.withCurrentTraceSpan = lazyFunction(
() => loadLangsmithTracing().withCurrentTraceSpan,
);
-export {
- IdRemapper,
- TraceIndex,
- TraceWriter,
- parseTraceJsonl,
- PURE_REPLAY_TOOLS,
-} from './tracing/trace-replay';
+export type IdRemapper = TraceReplayMod.IdRemapper;
+export const IdRemapper: typeof TraceReplayMod.IdRemapper = lazyClass(
+ () => loadTraceReplay().IdRemapper,
+);
+export type TraceIndex = TraceReplayMod.TraceIndex;
+export const TraceIndex: typeof TraceReplayMod.TraceIndex = lazyClass(
+ () => loadTraceReplay().TraceIndex,
+);
+export type TraceWriter = TraceReplayMod.TraceWriter;
+export const TraceWriter: typeof TraceReplayMod.TraceWriter = lazyClass(
+ () => loadTraceReplay().TraceWriter,
+);
+export const parseTraceJsonl: typeof TraceReplayMod.parseTraceJsonl = lazyFunction(
+ () => loadTraceReplay().parseTraceJsonl,
+);
+export declare const PURE_REPLAY_TOOLS: typeof TraceReplayMod.PURE_REPLAY_TOOLS;
export type {
TraceEvent,
TraceHeader,
@@ -180,26 +276,48 @@ export const createInstanceAgent: typeof InstanceAgentMod.createInstanceAgent =
export const createSubAgent: typeof SubAgentFactoryMod.createSubAgent = lazyFunction(
() => loadSubAgentFactory().createSubAgent,
);
-export { createAllTools, createOrchestrationTools } from './tools';
-export {
- createSubAgentResourceId,
- createSubAgentResourceIdPrefix,
- SUB_AGENT_RESOURCE_PREFIX,
-} from './tools/orchestration/agent-persistence';
+export const createAllTools: typeof ToolsMod.createAllTools = lazyFunction(
+ () => loadTools().createAllTools,
+);
+export const createOrchestrationTools: typeof ToolsMod.createOrchestrationTools = lazyFunction(
+ () => loadTools().createOrchestrationTools,
+);
+export const createSubAgentResourceId: typeof AgentPersistenceMod.createSubAgentResourceId =
+ lazyFunction(() => loadAgentPersistence().createSubAgentResourceId);
+export const createSubAgentResourceIdPrefix: typeof AgentPersistenceMod.createSubAgentResourceIdPrefix =
+ lazyFunction(() => loadAgentPersistence().createSubAgentResourceIdPrefix);
+export declare const SUB_AGENT_RESOURCE_PREFIX: typeof AgentPersistenceMod.SUB_AGENT_RESOURCE_PREFIX;
export const startDetachedDelegateTask: typeof DelegateToolMod.startDetachedDelegateTask =
lazyFunction(() => loadDelegateTool().startDetachedDelegateTask);
-export {
- iterationEntrySchema,
- formatPreviousAttempts,
- ThreadIterationLogStorage,
- ThreadTaskStorage,
- PlannedTaskStorage,
- getThread,
- TerminalOutcomeStorage,
- patchThread,
- WorkflowLoopStorage,
-} from './storage';
+export declare const iterationEntrySchema: typeof StorageMod.iterationEntrySchema;
+export const formatPreviousAttempts: typeof StorageMod.formatPreviousAttempts = lazyFunction(
+ () => loadStorage().formatPreviousAttempts,
+);
+export type ThreadIterationLogStorage = StorageMod.ThreadIterationLogStorage;
+export const ThreadIterationLogStorage: typeof StorageMod.ThreadIterationLogStorage = lazyClass(
+ () => loadStorage().ThreadIterationLogStorage,
+);
+export type ThreadTaskStorage = StorageMod.ThreadTaskStorage;
+export const ThreadTaskStorage: typeof StorageMod.ThreadTaskStorage = lazyClass(
+ () => loadStorage().ThreadTaskStorage,
+);
+export type PlannedTaskStorage = StorageMod.PlannedTaskStorage;
+export const PlannedTaskStorage: typeof StorageMod.PlannedTaskStorage = lazyClass(
+ () => loadStorage().PlannedTaskStorage,
+);
+export const getThread: typeof StorageMod.getThread = lazyFunction(() => loadStorage().getThread);
+export type TerminalOutcomeStorage = StorageMod.TerminalOutcomeStorage;
+export const TerminalOutcomeStorage: typeof StorageMod.TerminalOutcomeStorage = lazyClass(
+ () => loadStorage().TerminalOutcomeStorage,
+);
+export const patchThread: typeof StorageMod.patchThread = lazyFunction(
+ () => loadStorage().patchThread,
+);
+export type WorkflowLoopStorage = StorageMod.WorkflowLoopStorage;
+export const WorkflowLoopStorage: typeof StorageMod.WorkflowLoopStorage = lazyClass(
+ () => loadStorage().WorkflowLoopStorage,
+);
export type {
AgentTreeSnapshot,
IterationEntry,
@@ -219,8 +337,18 @@ export type McpClientManager = McpClientManagerMod.McpClientManager;
export const McpClientManager: typeof McpClientManagerMod.McpClientManager = lazyClass(
() => loadMcpClientManager().McpClientManager,
);
-export { mapAgentChunkToEvent } from './stream/map-chunk';
-export { isRecord, parseSuspension, asResumable } from './utils/stream-helpers';
+export const mapAgentChunkToEvent: typeof MapChunkMod.mapAgentChunkToEvent = lazyFunction(
+ () => loadMapChunk().mapAgentChunkToEvent,
+);
+export const isRecord: typeof StreamHelpersMod.isRecord = lazyFunction(
+ () => loadStreamHelpers().isRecord,
+);
+export const parseSuspension: typeof StreamHelpersMod.parseSuspension = lazyFunction(
+ () => loadStreamHelpers().parseSuspension,
+);
+export const asResumable: typeof StreamHelpersMod.asResumable = lazyFunction(
+ () => loadStreamHelpers().asResumable,
+);
export const createEvalAgent: typeof EvalAgentsMod.createEvalAgent = lazyFunction(
() => loadEvalAgents().createEvalAgent,
);
@@ -233,6 +361,12 @@ export declare const SONNET_MODEL: typeof EvalAgentsMod.SONNET_MODEL;
export declare const HAIKU_MODEL: typeof EvalAgentsMod.HAIKU_MODEL;
defineLazyExport('SONNET_MODEL', () => loadEvalAgents().SONNET_MODEL);
defineLazyExport('HAIKU_MODEL', () => loadEvalAgents().HAIKU_MODEL);
+defineLazyExport('PURE_REPLAY_TOOLS', () => loadTraceReplay().PURE_REPLAY_TOOLS);
+defineLazyExport(
+ 'SUB_AGENT_RESOURCE_PREFIX',
+ () => loadAgentPersistence().SUB_AGENT_RESOURCE_PREFIX,
+);
+defineLazyExport('iterationEntrySchema', () => loadStorage().iterationEntrySchema);
defineLazyExport('INSTANCE_AI_SKILLS_DIR', () => loadRuntimeSkills().INSTANCE_AI_SKILLS_DIR);
defineLazyExport(
'SANDBOX_RUNTIME_SKILLS_DIR',
@@ -256,12 +390,36 @@ defineLazyExport(
'N8N_WORKSPACE_DIR_ENV',
() => loadMaterializeRuntimeSkills().N8N_WORKSPACE_DIR_ENV,
);
+defineLazyExport(
+ 'INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG',
+ () => loadLivenessPolicy().INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG,
+);
+defineLazyExport('workflowBuildOutcomeSchema', () => loadWorkflowLoop().workflowBuildOutcomeSchema);
+defineLazyExport('attemptRecordSchema', () => loadWorkflowLoop().attemptRecordSchema);
+defineLazyExport('workflowLoopStateSchema', () => loadWorkflowLoop().workflowLoopStateSchema);
+defineLazyExport('verificationResultSchema', () => loadWorkflowLoop().verificationResultSchema);
+defineLazyExport(
+ 'PLANNED_TASK_PERMISSION_OVERRIDES',
+ () => loadPlannedTaskPermissions().PLANNED_TASK_PERMISSION_OVERRIDES,
+);
export type { SuspensionInfo, Resumable } from './utils/stream-helpers';
-export { buildAgentTreeFromEvents, findAgentNodeInTree } from './utils/agent-tree';
+export const buildAgentTreeFromEvents: typeof AgentTreeMod.buildAgentTreeFromEvents = lazyFunction(
+ () => loadAgentTree().buildAgentTreeFromEvents,
+);
+export const findAgentNodeInTree: typeof AgentTreeMod.findAgentNodeInTree = lazyFunction(
+ () => loadAgentTree().findAgentNodeInTree,
+);
export type { SandboxConfig } from './workspace/create-workspace';
-export { createLazyRuntimeWorkspace } from './workspace/lazy-runtime-workspace';
+export const createLazyRuntimeWorkspace: typeof LazyRuntimeWorkspaceMod.createLazyRuntimeWorkspace =
+ lazyFunction(() => loadLazyRuntimeWorkspace().createLazyRuntimeWorkspace);
export type { RuntimeWorkspaceResolver } from './workspace/lazy-runtime-workspace';
-export { getWorkspaceRoot, setupSandboxWorkspace } from './workspace/sandbox-setup';
+export const getWorkspaceRoot: typeof SandboxSetupMod.getWorkspaceRoot = lazyFunction(
+ () => loadSandboxSetup().getWorkspaceRoot,
+);
+export const setupSandboxWorkspace: typeof SandboxSetupMod.setupSandboxWorkspace = lazyFunction(
+ () => loadSandboxSetup().setupSandboxWorkspace,
+);
+export type BuilderTemplatesService = BuilderTemplatesServiceMod.BuilderTemplatesService;
export const BuilderTemplatesService: typeof BuilderTemplatesServiceMod.BuilderTemplatesService =
lazyClass(() => loadBuilderTemplatesService().BuilderTemplatesService);
export const builderTemplatesOptionsFromEnv: typeof BuilderTemplatesServiceMod.builderTemplatesOptionsFromEnv =
@@ -276,19 +434,27 @@ export const createSandbox: typeof CreateWorkspaceMod.createSandbox = lazyFuncti
export const createWorkspace: typeof CreateWorkspaceMod.createWorkspace = lazyFunction(
() => loadCreateWorkspace().createWorkspace,
);
-export { SnapshotManager } from './workspace/snapshot-manager';
+export type SnapshotManager = SnapshotManagerMod.SnapshotManager;
+export const SnapshotManager: typeof SnapshotManagerMod.SnapshotManager = lazyClass(
+ () => loadSnapshotManager().SnapshotManager,
+);
export type { InstanceAiEventBus, StoredEvent } from './event-bus';
-export {
- BackgroundTaskManager,
- enrichMessageWithRunningTasks as enrichMessageWithBackgroundTasks,
- enrichMessageWithRunningTasks,
-} from './runtime/background-task-manager';
+export type BackgroundTaskManager = BackgroundTaskManagerMod.BackgroundTaskManager;
+export const BackgroundTaskManager: typeof BackgroundTaskManagerMod.BackgroundTaskManager =
+ lazyClass(() => loadBackgroundTaskManager().BackgroundTaskManager);
+export const enrichMessageWithRunningTasks: typeof BackgroundTaskManagerMod.enrichMessageWithRunningTasks =
+ lazyFunction(() => loadBackgroundTaskManager().enrichMessageWithRunningTasks);
+export const enrichMessageWithBackgroundTasks: typeof BackgroundTaskManagerMod.enrichMessageWithRunningTasks =
+ enrichMessageWithRunningTasks;
export type {
BackgroundTaskStatus,
ManagedBackgroundTask,
SpawnManagedBackgroundTaskOptions,
} from './runtime/background-task-manager';
-export { RunStateRegistry } from './runtime/run-state-registry';
+export type RunStateRegistry = RunStateRegistryMod.RunStateRegistry;
+export const RunStateRegistry: typeof RunStateRegistryMod.RunStateRegistry = lazyClass(
+ () => loadRunStateRegistry().RunStateRegistry,
+);
export type {
ActiveRunState,
BackgroundTaskStatusSnapshot,
@@ -298,13 +464,17 @@ export type {
StartedRunState,
SuspendedRunState,
} from './runtime/run-state-registry';
-export { InstanceAiTerminalResponseGuard } from './runtime/terminal-response-guard';
+export type InstanceAiTerminalResponseGuard =
+ TerminalResponseGuardMod.InstanceAiTerminalResponseGuard;
+export const InstanceAiTerminalResponseGuard: typeof TerminalResponseGuardMod.InstanceAiTerminalResponseGuard =
+ lazyClass(() => loadTerminalResponseGuard().InstanceAiTerminalResponseGuard);
export type {
TerminalResponseDecision,
TerminalResponseStatus,
TerminalVisibilitySource,
} from './runtime/terminal-response-guard';
-export { executeResumableStream } from './runtime/resumable-stream-executor';
+export const executeResumableStream: typeof ResumableStreamExecutorMod.executeResumableStream =
+ lazyFunction(() => loadResumableStreamExecutor().executeResumableStream);
export type {
AutoResumeControl,
ExecuteResumableStreamOptions,
@@ -315,34 +485,51 @@ export type {
ResumableStreamSource,
} from './runtime/resumable-stream-executor';
export type { WorkSummary } from './stream/work-summary-accumulator';
-export { resumeAgentRun, streamAgentRun } from './runtime/stream-runner';
-export {
- createInstanceAiLivenessPolicyConfig,
- INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG,
- InstanceAiLivenessPolicy,
- type InstanceAiLivenessDecision,
- type InstanceAiLivenessInput,
- type InstanceAiLivenessPolicyConfig,
- type InstanceAiLivenessSurface,
- type InstanceAiLivenessTimeoutReason,
+export const resumeAgentRun: typeof StreamRunnerMod.resumeAgentRun = lazyFunction(
+ () => loadStreamRunner().resumeAgentRun,
+);
+export const streamAgentRun: typeof StreamRunnerMod.streamAgentRun = lazyFunction(
+ () => loadStreamRunner().streamAgentRun,
+);
+export const createInstanceAiLivenessPolicyConfig: typeof LivenessPolicyMod.createInstanceAiLivenessPolicyConfig =
+ lazyFunction(() => loadLivenessPolicy().createInstanceAiLivenessPolicyConfig);
+export declare const INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG: typeof LivenessPolicyMod.INSTANCE_AI_DEFAULT_LIVENESS_POLICY_CONFIG;
+export type InstanceAiLivenessPolicy = LivenessPolicyMod.InstanceAiLivenessPolicy;
+export const InstanceAiLivenessPolicy: typeof LivenessPolicyMod.InstanceAiLivenessPolicy =
+ lazyClass(() => loadLivenessPolicy().InstanceAiLivenessPolicy);
+export type {
+ InstanceAiLivenessDecision,
+ InstanceAiLivenessInput,
+ InstanceAiLivenessPolicyConfig,
+ InstanceAiLivenessSurface,
+ InstanceAiLivenessTimeoutReason,
} from './runtime/liveness-policy';
export type {
StreamableAgent,
StreamRunOptions,
StreamRunResult,
} from './runtime/stream-runner';
-export {
- createWorkItem,
- formatWorkflowLoopGuidance,
- handleBuildOutcome,
- handleVerificationVerdict,
- formatAttemptHistory,
- WorkflowTaskCoordinator,
- workflowBuildOutcomeSchema,
- attemptRecordSchema,
- workflowLoopStateSchema,
- verificationResultSchema,
-} from './workflow-loop';
+export const createWorkItem: typeof WorkflowLoopMod.createWorkItem = lazyFunction(
+ () => loadWorkflowLoop().createWorkItem,
+);
+export const formatWorkflowLoopGuidance: typeof WorkflowLoopMod.formatWorkflowLoopGuidance =
+ lazyFunction(() => loadWorkflowLoop().formatWorkflowLoopGuidance);
+export const handleBuildOutcome: typeof WorkflowLoopMod.handleBuildOutcome = lazyFunction(
+ () => loadWorkflowLoop().handleBuildOutcome,
+);
+export const handleVerificationVerdict: typeof WorkflowLoopMod.handleVerificationVerdict =
+ lazyFunction(() => loadWorkflowLoop().handleVerificationVerdict);
+export const formatAttemptHistory: typeof WorkflowLoopMod.formatAttemptHistory = lazyFunction(
+ () => loadWorkflowLoop().formatAttemptHistory,
+);
+export type WorkflowTaskCoordinator = WorkflowLoopMod.WorkflowTaskCoordinator;
+export const WorkflowTaskCoordinator: typeof WorkflowLoopMod.WorkflowTaskCoordinator = lazyClass(
+ () => loadWorkflowLoop().WorkflowTaskCoordinator,
+);
+export declare const workflowBuildOutcomeSchema: typeof WorkflowLoopMod.workflowBuildOutcomeSchema;
+export declare const attemptRecordSchema: typeof WorkflowLoopMod.attemptRecordSchema;
+export declare const workflowLoopStateSchema: typeof WorkflowLoopMod.workflowLoopStateSchema;
+export declare const verificationResultSchema: typeof WorkflowLoopMod.verificationResultSchema;
export type {
WorkflowLoopState,
WorkflowLoopAction,
@@ -350,12 +537,16 @@ export type {
VerificationResult,
AttemptRecord,
} from './workflow-loop';
-export { WorkflowLoopRuntime } from './workflow-loop/runtime';
-export { PlannedTaskCoordinator } from './planned-tasks/planned-task-service';
-export {
- applyPlannedTaskPermissions,
- PLANNED_TASK_PERMISSION_OVERRIDES,
-} from './planned-tasks/planned-task-permissions';
+export type WorkflowLoopRuntime = WorkflowLoopRuntimeMod.WorkflowLoopRuntime;
+export const WorkflowLoopRuntime: typeof WorkflowLoopRuntimeMod.WorkflowLoopRuntime = lazyClass(
+ () => loadWorkflowLoopRuntime().WorkflowLoopRuntime,
+);
+export type PlannedTaskCoordinator = PlannedTaskServiceMod.PlannedTaskCoordinator;
+export const PlannedTaskCoordinator: typeof PlannedTaskServiceMod.PlannedTaskCoordinator =
+ lazyClass(() => loadPlannedTaskService().PlannedTaskCoordinator);
+export const applyPlannedTaskPermissions: typeof PlannedTaskPermissionsMod.applyPlannedTaskPermissions =
+ lazyFunction(() => loadPlannedTaskPermissions().applyPlannedTaskPermissions);
+export declare const PLANNED_TASK_PERMISSION_OVERRIDES: typeof PlannedTaskPermissionsMod.PLANNED_TASK_PERMISSION_OVERRIDES;
export type {
InstanceAiContext,
InstanceAiWorkflowService,
@@ -421,12 +612,15 @@ export type {
ServiceProxyConfig,
} from './types';
export type { DetachedDelegateTaskResult } from './tools/orchestration/delegate.tool';
-export {
- classifyAttachments,
- buildAttachmentManifest,
- isStructuredAttachment,
- isParseableAttachment,
-} from './parsers/structured-file-parser';
+export const classifyAttachments: typeof StructuredFileParserMod.classifyAttachments = lazyFunction(
+ () => loadStructuredFileParser().classifyAttachments,
+);
+export const buildAttachmentManifest: typeof StructuredFileParserMod.buildAttachmentManifest =
+ lazyFunction(() => loadStructuredFileParser().buildAttachmentManifest);
+export const isStructuredAttachment: typeof StructuredFileParserMod.isStructuredAttachment =
+ lazyFunction(() => loadStructuredFileParser().isStructuredAttachment);
+export const isParseableAttachment: typeof StructuredFileParserMod.isParseableAttachment =
+ lazyFunction(() => loadStructuredFileParser().isParseableAttachment);
export type {
ClassifiedAttachment,
ParseableFormat,
@@ -434,11 +628,15 @@ export type {
TextLikeFormat,
SupportedFormat,
} from './parsers/structured-file-parser';
-export {
- getParseableAttachmentMimeTypes,
- getSupportedAttachmentMimeTypes,
- isSupportedAttachmentMimeType,
- validateAttachmentMimeTypes,
- UnsupportedAttachmentError,
-} from './parsers/validate-attachments';
+export const getParseableAttachmentMimeTypes: typeof ValidateAttachmentsMod.getParseableAttachmentMimeTypes =
+ lazyFunction(() => loadValidateAttachments().getParseableAttachmentMimeTypes);
+export const getSupportedAttachmentMimeTypes: typeof ValidateAttachmentsMod.getSupportedAttachmentMimeTypes =
+ lazyFunction(() => loadValidateAttachments().getSupportedAttachmentMimeTypes);
+export const isSupportedAttachmentMimeType: typeof ValidateAttachmentsMod.isSupportedAttachmentMimeType =
+ lazyFunction(() => loadValidateAttachments().isSupportedAttachmentMimeType);
+export const validateAttachmentMimeTypes: typeof ValidateAttachmentsMod.validateAttachmentMimeTypes =
+ lazyFunction(() => loadValidateAttachments().validateAttachmentMimeTypes);
+export type UnsupportedAttachmentError = ValidateAttachmentsMod.UnsupportedAttachmentError;
+export const UnsupportedAttachmentError: typeof ValidateAttachmentsMod.UnsupportedAttachmentError =
+ lazyClass(() => loadValidateAttachments().UnsupportedAttachmentError);
export type { UnsupportedAttachmentDetail } from './parsers/validate-attachments';
diff --git a/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts b/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts
index a7797593091..cbccdb28b9f 100644
--- a/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts
+++ b/packages/@n8n/instance-ai/src/tools/__tests__/nodes.tool.test.ts
@@ -135,6 +135,94 @@ describe('nodes tool', () => {
});
});
+ describe('search action', () => {
+ it('should search nodes by query and reuse the searchable node list reference', async () => {
+ const searchableNodes = [
+ {
+ name: 'n8n-nodes-base.httpRequest',
+ displayName: 'HTTP Request',
+ description: 'Make HTTP requests',
+ inputs: ['main'],
+ outputs: ['main'],
+ version: 1,
+ codex: { alias: ['api'] },
+ },
+ ];
+ const context = createMockContext();
+ (context.nodeService.listSearchable as Mock).mockResolvedValue(searchableNodes);
+
+ const tool = createNodesTool(context, 'full');
+ const first = await executeTool(
+ tool,
+ { action: 'search', query: 'http', limit: 5 } as never,
+ {} as never,
+ );
+ const second = await executeTool(
+ tool,
+ { action: 'search', query: 'http', limit: 5 } as never,
+ {} as never,
+ );
+
+ expect(context.nodeService.listSearchable).toHaveBeenCalledTimes(2);
+ expect(first).toMatchObject({
+ totalResults: 1,
+ results: [expect.objectContaining({ name: 'n8n-nodes-base.httpRequest' })],
+ });
+ expect(second).toMatchObject({
+ totalResults: 1,
+ results: [expect.objectContaining({ name: 'n8n-nodes-base.httpRequest' })],
+ });
+ });
+
+ it('should search nodes by connection type and enrich results with discriminators', async () => {
+ const searchableNodes = [
+ {
+ name: 'n8n-nodes-base.slackTool',
+ displayName: 'Slack Tool',
+ description: 'Send messages to Slack from an AI agent',
+ inputs: ['main'],
+ outputs: ['ai_tool'],
+ version: 1,
+ },
+ ];
+ const context = createMockContext();
+ (context.nodeService.listSearchable as Mock).mockResolvedValue(searchableNodes);
+ context.nodeService.listDiscriminators = vi.fn().mockResolvedValue({
+ resource: ['message'],
+ });
+
+ const tool = createNodesTool(context, 'full');
+ const result = await executeTool(
+ tool,
+ { action: 'search', connectionType: 'ai_tool', limit: 5 } as never,
+ {} as never,
+ );
+
+ expect(context.nodeService.listDiscriminators).toHaveBeenCalledWith(
+ 'n8n-nodes-base.slackTool',
+ );
+ expect(result).toMatchObject({
+ totalResults: 1,
+ results: [
+ expect.objectContaining({
+ name: 'n8n-nodes-base.slackTool',
+ discriminators: { resource: ['message'] },
+ }),
+ ],
+ });
+ });
+
+ it('should return no search results when neither query nor connection type is provided', async () => {
+ const context = createMockContext();
+ (context.nodeService.listSearchable as Mock).mockResolvedValue([]);
+
+ const tool = createNodesTool(context, 'full');
+ const result = await executeTool(tool, { action: 'search' } as never, {} as never);
+
+ expect(result).toEqual({ results: [], totalResults: 0 });
+ });
+ });
+
describe('explore-resources action', () => {
it('should return error when exploreResources is not available', async () => {
const context = createMockContext();
diff --git a/packages/@n8n/instance-ai/src/tools/nodes.tool.ts b/packages/@n8n/instance-ai/src/tools/nodes.tool.ts
index 159050572b5..cac7bea2aca 100644
--- a/packages/@n8n/instance-ai/src/tools/nodes.tool.ts
+++ b/packages/@n8n/instance-ai/src/tools/nodes.tool.ts
@@ -7,7 +7,7 @@ import { z } from 'zod';
import { sanitizeInputSchema } from '../agent/sanitize-mcp-schemas';
import type { InstanceAiContext } from '../types';
import { NodeSearchEngine } from './nodes/node-search-engine';
-import { AI_CONNECTION_TYPES } from './nodes/node-search-engine.types';
+import { AI_CONNECTION_TYPES, type SearchableNodeType } from './nodes/node-search-engine.types';
import { categoryList, suggestedNodesData } from './nodes/suggested-nodes-data';
// ── Action schemas ──────────────────────────────────────────────────────────
@@ -123,6 +123,12 @@ const fullInputSchema = sanitizeInputSchema(
type FullInput = z.infer;
+interface SearchEngineCache {
+ nodeTypes?: SearchableNodeType[];
+ nodeCount?: number;
+ engine?: NodeSearchEngine;
+}
+
// ── Handlers ────────────────────────────────────────────────────────────────
async function handleList(
@@ -138,9 +144,16 @@ async function handleList(
async function handleSearch(
context: InstanceAiContext,
input: Extract,
+ cache: SearchEngineCache,
) {
const nodeTypes = await context.nodeService.listSearchable();
- const engine = new NodeSearchEngine(nodeTypes);
+ let engine = cache.engine;
+ if (!engine || cache.nodeTypes !== nodeTypes || cache.nodeCount !== nodeTypes.length) {
+ cache.nodeTypes = nodeTypes;
+ cache.nodeCount = nodeTypes.length;
+ engine = new NodeSearchEngine(nodeTypes);
+ cache.engine = engine;
+ }
let results;
if (input.connectionType) {
@@ -312,6 +325,8 @@ export function createNodesTool(
context: InstanceAiContext,
surface: 'full' | 'orchestrator' = 'full',
) {
+ const searchEngineCache: SearchEngineCache = {};
+
if (surface === 'orchestrator') {
const orchestratorExploreAction = z.object({
action: z
@@ -385,7 +400,7 @@ export function createNodesTool(
case 'list':
return await handleList(context, input);
case 'search':
- return await handleSearch(context, input);
+ return await handleSearch(context, input, searchEngineCache);
case 'describe':
return await handleDescribe(context, input);
case 'type-definition':
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts
index 3e62b509002..300e1cf1782 100644
--- a/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts
+++ b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts
@@ -165,6 +165,23 @@ describe('NodeSearchEngine', () => {
expect(results[0].name).toBe('n8n-nodes-base.httpRequest');
});
+ it('should find nodes by description fallback', () => {
+ const results = engine.searchByName('requests');
+ expect(results.map((r) => r.name)).toContain('n8n-nodes-base.httpRequest');
+ });
+
+ it('should return fresh cached result objects', () => {
+ const first = engine.searchByName('AI Agent');
+ const agentResult = first.find((r) => r.name === '@n8n/n8n-nodes-langchain.agent');
+ expect(agentResult).toBeDefined();
+ agentResult!.displayName = 'Mutated Agent';
+
+ const second = engine.searchByName('AI Agent');
+ expect(second.find((r) => r.name === '@n8n/n8n-nodes-langchain.agent')?.displayName).toBe(
+ 'AI Agent',
+ );
+ });
+
it('should respect the limit parameter', () => {
const results = engine.searchByName('n', 2);
expect(results.length).toBeLessThanOrEqual(2);
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.ts b/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.ts
index 0a383ac29e6..558cccaf64d 100644
--- a/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.ts
+++ b/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.ts
@@ -40,7 +40,6 @@ const NODE_SEARCH_KEYS = [
{ key: 'displayName', weight: 1.5 },
{ key: 'name', weight: 1.3 },
{ key: 'codex.alias', weight: 1.0 },
- { key: 'description', weight: 0.7 },
];
/**
@@ -96,6 +95,38 @@ function dedupeNodes(nodes: SearchableNodeType[]): SearchableNodeType[] {
return Object.values(dedupeCache);
}
+function cloneSearchResult(result: NodeSearchResult): NodeSearchResult {
+ return {
+ ...result,
+ ...(result.subnodeRequirements
+ ? {
+ subnodeRequirements: result.subnodeRequirements.map((requirement) => ({
+ ...requirement,
+ })),
+ }
+ : {}),
+ };
+}
+
+function cloneSearchResults(results: NodeSearchResult[]): NodeSearchResult[] {
+ return results.map(cloneSearchResult);
+}
+
+function descriptionMatchScore(
+ description: string | undefined,
+ queryLower: string,
+ queryTerms: string[],
+): number {
+ if (!description) return 0;
+ const descriptionLower = description.toLowerCase();
+ if (queryTerms.length === 0) return descriptionLower.includes(queryLower) ? 1 : 0;
+ let matches = 0;
+ for (const term of queryTerms) {
+ if (descriptionLower.includes(term)) matches += 1;
+ }
+ return matches;
+}
+
// ---------------------------------------------------------------------------
// Engine
// ---------------------------------------------------------------------------
@@ -107,6 +138,10 @@ function dedupeNodes(nodes: SearchableNodeType[]): SearchableNodeType[] {
export class NodeSearchEngine {
private readonly nodeTypes: SearchableNodeType[];
+ private readonly nameSearchCache = new Map();
+
+ private readonly connectionSearchCache = new Map();
+
constructor(nodeTypes: SearchableNodeType[]) {
this.nodeTypes = dedupeNodes(nodeTypes);
}
@@ -119,6 +154,7 @@ export class NodeSearchEngine {
private fuzzySearchNodes(
query: string,
candidates: SearchableNodeType[],
+ limit?: number,
): Array<{ item: SearchableNodeType; score: number }> {
const queryLower = query.toLowerCase().trim();
const queryTerms = queryLower.split(/\s+/).filter((t) => t.length > 1);
@@ -133,7 +169,12 @@ export class NodeSearchEngine {
if (isMultiWord) {
const scoreMap = new Map();
for (const term of queryTerms) {
- const termResults = sublimeSearch(term, candidates, NODE_SEARCH_KEYS);
+ const termResults = sublimeSearch(
+ term,
+ candidates,
+ NODE_SEARCH_KEYS,
+ limit,
+ );
for (const r of termResults) {
const existing = scoreMap.get(r.item.name);
if (!existing || r.score > existing.score) {
@@ -143,10 +184,24 @@ export class NodeSearchEngine {
}
searchResults = [...scoreMap.values()];
} else {
- searchResults = sublimeSearch(query, candidates, NODE_SEARCH_KEYS);
+ searchResults = sublimeSearch(query, candidates, NODE_SEARCH_KEYS, limit);
}
const fuzzyResultNames = new Set(searchResults.map((r) => r.item.name));
+ const remainingDescriptionMatches =
+ limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, limit - searchResults.length);
+ if (remainingDescriptionMatches > 0) {
+ const descriptionResults: ScoredNode[] = [];
+ for (const item of candidates) {
+ if (fuzzyResultNames.has(item.name)) continue;
+ const score = descriptionMatchScore(item.description, queryLower, queryTerms);
+ if (score === 0) continue;
+ descriptionResults.push({ item, score });
+ fuzzyResultNames.add(item.name);
+ if (descriptionResults.length >= remainingDescriptionMatches) break;
+ }
+ searchResults = [...searchResults, ...descriptionResults];
+ }
// Direct type name / display name match — catches nodes fuzzy search missed
const typeNameMatches = candidates
@@ -196,7 +251,11 @@ export class NodeSearchEngine {
* @returns Array of matching nodes sorted by relevance
*/
searchByName(query: string, limit: number = 20): NodeSearchResult[] {
- return this.fuzzySearchNodes(query, this.nodeTypes)
+ const cacheKey = `${query}\0${limit}`;
+ const cached = this.nameSearchCache.get(cacheKey);
+ if (cached) return cloneSearchResults(cached);
+
+ const results = this.fuzzySearchNodes(query, this.nodeTypes, limit)
.slice(0, limit)
.map(({ item, score }): NodeSearchResult => {
const subnodeRequirements = extractSubnodeRequirements(item.builderHint?.inputs);
@@ -212,6 +271,8 @@ export class NodeSearchEngine {
...(subnodeRequirements.length > 0 && { subnodeRequirements }),
};
});
+ this.nameSearchCache.set(cacheKey, results);
+ return cloneSearchResults(results);
}
/**
@@ -227,6 +288,10 @@ export class NodeSearchEngine {
limit: number = 20,
nameFilter?: string,
): NodeSearchResult[] {
+ const cacheKey = `${connectionType}\0${limit}\0${nameFilter ?? ''}`;
+ const cached = this.connectionSearchCache.get(cacheKey);
+ if (cached) return cloneSearchResults(cached);
+
// First, filter by connection type
const nodesWithConnectionType = this.nodeTypes
.map((nodeType) => {
@@ -239,7 +304,7 @@ export class NodeSearchEngine {
// If no name filter, return connection matches sorted by score
if (!nameFilter) {
- return nodesWithConnectionType
+ const results = nodesWithConnectionType
.sort((a, b) => b.connectionScore - a.connectionScore)
.slice(0, limit)
.map(({ nodeType, connectionScore }) => {
@@ -258,14 +323,16 @@ export class NodeSearchEngine {
...(subnodeRequirements.length > 0 && { subnodeRequirements }),
};
});
+ this.connectionSearchCache.set(cacheKey, results);
+ return cloneSearchResults(results);
}
// Apply name filter using the same multi-word-aware fuzzy search as searchByName
const nodeTypesOnly = nodesWithConnectionType.map((result) => result.nodeType);
- const nameFilteredResults = this.fuzzySearchNodes(nameFilter, nodeTypesOnly);
+ const nameFilteredResults = this.fuzzySearchNodes(nameFilter, nodeTypesOnly, limit);
// Combine connection score with name score
- return nameFilteredResults.slice(0, limit).map(({ item, score: nameScore }) => {
+ const results = nameFilteredResults.slice(0, limit).map(({ item, score: nameScore }) => {
const connectionResult = nodesWithConnectionType.find(
(result) => result.nodeType.name === item.name,
);
@@ -284,6 +351,8 @@ export class NodeSearchEngine {
...(subnodeRequirements.length > 0 && { subnodeRequirements }),
};
});
+ this.connectionSearchCache.set(cacheKey, results);
+ return cloneSearchResults(results);
}
/**
diff --git a/packages/@n8n/instance-ai/vite.config.ts b/packages/@n8n/instance-ai/vite.config.ts
index cba8540843e..4705bcdb412 100644
--- a/packages/@n8n/instance-ai/vite.config.ts
+++ b/packages/@n8n/instance-ai/vite.config.ts
@@ -2,34 +2,82 @@ import path from 'node:path';
import { mergeConfig, type Plugin } from 'vite';
import { createVitestConfig } from '@n8n/vitest-config/node';
+const INDEX_LAZY_REQUIRE_TEST_SPECS = new Set([
+ './tracing/langsmith-tracing',
+ './agent/instance-agent',
+ './domain-access',
+ './agent/sub-agent-factory',
+ './tools/web-research/sanitize-web-content',
+ './tools/orchestration/delegate.tool',
+ './tools',
+ './tools/orchestration/agent-persistence',
+ './tracing/trace-replay',
+ './memory/title-utils',
+ './mcp/mcp-client-manager',
+ './utils/stream-helpers',
+ './storage',
+ './stream/map-chunk',
+ './skills/runtime-skills',
+ './skills/materialize-runtime-skills',
+ './utils/eval-agents',
+ './utils/agent-tree',
+ './workspace/builder-templates-service',
+ './workspace/create-workspace',
+ './workspace/lazy-runtime-workspace',
+ './workspace/sandbox-setup',
+ './workspace/snapshot-manager',
+ './runtime/run-state-registry',
+ './runtime/background-task-manager',
+ './runtime/terminal-response-guard',
+ './runtime/resumable-stream-executor',
+ './runtime/stream-runner',
+ './parsers/structured-file-parser',
+ './parsers/validate-attachments',
+ './runtime/liveness-policy',
+ './workflow-loop',
+ './workflow-loop/runtime',
+ './planned-tasks/planned-task-service',
+ './planned-tasks/planned-task-permissions',
+]);
+
/**
- * `src/tools/index.ts` lazy-loads each tool module with `require('./x.tool')`.
- * Under the production `tsc` build that resolves to the compiled `dist/*.js`, but
- * Vitest's module runner hands the (ESM) source a Node `createRequire`, which
- * cannot resolve relative `.ts` files — and `vi.mock` only intercepts ESM imports,
- * not these `require()`s. This transform (test-time only; the source on disk is
- * untouched) rewrites those `require('spec')` calls into eager static
- * `import * as __lazymod_N` bindings so Vite resolves them and `vi.mock` applies.
+ * Lazy exports use `require('./module')` because production runs the compiled
+ * CommonJS output. Vitest's module runner hands source files a Node
+ * `createRequire`, which cannot resolve relative `.ts` files — and `vi.mock`
+ * only intercepts ESM imports, not these `require()`s. This transform
+ * (test-time only; the source on disk is untouched) rewrites selected
+ * `require('spec')` calls into eager static `import * as __lazymod_N` bindings
+ * so Vite resolves them and `vi.mock` applies.
*/
function rewriteLazyRequireForTests(): Plugin {
return {
name: 'instance-ai-rewrite-lazy-require',
enforce: 'pre',
transform(code, id) {
- if (!id.replace(/\\/g, '/').endsWith('/src/tools/index.ts')) return null;
+ const normalizedId = id.replace(/\\/g, '/');
+ const shouldRewriteAllRequires = normalizedId.endsWith('/src/tools/index.ts');
+ const shouldRewriteSelectedRequires = normalizedId.endsWith('/src/index.ts');
+ if (!shouldRewriteAllRequires && !shouldRewriteSelectedRequires) return null;
+
const requireRe = /require\((['"])([^'"]+)\1\)/g;
const specs: string[] = [];
for (const match of code.matchAll(requireRe)) {
- if (!specs.includes(match[2])) specs.push(match[2]);
+ const spec = match[2];
+ if (
+ shouldRewriteAllRequires ||
+ (shouldRewriteSelectedRequires && INDEX_LAZY_REQUIRE_TEST_SPECS.has(spec))
+ ) {
+ if (!specs.includes(spec)) specs.push(spec);
+ }
}
if (specs.length === 0) return null;
const imports = specs
.map((spec, index) => `import * as __lazymod_${index} from '${spec}';`)
.join('\n');
- const rewritten = code.replace(
- requireRe,
- (_full, _quote, spec: string) => `__lazymod_${specs.indexOf(spec)}`,
- );
+ const rewritten = code.replace(requireRe, (full, _quote, spec: string) => {
+ const index = specs.indexOf(spec);
+ return index === -1 ? full : `__lazymod_${index}`;
+ });
return { code: `${imports}\n${rewritten}`, map: null };
},
};
diff --git a/packages/@n8n/utils/src/search/sublimeSearch.test.ts b/packages/@n8n/utils/src/search/sublimeSearch.test.ts
index 8cadfdca292..70b5db43e52 100644
--- a/packages/@n8n/utils/src/search/sublimeSearch.test.ts
+++ b/packages/@n8n/utils/src/search/sublimeSearch.test.ts
@@ -37,4 +37,22 @@ describe('sublimeSearch', () => {
},
);
});
+
+ it('should keep only the highest-scoring results when a limit is provided', () => {
+ const items = [{ name: 'x request archive' }, { name: 'request' }, { name: 'zz request' }];
+
+ const results = sublimeSearch('request', items, [{ key: 'name', weight: 1 }], 1);
+
+ expect(results).toHaveLength(1);
+ expect(results[0].item.name).toBe('request');
+ });
+
+ it('should score string array values and ignore non-string array entries', () => {
+ const items = [{ aliases: ['ordinary', 1, 'target alias'] }, { aliases: ['unrelated'] }];
+
+ const results = sublimeSearch('target', items, [{ key: 'aliases', weight: 1 }]);
+
+ expect(results).toHaveLength(1);
+ expect(results[0].item).toBe(items[0]);
+ });
});
diff --git a/packages/@n8n/utils/src/search/sublimeSearch.ts b/packages/@n8n/utils/src/search/sublimeSearch.ts
index adc59bf429d..e7bb726196d 100644
--- a/packages/@n8n/utils/src/search/sublimeSearch.ts
+++ b/packages/@n8n/utils/src/search/sublimeSearch.ts
@@ -219,60 +219,68 @@ function getValue(obj: T, prop: string): unknown {
return result;
}
+function scoreSearchValue(filter: string, value: string, weight: number): number | undefined {
+ if (!fuzzyMatchSimple(filter, value)) return undefined;
+
+ const match = fuzzyMatch(filter, value);
+ if (!match.matched) return undefined;
+
+ return match.outScore * weight;
+}
+
export function sublimeSearch(
filter: string,
data: readonly T[],
keys: Array<{ key: string; weight: number }> = DEFAULT_KEYS,
+ limit?: number,
): Array<{ score: number; item: T }> {
- const results = data.reduce((accu: Array<{ score: number; item: T }>, item: T) => {
- let values: Array<{ value: string; weight: number }> = [];
- keys.forEach(({ key, weight }) => {
+ const results: Array<{ score: number; item: T }> = [];
+
+ for (const item of data) {
+ let itemMatchScore: number | undefined;
+
+ for (const { key, weight } of keys) {
const value = getValue(item, key);
if (Array.isArray(value)) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- values = values.concat(value.map((v) => ({ value: v, weight })));
+ for (const entry of value) {
+ if (typeof entry !== 'string') continue;
+
+ const score = scoreSearchValue(filter, entry, weight);
+ if (score !== undefined && (itemMatchScore === undefined || score > itemMatchScore)) {
+ itemMatchScore = score;
+ }
+ }
} else if (typeof value === 'string') {
- values.push({
- value,
- weight,
- });
+ const score = scoreSearchValue(filter, value, weight);
+ if (score !== undefined && (itemMatchScore === undefined || score > itemMatchScore)) {
+ itemMatchScore = score;
+ }
}
- });
-
- // for each item, check every key and get maximum score
- const itemMatch = values.reduce(
- (
- result: null | { matched: boolean; outScore: number },
- { value, weight }: { value: string; weight: number },
- ) => {
- if (!fuzzyMatchSimple(filter, value)) {
- return result;
- }
-
- const match = fuzzyMatch(filter, value);
- match.outScore *= weight;
-
- const { matched, outScore } = match;
- if (!result && matched) {
- return match;
- }
- if (matched && result && outScore > result.outScore) {
- return match;
- }
- return result;
- },
- null,
- );
-
- if (itemMatch) {
- accu.push({
- score: itemMatch.outScore,
- item,
- });
}
- return accu;
- }, []);
+ if (itemMatchScore !== undefined) {
+ const result: { score: number; item: T } = {
+ score: itemMatchScore,
+ item,
+ };
+
+ if (limit === undefined || results.length < limit) {
+ results.push(result);
+ } else {
+ let lowestIndex = 0;
+ let lowestScore = results[0].score;
+ for (let i = 1; i < results.length; i++) {
+ if (results[i].score < lowestScore) {
+ lowestIndex = i;
+ lowestScore = results[i].score;
+ }
+ }
+ if (result.score > lowestScore) {
+ results[lowestIndex] = result;
+ }
+ }
+ }
+ }
results.sort((a, b) => {
return b.score - a.score;