From 7e8c04299cdbc641a18ee49cd4ce3eb3c2c3a920 Mon Sep 17 00:00:00 2001 From: Albert Alises Date: Wed, 3 Jun 2026 17:54:42 +0200 Subject: [PATCH] perf: Reduce Instance AI memory usage (#31656) Co-authored-by: Jaakko Husso --- .../instance-ai/src/__tests__/index.test.ts | 393 ++++++++++++++++++ packages/@n8n/instance-ai/src/index.ts | 360 ++++++++++++---- .../src/tools/__tests__/nodes.tool.test.ts | 88 ++++ .../@n8n/instance-ai/src/tools/nodes.tool.ts | 21 +- .../__tests__/node-search-engine.test.ts | 17 + .../src/tools/nodes/node-search-engine.ts | 83 +++- packages/@n8n/instance-ai/vite.config.ts | 74 +++- .../utils/src/search/sublimeSearch.test.ts | 18 + .../@n8n/utils/src/search/sublimeSearch.ts | 94 +++-- 9 files changed, 1001 insertions(+), 147 deletions(-) create mode 100644 packages/@n8n/instance-ai/src/__tests__/index.test.ts 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;