mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
refactor(core): Remove node tools searcher module (#32876)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -70,7 +70,6 @@ describe('sanitizeAgentJsonConfig', () => {
|
||||
config: {
|
||||
toolCallConcurrency: 2,
|
||||
webSearch: { enabled: true, provider: 'native' },
|
||||
nodeTools: { enabled: true },
|
||||
},
|
||||
});
|
||||
expect(AgentJsonConfigSchema.safeParse(sanitized).success).toBe(true);
|
||||
|
||||
@@ -300,11 +300,6 @@ export const AgentJsonConfigSchema = z.object({
|
||||
.describe(
|
||||
'Maximum number of agent loop iterations per run. Do not set unless the user explicitly asks.',
|
||||
),
|
||||
nodeTools: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
@@ -358,7 +353,3 @@ export function formatZodErrors(error: ZodError): ConfigValidationError[] {
|
||||
received: 'received' in issue ? String(issue.received) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
export function isNodeToolsEnabled(config: AgentJsonConfig['config']): boolean {
|
||||
return config?.nodeTools?.enabled === true;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Config, Env } from '../decorators';
|
||||
* `N8N_AGENTS_MODULES`. The backend fails fast on unknown tokens so typos
|
||||
* surface at startup instead of silently disabling a feature.
|
||||
*/
|
||||
export const AGENTS_MODULE_NAMES = ['node-tools-searcher'] as const;
|
||||
export const AGENTS_MODULE_NAMES = [] as const;
|
||||
|
||||
export type AgentsModuleName = (typeof AGENTS_MODULE_NAMES)[number];
|
||||
|
||||
@@ -17,9 +17,15 @@ class AgentsModuleArray extends CommaSeparatedStringArray<AgentsModuleName> {
|
||||
super(str);
|
||||
|
||||
for (const name of this) {
|
||||
const moduleName: string = name;
|
||||
if (!AGENTS_MODULE_NAMES.includes(name)) {
|
||||
const validTokens = AGENTS_MODULE_NAMES.join(', ');
|
||||
throw new Error(
|
||||
`Unknown agents module: "${name}". Valid tokens: ${AGENTS_MODULE_NAMES.join(', ')}.`,
|
||||
`Unknown agents module: "${moduleName}". ${
|
||||
validTokens
|
||||
? `Valid tokens: ${validTokens}.`
|
||||
: 'No agents modules are currently supported.'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -35,12 +41,7 @@ export class AgentsConfig {
|
||||
/**
|
||||
* Comma-separated list of agent sub-feature modules to enable. Each entry
|
||||
* gates a specific frontend/runtime capability inside the agents module.
|
||||
* Currently known:
|
||||
* - `node-tools-searcher` — surfaces the "Built-in node tools" toggle in
|
||||
* the agent editor.
|
||||
*
|
||||
* Gates the UI surface only — existing agents persisted with a given
|
||||
* capability turned on continue to run even if its token is removed here.
|
||||
* Add supported module tokens to `AGENTS_MODULE_NAMES`.
|
||||
*/
|
||||
@Env('N8N_AGENTS_MODULES')
|
||||
modules: AgentsModuleArray = [];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { AgentJsonConfig } from '@n8n/api-types';
|
||||
import { mockLogger } from '@n8n/backend-test-utils';
|
||||
import type { AgentsConfig } from '@n8n/config';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import type { CredentialsService } from '@/credentials/credentials.service';
|
||||
@@ -37,13 +36,12 @@ function makeAgent(overrides: Partial<Agent> = {}): Agent {
|
||||
} as unknown as Agent;
|
||||
}
|
||||
|
||||
function makeService(config: Partial<AgentsConfig> = {}) {
|
||||
function makeService() {
|
||||
const agentRepository = mock<AgentRepository>();
|
||||
const agentTaskRepository = mock<AgentTaskRepository>();
|
||||
const agentSkillsService = mock<AgentSkillsService>();
|
||||
const runtimeCacheService = mock<AgentRuntimeCacheService>();
|
||||
const credentialsService = mock<CredentialsService>();
|
||||
const agentsConfig = { modules: [], ...config } as AgentsConfig;
|
||||
|
||||
agentRepository.save.mockImplementation(async (agent) => agent as Agent);
|
||||
credentialsService.findAllCredentialIdsForProject.mockResolvedValue([]);
|
||||
@@ -61,7 +59,6 @@ function makeService(config: Partial<AgentsConfig> = {}) {
|
||||
agentRepository,
|
||||
agentTaskRepository,
|
||||
agentSkillsService,
|
||||
agentsConfig,
|
||||
runtimeCacheService,
|
||||
credentialsService,
|
||||
);
|
||||
@@ -112,27 +109,6 @@ describe('AgentConfigService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('gates config.nodeTools.enabled on the node-tools module', async () => {
|
||||
await expect(
|
||||
makeService().service.validateConfig({
|
||||
...baseConfig,
|
||||
config: { nodeTools: { enabled: true } },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
valid: false,
|
||||
error: expect.stringContaining('node-tools-searcher'),
|
||||
});
|
||||
|
||||
await expect(
|
||||
makeService({
|
||||
modules: ['node-tools-searcher'] as AgentsConfig['modules'],
|
||||
}).service.validateConfig({
|
||||
...baseConfig,
|
||||
config: { nodeTools: { enabled: true } },
|
||||
}),
|
||||
).resolves.toMatchObject({ valid: true });
|
||||
});
|
||||
|
||||
it('accepts draft credentials that are not checked until update sanitization', async () => {
|
||||
const { service } = makeService();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AgentJsonConfigSchema, isNodeToolsEnabled, type AgentJsonConfig } from '@n8n/api-types';
|
||||
import { AgentJsonConfigSchema, type AgentJsonConfig } from '@n8n/api-types';
|
||||
|
||||
const baseConfig: AgentJsonConfig = {
|
||||
name: 'Test Agent',
|
||||
@@ -6,43 +6,6 @@ const baseConfig: AgentJsonConfig = {
|
||||
instructions: 'Be helpful',
|
||||
};
|
||||
|
||||
describe('AgentJsonConfigSchema — config.nodeTools', () => {
|
||||
it('accepts a config without nodeTools', () => {
|
||||
expect(AgentJsonConfigSchema.safeParse({ ...baseConfig, config: {} }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts nodeTools: { enabled: true }', () => {
|
||||
const parsed = AgentJsonConfigSchema.safeParse({
|
||||
...baseConfig,
|
||||
config: { nodeTools: { enabled: true } },
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts nodeTools: { enabled: false }', () => {
|
||||
const parsed = AgentJsonConfigSchema.safeParse({
|
||||
...baseConfig,
|
||||
config: { nodeTools: { enabled: false } },
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects nodeTools without enabled', () => {
|
||||
expect(
|
||||
AgentJsonConfigSchema.safeParse({ ...baseConfig, config: { nodeTools: {} } }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects nodeTools.enabled of the wrong type', () => {
|
||||
expect(
|
||||
AgentJsonConfigSchema.safeParse({
|
||||
...baseConfig,
|
||||
config: { nodeTools: { enabled: 'yes' } },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentJsonConfigSchema — skill refs', () => {
|
||||
it('accepts a skill ref with a valid id', () => {
|
||||
const parsed = AgentJsonConfigSchema.safeParse({
|
||||
@@ -72,24 +35,6 @@ describe('AgentJsonConfigSchema — skill refs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNodeToolsEnabled', () => {
|
||||
it('returns false when config is undefined', () => {
|
||||
expect(isNodeToolsEnabled(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when config has no nodeTools field', () => {
|
||||
expect(isNodeToolsEnabled({})).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when nodeTools.enabled is false', () => {
|
||||
expect(isNodeToolsEnabled({ nodeTools: { enabled: false } })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true only when nodeTools.enabled is explicitly true', () => {
|
||||
expect(isNodeToolsEnabled({ nodeTools: { enabled: true } })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentJsonConfigSchema — subAgents', () => {
|
||||
it('accepts saved agent references', () => {
|
||||
const parsed = AgentJsonConfigSchema.safeParse({
|
||||
|
||||
-11
@@ -43,7 +43,6 @@ import type { AgentTaskService } from '../agent-task.service';
|
||||
import { AgentsService } from '../agents.service';
|
||||
import { AgentTestChatService } from '../agent-test-chat.service';
|
||||
import { AgentValidationService } from '../agent-validation.service';
|
||||
import type { AgentsToolsService } from '../agents-tools.service';
|
||||
import type { AgentHistory } from '../entities/agent-history.entity';
|
||||
import type { AgentTaskSnapshot } from '../entities/agent-task-snapshot.entity';
|
||||
import type { Agent } from '../entities/agent.entity';
|
||||
@@ -118,7 +117,6 @@ function makeRuntimeReconstructionService(
|
||||
mock<N8NCheckpointStorage>(),
|
||||
mock<AgentSecureRuntime>(),
|
||||
mock<EphemeralNodeExecutor>(),
|
||||
mock<AgentsToolsService>(),
|
||||
mock<N8nMemory>(),
|
||||
mock<OauthService>(),
|
||||
{ modules } as unknown as AgentsConfig,
|
||||
@@ -182,7 +180,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
|
||||
let chatIntegrationService: jest.Mocked<ChatIntegrationService>;
|
||||
let agentKnowledgeService: jest.Mocked<AgentKnowledgeService>;
|
||||
let publisher: jest.Mocked<Publisher>;
|
||||
let agentsConfig: AgentsConfig;
|
||||
let globalConfig: jest.Mocked<GlobalConfig>;
|
||||
let telemetry: jest.Mocked<Telemetry>;
|
||||
let runtimeCacheService: AgentRuntimeCacheService;
|
||||
@@ -214,11 +211,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
|
||||
agentKnowledgeService = mock<AgentKnowledgeService>();
|
||||
publisher = mock<Publisher>();
|
||||
publisher.publishCommand.mockResolvedValue();
|
||||
agentsConfig = {
|
||||
modules: [],
|
||||
sandboxEnabled: false,
|
||||
sandboxProvider: '',
|
||||
} as unknown as AgentsConfig;
|
||||
globalConfig = mock<GlobalConfig>({
|
||||
multiMainSetup: { enabled: false },
|
||||
} as Partial<GlobalConfig>);
|
||||
@@ -245,7 +237,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
|
||||
agentRepository,
|
||||
agentTaskRepository,
|
||||
agentSkillsService,
|
||||
agentsConfig,
|
||||
runtimeCacheService,
|
||||
credentialsService,
|
||||
);
|
||||
@@ -349,7 +340,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
|
||||
userId: string;
|
||||
runtimeProfile: 'top-level';
|
||||
config: AgentJsonConfig;
|
||||
nodeToolsEnabled: boolean;
|
||||
subAgentDelegation: {
|
||||
sourcesById: Record<string, never>;
|
||||
availableSubAgents: [];
|
||||
@@ -370,7 +360,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => {
|
||||
model: 'anthropic/claude-sonnet-4-5',
|
||||
instructions: 'Be helpful',
|
||||
},
|
||||
nodeToolsEnabled: false,
|
||||
parentAgentIdForDelegation: agentId,
|
||||
subAgentDelegation: {
|
||||
sourcesById: {},
|
||||
|
||||
@@ -45,7 +45,6 @@ describe('agent telemetry', () => {
|
||||
},
|
||||
config: {
|
||||
webSearch: { enabled: true },
|
||||
nodeTools: { enabled: true },
|
||||
},
|
||||
memory: {
|
||||
enabled: true,
|
||||
@@ -61,8 +60,8 @@ describe('agent telemetry', () => {
|
||||
).toEqual({
|
||||
model: 'anthropic/claude-sonnet-4-5',
|
||||
channels: ['linear', 'slack'],
|
||||
tool_types: ['custom', 'mcp', 'node_tools', 'provider', 'subagent', 'web_search', 'workflow'],
|
||||
tool_count: 7,
|
||||
tool_types: ['custom', 'mcp', 'provider', 'subagent', 'web_search', 'workflow'],
|
||||
tool_count: 6,
|
||||
num_skills: 1,
|
||||
memory_type: 'n8n_observational_episodic',
|
||||
});
|
||||
|
||||
+10
-106
@@ -33,7 +33,6 @@ import type { WorkflowFinderService } from '@/workflows/workflow-finder.service'
|
||||
|
||||
import { AgentRuntimeReconstructionService } from '../agent-runtime-reconstruction.service';
|
||||
import type { AgentKnowledgeSandboxService } from '../agent-knowledge-sandbox.service';
|
||||
import type { AgentsToolsService } from '../agents-tools.service';
|
||||
import type { Agent } from '../entities/agent.entity';
|
||||
import { ChatIntegrationRegistry } from '../integrations/agent-chat-integration';
|
||||
import { ChatIntegrationActionExecutor } from '../integrations/integration-action-executor';
|
||||
@@ -85,7 +84,6 @@ function getInjectedToolNames(): string[] {
|
||||
}
|
||||
|
||||
function makeReconstructionService(
|
||||
agentsToolsService: AgentsToolsService,
|
||||
modules: string[] = [],
|
||||
overrides: {
|
||||
logger?: Logger;
|
||||
@@ -111,7 +109,6 @@ function makeReconstructionService(
|
||||
overrides.n8nCheckpointStorage ?? mock<N8NCheckpointStorage>(),
|
||||
secureRuntime,
|
||||
mock<EphemeralNodeExecutor>(),
|
||||
agentsToolsService,
|
||||
mock<N8nMemory>(),
|
||||
mock<OauthService>(),
|
||||
{
|
||||
@@ -144,79 +141,6 @@ function makeAgentEntity(
|
||||
} as unknown as Agent;
|
||||
}
|
||||
|
||||
describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — node tools gating', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
builtAgent.hasCheckpointStorage.mockReturnValue(true);
|
||||
});
|
||||
|
||||
function setup(options: { nodeToolsModuleEnabled?: boolean } = {}) {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(
|
||||
agentsToolsService,
|
||||
options.nodeToolsModuleEnabled ? ['node-tools-searcher'] : [],
|
||||
);
|
||||
return { service, agentsToolsService, credentialProvider };
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'config.nodeTools is absent and the module is disabled',
|
||||
nodeToolsModuleEnabled: false,
|
||||
schemaConfig: undefined,
|
||||
attaches: false,
|
||||
},
|
||||
{
|
||||
name: 'config.nodeTools is absent and the module is enabled',
|
||||
nodeToolsModuleEnabled: true,
|
||||
schemaConfig: undefined,
|
||||
attaches: false,
|
||||
},
|
||||
{
|
||||
name: 'config.nodeTools.enabled is true but the module is disabled',
|
||||
nodeToolsModuleEnabled: false,
|
||||
schemaConfig: { nodeTools: { enabled: true } },
|
||||
attaches: false,
|
||||
},
|
||||
{
|
||||
name: 'config.nodeTools.enabled is true and the module is enabled',
|
||||
nodeToolsModuleEnabled: true,
|
||||
schemaConfig: { nodeTools: { enabled: true } },
|
||||
attaches: true,
|
||||
},
|
||||
{
|
||||
name: 'config.nodeTools.enabled is false and the module is disabled',
|
||||
nodeToolsModuleEnabled: false,
|
||||
schemaConfig: { nodeTools: { enabled: false } },
|
||||
attaches: false,
|
||||
},
|
||||
{
|
||||
name: 'config.nodeTools.enabled is false and the module is enabled',
|
||||
nodeToolsModuleEnabled: true,
|
||||
schemaConfig: { nodeTools: { enabled: false } },
|
||||
attaches: false,
|
||||
},
|
||||
])('$name', async ({ nodeToolsModuleEnabled, schemaConfig, attaches }) => {
|
||||
const { service, agentsToolsService, credentialProvider } = setup({
|
||||
nodeToolsModuleEnabled,
|
||||
});
|
||||
const entity = makeAgentEntity(schemaConfig);
|
||||
|
||||
await service.reconstructFromAgentEntity(entity, credentialProvider, 'user-1');
|
||||
|
||||
if (attaches) {
|
||||
expect(agentsToolsService.getRuntimeTools).toHaveBeenCalledWith(
|
||||
credentialProvider,
|
||||
'project-1',
|
||||
);
|
||||
} else {
|
||||
expect(agentsToolsService.getRuntimeTools).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — MCP wiring', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -233,10 +157,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — MCP w
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService);
|
||||
const service = makeReconstructionService();
|
||||
return { service, credentialProvider };
|
||||
}
|
||||
|
||||
@@ -284,10 +206,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService);
|
||||
const service = makeReconstructionService();
|
||||
return { service, credentialProvider };
|
||||
}
|
||||
|
||||
@@ -356,10 +276,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
});
|
||||
|
||||
it('uses the shared default maxChildren when config does not override it', async () => {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService, []);
|
||||
const service = makeReconstructionService();
|
||||
|
||||
await service.reconstructFromAgentEntity(makeAgentEntity(), credentialProvider, 'user-1');
|
||||
|
||||
@@ -369,10 +287,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
});
|
||||
|
||||
it('uses subAgents.maxChildren over the SDK default', async () => {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService, []);
|
||||
const service = makeReconstructionService();
|
||||
const entity = makeAgentEntity(undefined, { subAgents: { maxChildren: 2 } });
|
||||
|
||||
await service.reconstructFromAgentEntity(entity, credentialProvider, 'user-1');
|
||||
@@ -383,8 +299,6 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
});
|
||||
|
||||
it('resolves subAgents.modelsByDifficulty into delegate tool metadata', async () => {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
credentialProvider.resolve.mockImplementation(async (credentialId: string) => {
|
||||
if (credentialId === 'low-cred') {
|
||||
@@ -395,7 +309,7 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
}
|
||||
throw new Error(`unexpected credential ${credentialId}`);
|
||||
});
|
||||
const service = makeReconstructionService(agentsToolsService, []);
|
||||
const service = makeReconstructionService();
|
||||
const entity = makeAgentEntity(undefined, {
|
||||
subAgents: {
|
||||
modelsByDifficulty: {
|
||||
@@ -421,8 +335,6 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
});
|
||||
|
||||
it('resolves inline child provider tools for the child model provider', async () => {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
credentialProvider.resolve.mockImplementation(async (credentialId: string) => {
|
||||
if (credentialId === 'high-cred') {
|
||||
@@ -430,7 +342,7 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
}
|
||||
throw new Error(`unexpected credential ${credentialId}`);
|
||||
});
|
||||
const service = makeReconstructionService(agentsToolsService, []);
|
||||
const service = makeReconstructionService();
|
||||
const entity = makeAgentEntity(
|
||||
{ webSearch: { enabled: true } },
|
||||
{
|
||||
@@ -457,10 +369,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a
|
||||
});
|
||||
|
||||
it('omits inlineSubAgentModelsByDifficulty when no difficulty mappings are configured', async () => {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService, []);
|
||||
const service = makeReconstructionService();
|
||||
|
||||
await service.reconstructFromAgentEntity(makeAgentEntity(), credentialProvider, 'user-1');
|
||||
|
||||
@@ -485,10 +395,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — n8n c
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService);
|
||||
const service = makeReconstructionService();
|
||||
return { service, credentialProvider };
|
||||
}
|
||||
|
||||
@@ -547,10 +455,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — check
|
||||
};
|
||||
const n8nCheckpointStorage = mock<N8NCheckpointStorage>();
|
||||
n8nCheckpointStorage.getStorage.mockReturnValue(scopedStorage);
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService, [], { n8nCheckpointStorage });
|
||||
const service = makeReconstructionService([], { n8nCheckpointStorage });
|
||||
|
||||
await service.reconstructFromAgentEntity(makeAgentEntity(), credentialProvider, 'user-1');
|
||||
|
||||
@@ -567,10 +473,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromResolvedSource — su
|
||||
});
|
||||
|
||||
it('does not inject top-level integration context/action tools', async () => {
|
||||
const agentsToolsService = mock<AgentsToolsService>();
|
||||
agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]);
|
||||
const credentialProvider = mock<CredentialProvider>();
|
||||
const service = makeReconstructionService(agentsToolsService);
|
||||
const service = makeReconstructionService();
|
||||
|
||||
const config: AgentJsonConfig = {
|
||||
name: 'Child',
|
||||
|
||||
@@ -4,22 +4,15 @@ import {
|
||||
AGENT_BUILDER_AVAILABLE_AI_UTILITY_TOOL_NODE_TYPES,
|
||||
AGENT_BUILDER_HIDDEN_AVAILABLE_TOOL_NODE_TYPES,
|
||||
} from '@n8n/api-types';
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
import { validateNodeConfig } from '@n8n/workflow-sdk';
|
||||
|
||||
import type { EphemeralNodeExecutor } from '@/node-execution';
|
||||
import type { NodeCatalogService } from '@/node-catalog';
|
||||
|
||||
import {
|
||||
AgentsToolsService,
|
||||
isAgentToolNodeType,
|
||||
isExecutableNodeType,
|
||||
AgentsToolsService,
|
||||
} from '../agents-tools.service';
|
||||
|
||||
jest.mock('@n8n/workflow-sdk', () => ({
|
||||
validateNodeConfig: jest.fn().mockReturnValue({ valid: true, errors: [] }),
|
||||
}));
|
||||
|
||||
const ctx = {
|
||||
resumeData: undefined,
|
||||
suspend: jest.fn().mockResolvedValue(undefined as never),
|
||||
@@ -34,13 +27,9 @@ function makeService() {
|
||||
});
|
||||
nodeCatalogService.getNodeTypes.mockResolvedValue('node-types-string');
|
||||
|
||||
const ephemeralNodeExecutor = mock<EphemeralNodeExecutor>();
|
||||
const service = new AgentsToolsService(nodeCatalogService);
|
||||
|
||||
const logger = mock<Logger>();
|
||||
|
||||
const service = new AgentsToolsService(logger, nodeCatalogService, ephemeralNodeExecutor);
|
||||
|
||||
return { service, nodeCatalogService, ephemeralNodeExecutor, logger };
|
||||
return { service, nodeCatalogService };
|
||||
}
|
||||
|
||||
function makeCredentialProvider(
|
||||
@@ -64,21 +53,6 @@ describe('AgentsToolsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuntimeTools()', () => {
|
||||
it('returns all four tools including run_node_tool', () => {
|
||||
const { service } = makeService();
|
||||
const names = service
|
||||
.getRuntimeTools(makeCredentialProvider(), 'project-1')
|
||||
.map((t) => t.name);
|
||||
expect(names).toEqual([
|
||||
'search_nodes',
|
||||
'get_node_types',
|
||||
'list_credentials',
|
||||
'run_node_tool',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list_credentials handler', () => {
|
||||
function getListTool(service: AgentsToolsService, provider: CredentialProvider) {
|
||||
return service.getSharedTools(provider, 'hint').find((t) => t.name === 'list_credentials')!;
|
||||
@@ -137,7 +111,7 @@ describe('AgentsToolsService', () => {
|
||||
describe('search_nodes handler', () => {
|
||||
function getSearchTool(service: AgentsToolsService) {
|
||||
return service
|
||||
.getRuntimeTools(makeCredentialProvider(), 'project-1')
|
||||
.getSharedTools(makeCredentialProvider(), 'hint')
|
||||
.find((t) => t.name === 'search_nodes')!;
|
||||
}
|
||||
|
||||
@@ -198,7 +172,7 @@ describe('AgentsToolsService', () => {
|
||||
describe('get_node_types handler', () => {
|
||||
function getTypesTool(service: AgentsToolsService) {
|
||||
return service
|
||||
.getRuntimeTools(makeCredentialProvider(), 'project-1')
|
||||
.getSharedTools(makeCredentialProvider(), 'hint')
|
||||
.find((t) => t.name === 'get_node_types')!;
|
||||
}
|
||||
|
||||
@@ -223,132 +197,4 @@ describe('AgentsToolsService', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('run_node_tool handler', () => {
|
||||
function getRunTool(service: AgentsToolsService) {
|
||||
return service
|
||||
.getRuntimeTools(makeCredentialProvider(), 'project-1')
|
||||
.find((t) => t.name === 'run_node_tool')!;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(validateNodeConfig).mockReturnValue({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it('refuses to run trigger nodes', async () => {
|
||||
const { service, ephemeralNodeExecutor } = makeService();
|
||||
|
||||
const result = await getRunTool(service).handler!(
|
||||
{ nodeType: 'n8n-nodes-base.scheduleTrigger', nodeTypeVersion: 1 },
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(ephemeralNodeExecutor.executeInline).not.toHaveBeenCalled();
|
||||
expect(result).toMatchObject({
|
||||
status: 'error',
|
||||
message: expect.stringContaining('scheduleTrigger'),
|
||||
});
|
||||
});
|
||||
|
||||
it('skips validation and calls executeInline when nodeParameters is absent', async () => {
|
||||
const { service, ephemeralNodeExecutor } = makeService();
|
||||
ephemeralNodeExecutor.executeInline.mockResolvedValue({ status: 'success' } as never);
|
||||
|
||||
await getRunTool(service).handler!(
|
||||
{ nodeType: 'n8n-nodes-base.httpRequest', nodeTypeVersion: 4 },
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(validateNodeConfig).not.toHaveBeenCalled();
|
||||
expect(ephemeralNodeExecutor.executeInline).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('validates nodeParameters via validateNodeConfig before executing', async () => {
|
||||
const { service, ephemeralNodeExecutor } = makeService();
|
||||
ephemeralNodeExecutor.executeInline.mockResolvedValue({ status: 'success' } as never);
|
||||
|
||||
await getRunTool(service).handler!(
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeTypeVersion: 4,
|
||||
nodeParameters: { url: 'https://example.com' },
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(validateNodeConfig).toHaveBeenCalledWith(
|
||||
'n8n-nodes-base.httpRequest',
|
||||
4,
|
||||
{
|
||||
parameters: { url: 'https://example.com' },
|
||||
},
|
||||
{ isToolNode: true },
|
||||
);
|
||||
expect(ephemeralNodeExecutor.executeInline).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns an error and skips executeInline when validation fails', async () => {
|
||||
jest.mocked(validateNodeConfig).mockReturnValue({
|
||||
valid: false,
|
||||
errors: [{ path: 'method', message: 'Field "method" has invalid value.' }],
|
||||
});
|
||||
const { service, ephemeralNodeExecutor } = makeService();
|
||||
|
||||
const result = await getRunTool(service).handler!(
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.httpRequest',
|
||||
nodeTypeVersion: 4,
|
||||
nodeParameters: { method: 'DELETE' },
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(ephemeralNodeExecutor.executeInline).not.toHaveBeenCalled();
|
||||
expect(result).toMatchObject({
|
||||
status: 'error',
|
||||
message: expect.stringContaining('"method"'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a structured error when executeInline throws', async () => {
|
||||
const { service, ephemeralNodeExecutor, logger } = makeService();
|
||||
ephemeralNodeExecutor.executeInline.mockRejectedValue(new Error('boom'));
|
||||
|
||||
const result = await getRunTool(service).handler!(
|
||||
{ nodeType: 'n8n-nodes-base.httpRequest', nodeTypeVersion: 4 },
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'error',
|
||||
message: expect.stringContaining('boom'),
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('maps inputData, passes credentials and projectId through to executeInline', async () => {
|
||||
const executionResult = { status: 'success', data: [{ json: { ok: true } }] };
|
||||
const { service, ephemeralNodeExecutor } = makeService();
|
||||
ephemeralNodeExecutor.executeInline.mockResolvedValue(executionResult as never);
|
||||
|
||||
const result = await getRunTool(service).handler!(
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.gmail',
|
||||
nodeTypeVersion: 2,
|
||||
credentials: { gmailOAuth2: { id: 'cred-1', name: 'My Gmail' } },
|
||||
inputData: { to: 'user@example.com' },
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(ephemeralNodeExecutor.executeInline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
credentialDetails: { gmailOAuth2: { id: 'cred-1', name: 'My Gmail' } },
|
||||
inputData: [{ json: { to: 'user@example.com' } }],
|
||||
projectId: 'project-1',
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(executionResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { extractFromAIParameters } from '@n8n/ai-utilities/fromai-helpers';
|
||||
import {
|
||||
AgentJsonConfigSchema,
|
||||
isNodeToolsEnabled,
|
||||
sanitizeAgentJsonConfig,
|
||||
type AgentJsonConfig,
|
||||
type AgentJsonToolConfig,
|
||||
} from '@n8n/api-types';
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { AgentsConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
import { UserError, type INodeParameters } from 'n8n-workflow';
|
||||
|
||||
@@ -34,15 +32,10 @@ export class AgentConfigService {
|
||||
private readonly agentRepository: AgentRepository,
|
||||
private readonly agentTaskRepository: AgentTaskRepository,
|
||||
private readonly agentSkillsService: AgentSkillsService,
|
||||
private readonly agentsConfig: AgentsConfig,
|
||||
private readonly runtimeCacheService: AgentRuntimeCacheService,
|
||||
private readonly credentialsService: CredentialsService,
|
||||
) {}
|
||||
|
||||
private isNodeToolsModuleEnabled(): boolean {
|
||||
return this.agentsConfig.modules.includes('node-tools-searcher');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON config for an agent.
|
||||
*/
|
||||
@@ -74,14 +67,6 @@ export class AgentConfigService {
|
||||
|
||||
const config = parsed.data;
|
||||
|
||||
if (isNodeToolsEnabled(config.config) && !this.isNodeToolsModuleEnabled()) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
'config.nodeTools.enabled requires the node-tools-searcher agents module to be enabled.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
this.validateNodeToolExpressions(config);
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ToolDescriptor,
|
||||
} from '@n8n/agents';
|
||||
import {
|
||||
isNodeToolsEnabled,
|
||||
N8N_CHAT_ACTION_TOOL_NAME,
|
||||
N8N_CHAT_CONTEXT_TOOL_NAME,
|
||||
N8N_CHAT_INTEGRATION_TYPE,
|
||||
@@ -38,7 +37,6 @@ import { createAiMcpFetch, createAiProxyFetch } from '@/utils/ai-proxy-fetch';
|
||||
import { WorkflowRunner } from '@/workflow-runner';
|
||||
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';
|
||||
|
||||
import { AgentsToolsService } from './agents-tools.service';
|
||||
import { Agent } from './entities/agent.entity';
|
||||
import { ChatIntegrationRegistry } from './integrations/agent-chat-integration';
|
||||
import {
|
||||
@@ -133,7 +131,6 @@ export class AgentRuntimeReconstructionService {
|
||||
private readonly n8nCheckpointStorage: N8NCheckpointStorage,
|
||||
private readonly secureRuntime: AgentSecureRuntime,
|
||||
private readonly ephemeralNodeExecutor: EphemeralNodeExecutor,
|
||||
private readonly agentsToolsService: AgentsToolsService,
|
||||
private readonly n8nMemory: N8nMemory,
|
||||
private readonly oauthService: OauthService,
|
||||
private readonly agentsConfig: AgentsConfig,
|
||||
@@ -272,7 +269,6 @@ export class AgentRuntimeReconstructionService {
|
||||
userId,
|
||||
runtimeProfile,
|
||||
config,
|
||||
nodeToolsEnabled: this.shouldAttachNodeTools(config.config),
|
||||
subAgentDelegation,
|
||||
parentAgentIdForDelegation: parentAgentIdForDelegation ?? memoryOwnerAgentId,
|
||||
integrationType,
|
||||
@@ -312,14 +308,6 @@ export class AgentRuntimeReconstructionService {
|
||||
return (_params: AgentJsonMemoryConfig) => this.n8nMemory.getImplementation(agentId);
|
||||
}
|
||||
|
||||
private shouldAttachNodeTools(config: AgentJsonConfig['config']): boolean {
|
||||
return this.isNodeToolsModuleEnabled() && isNodeToolsEnabled(config);
|
||||
}
|
||||
|
||||
private isNodeToolsModuleEnabled(): boolean {
|
||||
return this.agentsConfig.modules.includes('node-tools-searcher');
|
||||
}
|
||||
|
||||
private makeToolResolver(projectId: string, userId: string): ToolResolver {
|
||||
return async (ref: AgentJsonToolConfig) => {
|
||||
if (ref.type === 'workflow') {
|
||||
@@ -359,7 +347,6 @@ export class AgentRuntimeReconstructionService {
|
||||
userId: string;
|
||||
runtimeProfile: AgentRuntimeProfile;
|
||||
config: AgentJsonConfig;
|
||||
nodeToolsEnabled: boolean;
|
||||
subAgentDelegation: SubAgentDelegationConfig;
|
||||
parentAgentIdForDelegation: string;
|
||||
integrationType?: string;
|
||||
@@ -373,7 +360,6 @@ export class AgentRuntimeReconstructionService {
|
||||
userId,
|
||||
runtimeProfile,
|
||||
config,
|
||||
nodeToolsEnabled,
|
||||
subAgentDelegation,
|
||||
parentAgentIdForDelegation,
|
||||
integrationType,
|
||||
@@ -449,10 +435,6 @@ export class AgentRuntimeReconstructionService {
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeToolsEnabled) {
|
||||
agent.tool(this.agentsToolsService.getRuntimeTools(credentialProvider, projectId));
|
||||
}
|
||||
|
||||
if (runtimeProfile === 'top-level') {
|
||||
await this.attachSubAgentDelegationTool({
|
||||
agent,
|
||||
|
||||
@@ -23,7 +23,6 @@ export function buildAgentConfigurationTelemetryFromConfig(
|
||||
]);
|
||||
const providerToolCount = Object.keys(config?.providerTools ?? {}).length;
|
||||
const hasWebSearch = config?.config?.webSearch?.enabled === true;
|
||||
const hasNodeTools = config?.config?.nodeTools?.enabled === true;
|
||||
const mcpServerCount = config?.mcpServers?.length ?? 0;
|
||||
const subAgentCount = config?.subAgents?.agents?.length ?? 0;
|
||||
|
||||
@@ -31,7 +30,6 @@ export function buildAgentConfigurationTelemetryFromConfig(
|
||||
if (mcpServerCount > 0) toolTypes.add('mcp');
|
||||
if (providerToolCount > 0) toolTypes.add('provider');
|
||||
if (hasWebSearch) toolTypes.add('web_search');
|
||||
if (hasNodeTools) toolTypes.add('node_tools');
|
||||
if (subAgentCount > 0) toolTypes.add('subagent');
|
||||
|
||||
return {
|
||||
@@ -43,7 +41,6 @@ export function buildAgentConfigurationTelemetryFromConfig(
|
||||
mcpServerCount +
|
||||
providerToolCount +
|
||||
(hasWebSearch ? 1 : 0) +
|
||||
(hasNodeTools ? 1 : 0) +
|
||||
subAgentCount,
|
||||
num_skills: config?.skills?.length ?? 0,
|
||||
memory_type: getMemoryType(config),
|
||||
|
||||
@@ -4,17 +4,14 @@ import {
|
||||
AGENT_BUILDER_AVAILABLE_AI_UTILITY_TOOL_NODE_TYPES,
|
||||
AGENT_BUILDER_HIDDEN_AVAILABLE_TOOL_NODE_TYPES,
|
||||
} from '@n8n/api-types';
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { Service } from '@n8n/di';
|
||||
import { validateNodeConfig } from '@n8n/workflow-sdk';
|
||||
import { isToolType, isTriggerNodeType } from 'n8n-workflow';
|
||||
import type { IDataObject, INodeParameters } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { MCP_REGISTRY_PACKAGE_NAME } from '../mcp-registry/node-description-transform';
|
||||
|
||||
import { NodeCatalogService } from '@/node-catalog';
|
||||
import { EphemeralNodeExecutor, isAgentProviderNode } from '@/node-execution';
|
||||
import { isAgentProviderNode } from '@/node-execution';
|
||||
|
||||
type NodeRequest =
|
||||
| string
|
||||
@@ -104,39 +101,13 @@ const listCredentialsInputSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
const runNodeInputSchema = z.object({
|
||||
nodeType: z.string().describe('Tool node type identifier from search_nodes'),
|
||||
nodeTypeVersion: nodeVersionSchema,
|
||||
nodeParameters: z
|
||||
.record(z.unknown())
|
||||
.optional()
|
||||
.describe(
|
||||
'Static node config. Use expressions like ={{ $json.url }} to reference inputData fields.',
|
||||
),
|
||||
credentials: z
|
||||
.record(z.object({ id: z.string(), name: z.string() }))
|
||||
.optional()
|
||||
.describe('Credential slot → { id, name }. Copy from list_credentials results.'),
|
||||
inputData: z
|
||||
.record(z.unknown())
|
||||
.optional()
|
||||
.describe('Runtime input, available as $json inside nodeParameters expressions.'),
|
||||
});
|
||||
|
||||
@Service()
|
||||
export class AgentsToolsService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly nodeCatalogService: NodeCatalogService,
|
||||
private readonly ephemeralNodeExecutor: EphemeralNodeExecutor,
|
||||
) {}
|
||||
constructor(private readonly nodeCatalogService: NodeCatalogService) {}
|
||||
|
||||
/**
|
||||
* Tools usable from both the builder and the agent runtime.
|
||||
*
|
||||
* `listCredentialsUsageHint` lets each caller tailor the `list_credentials`
|
||||
* description to its flow — the runtime points at `run_node_tool`, while the
|
||||
* builder points at code generation.
|
||||
* Tools usable by the builder while configuring node-backed tools.
|
||||
* `listCredentialsUsageHint` lets callers tailor the credential guidance.
|
||||
*/
|
||||
getSharedTools(
|
||||
credentialProvider: CredentialProvider,
|
||||
@@ -149,17 +120,6 @@ export class AgentsToolsService {
|
||||
];
|
||||
}
|
||||
|
||||
/** Shared tools plus the runtime-only `run_node_tool` which binds to a project. */
|
||||
getRuntimeTools(credentialProvider: CredentialProvider, projectId: string): BuiltTool[] {
|
||||
return [
|
||||
...this.getSharedTools(
|
||||
credentialProvider,
|
||||
'Call this before run_node_tool to know which credential to pass.',
|
||||
),
|
||||
this.buildRunNodeTool(projectId),
|
||||
];
|
||||
}
|
||||
|
||||
private buildSearchNodesTool(): BuiltTool {
|
||||
return new Tool('search_nodes')
|
||||
.description(
|
||||
@@ -216,70 +176,11 @@ export class AgentsToolsService {
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private buildRunNodeTool(projectId: string): BuiltTool {
|
||||
return new Tool('run_node_tool')
|
||||
.description(
|
||||
'Execute an n8n node for the current request. ' +
|
||||
'Use the tool nodeType and nodeTypeVersion from search_nodes. ' +
|
||||
'Call get_node_types first to understand what nodeParameters the node accepts. ' +
|
||||
'nodeParameters holds static node config; use n8n expressions like ={{ $json.url }} to map inputData fields. ' +
|
||||
'credentials maps slot names to { id, name } — copy from the list_credentials results. ' +
|
||||
'inputData is the runtime payload available as $json inside expressions. ' +
|
||||
'Parameters are validated against the node schema before execution.',
|
||||
)
|
||||
.input(runNodeInputSchema)
|
||||
.handler(async ({ nodeType, nodeTypeVersion, nodeParameters, credentials, inputData }) => {
|
||||
if (!isExecutableNodeType(nodeType)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: `Node type "${nodeType}" cannot be executed directly — trigger nodes are not supported here.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (nodeParameters) {
|
||||
const { valid, errors } = validateNodeConfig(
|
||||
nodeType,
|
||||
nodeTypeVersion,
|
||||
{
|
||||
parameters: nodeParameters,
|
||||
},
|
||||
{ isToolNode: true },
|
||||
);
|
||||
if (!valid) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: `Invalid nodeParameters: ${errors.map((e) => e.message).join('; ')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.ephemeralNodeExecutor.executeInline({
|
||||
nodeType,
|
||||
nodeTypeVersion,
|
||||
nodeParameters: (nodeParameters ?? {}) as INodeParameters,
|
||||
credentialDetails: credentials,
|
||||
inputData: [{ json: (inputData ?? {}) as IDataObject }],
|
||||
projectId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.warn('run_node_tool execution failed', { nodeType, error });
|
||||
return {
|
||||
status: 'error',
|
||||
message: `Node execution failed: ${message}`,
|
||||
};
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The catalog's `getNodeTypes` signature expects `version` as a string (matching the
|
||||
* code-builder tool's wire format). Our public schema uses `number` for consistency
|
||||
* with `run_node_tool`; adapt at the boundary.
|
||||
* builder tool's wire format); adapt at the boundary.
|
||||
*/
|
||||
function normalizeNodeRequestForCatalog(req: NodeRequest):
|
||||
| string
|
||||
|
||||
@@ -331,9 +331,9 @@ export class EphemeralNodeExecutor {
|
||||
// Validation failures (unknown node type, trigger nodes, blacklisted
|
||||
// operations like send-and-wait) need to surface to the agent as a
|
||||
// tool error rather than crashing silently. Returning the standard
|
||||
// `{ status: 'error', error }` shape lets `run_node_tool` translate
|
||||
// it into a tool-result the LLM sees AND lets the ExecutionRecorder
|
||||
// record it as a failed tool call in the session timeline.
|
||||
// `{ status: 'error', error }` shape lets node tools surface the error
|
||||
// to the LLM and lets the ExecutionRecorder record it as a failed tool
|
||||
// call in the session timeline.
|
||||
try {
|
||||
this.validateNodeForExecution(
|
||||
request.nodeType,
|
||||
|
||||
@@ -6748,8 +6748,6 @@
|
||||
"agents.builder.tools.title": "Tools",
|
||||
"agents.builder.tools.count": "{count} tool configured | {count} tools configured",
|
||||
"agents.builder.tools.add": "Add tool",
|
||||
"agents.builder.tools.builtIn.title": "Built-in node tools",
|
||||
"agents.builder.tools.builtIn.hint": "Let the agent search the n8n node catalog and execute nodes on demand.",
|
||||
"agents.builder.tools.empty": "No tools added yet.",
|
||||
"agents.builder.tools.type.workflow": "Workflow",
|
||||
"agents.builder.tools.type.custom": "Custom tool",
|
||||
|
||||
@@ -183,12 +183,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
return isOtelCustomSpanAttributesLicensed && isOtelModuleActive;
|
||||
});
|
||||
|
||||
// Opt-in flag: the `node-tools-searcher` token must be listed in the backend
|
||||
// `N8N_AGENTS_MODULES` env var for this to evaluate true.
|
||||
const isAgentsNodeToolsFeatureEnabled = computed(() =>
|
||||
isAgentModuleActive('node-tools-searcher'),
|
||||
);
|
||||
|
||||
// Opt-in flag: requires `N8N_AGENTS_AI_SANDBOX_ENABLED=true` and
|
||||
// `N8N_AGENTS_AI_SANDBOX_PROVIDER=daytona` on the backend.
|
||||
const isAgentsKnowledgeBaseFeatureEnabled = computed(
|
||||
@@ -492,7 +486,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
isDataTableFeatureEnabled,
|
||||
isChatFeatureEnabled,
|
||||
isOtelCustomSpanAttributesEnabled,
|
||||
isAgentsNodeToolsFeatureEnabled,
|
||||
isAgentsKnowledgeBaseFeatureEnabled,
|
||||
isPublicChatTriggerDisabled,
|
||||
};
|
||||
|
||||
@@ -15,14 +15,12 @@ import {
|
||||
N8nIcon,
|
||||
N8nIconButton,
|
||||
N8nCard,
|
||||
N8nSwitch2,
|
||||
N8nText,
|
||||
N8nTooltip,
|
||||
} from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import type { INode, INodeTypeDescription } from 'n8n-workflow';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
||||
@@ -49,22 +47,6 @@ const emit = defineEmits<{
|
||||
'remove-tool': [index: number];
|
||||
}>();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const nodeToolsFeatureEnabled = computed(() => settingsStore.isAgentsNodeToolsFeatureEnabled);
|
||||
|
||||
const nodeToolsEnabled = computed<boolean>(
|
||||
() => props.config?.config?.nodeTools?.enabled !== false,
|
||||
);
|
||||
|
||||
function setNodeToolsEnabled(enabled: boolean) {
|
||||
emit('update:config', {
|
||||
config: {
|
||||
...(props.config?.config ?? {}),
|
||||
nodeTools: { enabled },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
@@ -169,21 +151,6 @@ const totalCount = computed(() => props.tools.length);
|
||||
</template>
|
||||
</AgentPanelHeader>
|
||||
|
||||
<div v-if="nodeToolsFeatureEnabled" :class="$style.toggleRow">
|
||||
<div :class="$style.toggleText">
|
||||
<N8nText :bold="true">{{ i18n.baseText('agents.builder.tools.builtIn.title') }}</N8nText>
|
||||
<N8nText size="small" color="text-light">
|
||||
{{ i18n.baseText('agents.builder.tools.builtIn.hint') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
<N8nSwitch2
|
||||
size="large"
|
||||
data-testid="node-tools-toggle"
|
||||
:model-value="nodeToolsEnabled"
|
||||
@update:model-value="setNodeToolsEnabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="totalCount === 0" :class="$style.empty">
|
||||
<N8nText size="small" color="text-light">{{
|
||||
i18n.baseText('agents.builder.tools.empty')
|
||||
@@ -337,21 +304,6 @@ const totalCount = computed(() => props.tools.length);
|
||||
scrollbar-color: var(--border-color) transparent;
|
||||
}
|
||||
|
||||
.toggleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing--sm);
|
||||
padding: var(--spacing--xs) 0;
|
||||
border-bottom: var(--border);
|
||||
}
|
||||
|
||||
.toggleText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--5xs);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--spacing--lg);
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user