mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 21:49:36 +02:00
fix(core): Resume tool calls for deferred tools that a skill loaded (backport to release-candidate/2.43.x) (#40746)
Co-authored-by: Raúl Gómez Morales <raul00gm@gmail.com> Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
parent
3d47f4bfaa
commit
ea448c6f61
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user