refactor(core): Move prompt assembly into runtime context

This commit is contained in:
Robin Braumann 2026-09-24 15:20:03 +02:00
parent 61e41f966b
commit d8275daabb
No known key found for this signature in database
2 changed files with 65 additions and 46 deletions

View File

@ -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<RuntimeContextBuilder['buildToolLoopContext']>,
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,

View File

@ -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<typeof toAiSdkProviderTools>,
@ -134,6 +148,44 @@ export class RuntimeContextBuilder {
};
}
buildModelPrompt({
list,
tools,
instructionProviderOptions,
hostVolatileInstructions,
activeSkills,
}: {
list: AgentMessageList;
tools: ReturnType<RuntimeContextBuilder['buildToolLoopContext']>;
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