fix(core): Keep n8n-docs always loaded so n8n questions do not route to web search (#35537)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mutasem Aldmour 2026-08-07 12:46:40 +02:00 committed by GitHub
parent daba594654
commit e73f0a4654
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 65 additions and 1 deletions

View File

@ -66,6 +66,7 @@ vi.mock('../../tools', () => ({
['workflows', mockBuiltTool(`workflows-${context.runLabel ?? 'unknown'}`)],
['evals', mockBuiltTool(`evals-${context.runLabel ?? 'unknown'}`)],
['research', mockBuiltTool(`research-${context.runLabel ?? 'unknown'}`)],
['n8n-docs', mockBuiltTool(`n8n-docs-${context.runLabel ?? 'unknown'}`)],
['nodes', mockBuiltTool(`nodes-${context.runLabel ?? 'unknown'}`)],
['executions', mockBuiltTool(`executions-${context.runLabel ?? 'unknown'}`)],
['build-workflow', mockBuiltTool(`build-workflow-${context.runLabel ?? 'unknown'}`)],
@ -290,6 +291,39 @@ describe('createInstanceAgent', () => {
}
});
// INS-749: `research` (web-search) is always loaded while `n8n-docs` used to be
// deferred, so answering an n8n question from n8n's own docs cost a search_tools +
// load_tool round trip that web search did not. That price gap pushed the agent to
// web-search things the docs already answer.
it('keeps n8n-docs always loaded so it is no costlier to reach than web search', async () => {
await createInstanceAgent({
modelId: 'test-model',
context: {
runLabel: 'docs-parity-run',
localGatewayStatus: undefined,
licenseHints: undefined,
localMcpServer: undefined,
},
orchestrationContext: {
runId: 'docs-parity-run',
},
memoryConfig: {},
mcpManager: createMcpManagerStub(),
} as never);
const attachedTools = getAttachedTools();
const deferredTools = getDeferredTools();
expect(attachedTools['n8n-docs-docs-parity-run']).toMatchObject({
name: 'n8n-docs-docs-parity-run',
});
expect(deferredTools['n8n-docs-docs-parity-run']).toBeUndefined();
// Parity is the point: both routes must be one call away.
expect(attachedTools['research-docs-parity-run']).toMatchObject({
name: 'research-docs-parity-run',
});
});
it('attaches the orchestration workspace when provided', async () => {
const memoryConfig = {} as never;
const fakeWorkspace = { id: 'thread-runtime-workspace' } as never;

View File

@ -167,6 +167,29 @@ describe('getSystemPrompt — browser/computer-use discoverability', () => {
});
});
// INS-749: n8n-docs is always loaded now, so telling the orchestrator to
// discover it via search_tools is both wrong and a nudge away from the tool
// it should reach for first on n8n questions.
describe('n8n-docs is presented as already available, not as something to discover', () => {
// The Tool Discovery section only renders with tool search on, which is where
// the stale "search for n8n docs" example lived.
const toolSearchOptions = { ...browserCapableOptions, toolSearchEnabled: true };
it('does not offer n8n docs as a search_tools discovery example', () => {
const prompt = getSystemPrompt(toolSearchOptions);
expect(prompt).toContain('## Tool Discovery');
expect(prompt).not.toMatch(/search "n8n docs"/i);
});
it('tells the orchestrator to answer n8n questions from n8n-docs rather than web search', () => {
const prompt = getSystemPrompt(toolSearchOptions);
expect(prompt).toMatch(/prefer[^.]{0,60}n8n-docs/i);
expect(prompt).toMatch(/n8n-docs[^.]{0,120}already (loaded|available)/i);
});
});
describe('browser availability state propagates to the prompt', () => {
it('includes browser automation rules when browser is available', () => {
const prompt = getSystemPrompt({

View File

@ -65,7 +65,9 @@ function getToolDiscoverySection(
${mcpSearchGuidance}When the available tools do not cover the user's request, remember that you have access to more tools. Use \`search_tools\` with keyword queries to find relevant tools, then \`load_tool\` to activate them. Loaded tools persist for the rest of the conversation. When a loaded skill names a tool you do not see, search for that tool name and load it before proceeding.
Examples: ${mcpExamples}search "n8n docs" for \`n8n-docs\`, search "create tasks" for \`create-tasks\`, search "eval" for \`evals\`.
Examples: ${mcpExamples}search "create tasks" for \`create-tasks\`, search "eval" for \`evals\`.
For questions about n8n itself — how a node behaves, the shape of its output, what a parameter does, product semantics — prefer \`n8n-docs\` and the node type definitions, both already loaded and needing no search, over web search, which is for third-party services and APIs.
`;
}

View File

@ -64,6 +64,11 @@ export const ALWAYS_LOADED_TOOL_NAMES = new Set<string>([
DOMAIN_TOOL_IDS.NODES,
ORCHESTRATION_TOOL_IDS.VERIFY_BUILT_WORKFLOW,
DOMAIN_TOOL_IDS.RESEARCH,
// Paired with RESEARCH on purpose: the research tool tells the agent to prefer
// n8n's own docs for n8n questions, but deferring n8n-docs priced that route at
// search_tools + load_tool while web search stayed one call away — so the agent
// web-searched things the docs answer (INS-749).
DOMAIN_TOOL_IDS.N8N_DOCS,
DOMAIN_TOOL_IDS.AGENTS,
// Deferring this one defeats its purpose: it exists for the case where
// nothing is connected, which is exactly when `search_tools` has no MCP tool