diff --git a/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts b/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts index 30030ef6480..3ffbb85c51a 100644 --- a/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts +++ b/packages/@n8n/agents/src/runtime/loop/agent-runtime.ts @@ -55,14 +55,8 @@ import { createFilteredLogger } from '../logger'; import { MemoryOrchestrator } from '../memory/memory-orchestrator'; import { generateThreadTitle } from '../memory/title-generation'; import { AgentMessageList, type SerializedMessageList } from '../model/message-list'; -import { supportsSplitSystemMessages } from '../model/model-factory'; import { createModelTokenCounter } from '../model/model-token-counter'; -import { - applyRuntimeCacheBreakpoints, - buildInstructionPromptCacheOptions, - getEffectiveAnthropicCacheTtl, - mergeProviderOptions, -} from '../model/prompt-cache'; +import { getEffectiveAnthropicCacheTtl } from '../model/prompt-cache'; import { ActiveSkills } from '../skills/active-skills'; import { BackgroundTaskTracker } from '../state/background-task-tracker'; import { AgentEventBus, type AgentAbortScope } from '../state/event-bus'; @@ -758,11 +752,7 @@ export class AgentRuntime { ...options, persistence: options?.persistence, }); - // Explicit instruction options take precedence over cache defaults. - const instructionProviderOptions = mergeProviderOptions( - buildInstructionPromptCacheOptions(this.config.promptCaching, this.modelIdString), - this.config.instructionProviderOptions, - ); + const instructionProviderOptions = this.context.buildInstructionProviderOptions(); const state: LoopState = { totalUsage: undefined, lastFinishReason: 'stop', @@ -900,23 +890,20 @@ export class AgentRuntime { list, ); const hostVolatileInstructions = await this.resolveVolatileInstructions(options?.persistence); - const { system, messages } = this.buildModelPrompt(ctx, tools, hostVolatileInstructions); - // Cache breakpoints apply to this call only. Do not change stored messages or tools. - const cached = applyRuntimeCacheBreakpoints({ - system, - messages: this.activeSkills?.modelMessages(messages, list) ?? messages, - aiTools: tools.aiTools, - promptCaching: this.config.promptCaching, - modelId: this.modelIdString, - staticToolCacheName: tools.staticToolCacheName, + const prompt = this.context.buildModelPrompt({ + list, + tools, + instructionProviderOptions: ctx.instructionProviderOptions, + hostVolatileInstructions, + activeSkills: this.activeSkills, }); const modelCallContext: ModelCallContext = { model: staticContext.model, - system, - messages: cached.messages, + system: prompt.system, + messages: prompt.messages, abortSignal: abortScope.signal, hasTools: tools.hasTools, - aiTools: cached.aiTools, + aiTools: prompt.aiTools, reasoning: staticContext.reasoning, providerOptions: staticContext.providerOptions, outputSpec: staticContext.outputSpec, @@ -927,26 +914,6 @@ export class AgentRuntime { return { toolMap: tools.toolMap, modelCallContext }; } - private buildModelPrompt( - ctx: PreparedLoopContext, - tools: ReturnType, - hostVolatileInstructions: string | undefined, - ) { - const combinedVolatileInstructions = [tools.volatileInstructions, hostVolatileInstructions] - .map((value) => value?.trim()) - .filter((value): value is string => Boolean(value)) - .join('\n\n'); - return ctx.list.forLlm( - // Skill content changes only on activation. Keep it cached when memory compacts. - [tools.effectiveInstructions, this.activeSkills?.instructions()] - .filter(Boolean) - .join('\n\n'), - ctx.instructionProviderOptions, - combinedVolatileInstructions || undefined, - supportsSplitSystemMessages(this.config.model), - ); - } - private createInputRejectionHandler( ctx: PreparedLoopContext, iterationCount: number, diff --git a/packages/@n8n/agents/src/runtime/loop/runtime-context.ts b/packages/@n8n/agents/src/runtime/loop/runtime-context.ts index 01daaf1255f..629cc694c9c 100644 --- a/packages/@n8n/agents/src/runtime/loop/runtime-context.ts +++ b/packages/@n8n/agents/src/runtime/loop/runtime-context.ts @@ -23,13 +23,19 @@ import { } from '../memory/episodic-memory-capture'; import { loadAi } from '../model/lazy-ai'; import type { AgentMessageList } from '../model/message-list'; -import { createModel } from '../model/model-factory'; -import { buildCallPromptCacheOptions, mergeProviderOptions } from '../model/prompt-cache'; +import { createModel, supportsSplitSystemMessages } from '../model/model-factory'; +import { + applyRuntimeCacheBreakpoints, + buildCallPromptCacheOptions, + buildInstructionPromptCacheOptions, + mergeProviderOptions, +} from '../model/prompt-cache'; import { getProviderQuirks, PROVIDER_QUIRKS, resolveDefaultMaxOutputTokens, } from '../model/provider-quirks'; +import type { ActiveSkills } from '../skills/active-skills'; import type { DeferredToolManager } from '../tools/deferred-tool-manager'; import { buildToolMap, toAiSdkProviderTools, toAiSdkTools } from '../tools/tool-adapter'; @@ -109,6 +115,14 @@ export class RuntimeContextBuilder { return Output.object({ schema }); } + buildInstructionProviderOptions(): ProviderOptions | undefined { + // Explicit instruction options take precedence over cache defaults. + return mergeProviderOptions( + buildInstructionPromptCacheOptions(this.config.promptCaching, this.modelId), + this.config.instructionProviderOptions, + ); + } + /** Build the current local tool view; deferred loads can change this between iterations. */ buildToolLoopContext( aiProviderTools: ReturnType, @@ -134,6 +148,44 @@ export class RuntimeContextBuilder { }; } + buildModelPrompt({ + list, + tools, + instructionProviderOptions, + hostVolatileInstructions, + activeSkills, + }: { + list: AgentMessageList; + tools: ReturnType; + instructionProviderOptions: ProviderOptions | undefined; + hostVolatileInstructions: string | undefined; + activeSkills: ActiveSkills | undefined; + }) { + const combinedVolatileInstructions = [tools.volatileInstructions, hostVolatileInstructions] + .map((value) => value?.trim()) + .filter((value): value is string => Boolean(value)) + .join('\n\n'); + const { system, messages } = list.forLlm( + // Skill content changes only on activation. Keep it cached when memory compacts. + [tools.effectiveInstructions, activeSkills?.instructions()] + .filter(Boolean) + .join('\n\n'), + instructionProviderOptions, + combinedVolatileInstructions || undefined, + supportsSplitSystemMessages(this.config.model), + ); + // Cache breakpoints apply to this call only. Do not change stored messages or tools. + const cached = applyRuntimeCacheBreakpoints({ + system, + messages: activeSkills?.modelMessages(messages, list) ?? messages, + aiTools: tools.aiTools, + promptCaching: this.config.promptCaching, + modelId: this.modelId, + staticToolCacheName: tools.staticToolCacheName, + }); + return { system, ...cached }; + } + /** * Name of the tool eligible for an Anthropic tool-definitions cache * breakpoint, or `undefined` if the tool set isn't fully static. Deferred