mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-05 02:59:27 +02:00
perf: Reduce Instance AI memory usage (#31656)
Co-authored-by: Jaakko Husso <jaakko@n8n.io>
This commit is contained in:
parent
68b205317a
commit
7e8c04299c
393
packages/@n8n/instance-ai/src/__tests__/index.test.ts
Normal file
393
packages/@n8n/instance-ai/src/__tests__/index.test.ts
Normal file
|
|
@ -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) =>
|
||||
`<untrusted_data source="${source}">${content}</untrusted_data>`,
|
||||
}));
|
||||
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<string, unknown> }) => ({
|
||||
...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(
|
||||
'<untrusted_data source="https://example.com">',
|
||||
);
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 = <TValue>(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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<typeof fullInputSchema>;
|
||||
|
||||
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<FullInput, { action: 'search' }>,
|
||||
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':
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<string, NodeSearchResult[]>();
|
||||
|
||||
private readonly connectionSearchCache = new Map<string, NodeSearchResult[]>();
|
||||
|
||||
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<string, ScoredNode>();
|
||||
for (const term of queryTerms) {
|
||||
const termResults = sublimeSearch<SearchableNodeType>(term, candidates, NODE_SEARCH_KEYS);
|
||||
const termResults = sublimeSearch<SearchableNodeType>(
|
||||
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<SearchableNodeType>(query, candidates, NODE_SEARCH_KEYS);
|
||||
searchResults = sublimeSearch<SearchableNodeType>(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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -219,60 +219,68 @@ function getValue<T extends object>(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<T extends object>(
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user