From ea448c6f61fb7cae06cfbeccbd32713a70d112ca Mon Sep 17 00:00:00 2001 From: "n8n-assistant[bot]" <100856346+n8n-assistant[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:39:32 +0000 Subject: [PATCH] fix(core): Resume tool calls for deferred tools that a skill loaded (backport to release-candidate/2.43.x) (#40746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Raúl Gómez Morales Co-authored-by: Mutasem Aldmour --- .../runtime/__tests__/agent-runtime.test.ts | 49 +++++++++++++++++++ .../agents/src/runtime/loop/agent-runtime.ts | 8 +-- .../runtime/tools/deferred-tool-manager.ts | 5 ++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts b/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts index 91bdd09977d..aa5cc821d56 100644 --- a/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts +++ b/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts @@ -8753,6 +8753,55 @@ describe('AgentRuntime — mid-run observation', () => { expect(JSON.stringify(capturedCall(2))).not.toContain('Old workflow policy.'); }); + it('resumes a deferred tool that a skill dependency loaded', async () => { + const source = createRuntimeSkillSource([ + { + id: 'planning', + name: 'planning', + description: 'Plan multi-step work.', + instructions: 'Ask the user to approve the plan.', + dependencies: { tools: ['approve'] }, + }, + ]); + const checkpointStore = makeClaimingCheckpointStore(); + const memory = new InMemoryMemory(); + const options = { + skillSource: source, + tools: createRuntimeSkillTools(source), + deferredTools: [makeInterruptibleTool()], + checkpointStorage: checkpointStore, + }; + const first = buildMidRunRuntime(memory, options); + generateText + .mockResolvedValueOnce( + makeGenerateWithToolCall('load-planning', 'load_skill', { skillId: 'planning' }), + ) + .mockResolvedValueOnce( + makeGenerateWithToolCall('confirm', 'approve', { question: 'Run the plan?' }), + ); + const result = await first.generate('Plan it', { persistence: PERSISTENCE }); + await first.dispose(); + const suspension = result.pendingSuspend?.[0]; + if (!suspension) throw new Error('Expected a plan confirmation'); + + const resumed = buildMidRunRuntime(memory, options); + generateText.mockResolvedValueOnce(makeGenerateSuccess('Plan approved.')); + const resumeResult = await resumed.resume( + 'generate', + { approved: true }, + { runId: suspension.runId, toolCallId: suspension.toolCallId }, + ); + await resumed.dispose(); + + expect(resumeResult.error).toBeUndefined(); + expect(resumeResult.toolCalls).toEqual( + expect.arrayContaining([ + expect.objectContaining({ tool: 'approve', output: { approved: true } }), + ]), + ); + expect(capturedCall(2).tools).toHaveProperty('approve'); + }); + it('merges system messages after compaction for custom OpenAI-compatible endpoints', async () => { const memory = new InMemoryMemory(); const runtime = buildMidRunRuntime(memory, { diff --git a/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts b/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts index 83143657b78..0684470c983 100644 --- a/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts +++ b/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts @@ -509,9 +509,11 @@ export class AgentRuntime { const list = await this.restoreCheckpointMessages(state); - const tool = this.context - .getCurrentTools(state.persistence) - .find((t) => t.name === toolCall.toolName); + // A skill dependency loads a deferred tool without a `load_tool` record, and + // skills restore only when the loop starts. Accept any registered deferred tool. + const tool = + this.context.getCurrentTools(state.persistence).find((t) => t.name === toolCall.toolName) ?? + this.deferredToolManager?.getTool(toolCall.toolName); if (!tool) throw new Error(`Tool ${toolCall.toolName} not found`); const resumeSchema = toolCall.suspended ? toolCall.resumeSchema : tool.resumeSchema; diff --git a/packages/@n8n/agents/src/runtime/tools/deferred-tool-manager.ts b/packages/@n8n/agents/src/runtime/tools/deferred-tool-manager.ts index c159c9aadae..cc852791ccc 100644 --- a/packages/@n8n/agents/src/runtime/tools/deferred-tool-manager.ts +++ b/packages/@n8n/agents/src/runtime/tools/deferred-tool-manager.ts @@ -136,6 +136,11 @@ export class DeferredToolManager { return [this.searchTool, this.loadTool]; } + /** A registered deferred tool, loaded or not. */ + getTool(name: string): BuiltTool | undefined { + return this.toolsByName.get(name); + } + getLoadedTools(): BuiltTool[] { return Array.from(this.loadedToolNames) .map((name) => this.toolsByName.get(name))