diff --git a/.changeset/custom-compaction-prompt.md b/.changeset/custom-compaction-prompt.md new file mode 100644 index 000000000..784476fd0 --- /dev/null +++ b/.changeset/custom-compaction-prompt.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Allow agents to replace the default compaction summary instructions with `defineAgent({ compaction: { prompt } })`. diff --git a/docs/agent-config.md b/docs/agent-config.md index 20e84d7e5..f3d116162 100644 --- a/docs/agent-config.md +++ b/docs/agent-config.md @@ -117,6 +117,20 @@ export default defineAgent({ }); ``` +Set `prompt` when a checkpoint must preserve domain-specific context. It replaces eve's default summary instructions. eve still supplies the previous checkpoint and conversation transcript: + +```ts title="agent/agent.ts" {3,8} +import { defineAgent } from "eve"; + +const compactionPrompt = `Create a handoff summary for the next model. +Preserve unresolved customer questions and exact quoted requirements.`; + +export default defineAgent({ + model: "anthropic/claude-opus-4.8", + compaction: { prompt: compactionPrompt }, +}); +``` + See [Default harness](./concepts/default-harness#compaction) for how the loop applies it. ## Runtime limits diff --git a/docs/concepts/default-harness.md b/docs/concepts/default-harness.md index 4d88e0c32..f9f6c781a 100644 --- a/docs/concepts/default-harness.md +++ b/docs/concepts/default-harness.md @@ -7,7 +7,9 @@ The default harness is eve's built-in agent loop. It manages model calls, compac ## Compaction -The harness keeps a long session from overflowing the model's context window. Before comparing the conversation with `thresholdPercent` (`0.9` by default), it adds the estimated fixed envelope of the checkpoint prompt used for compaction. It then summarizes the older turns and keeps going. The prompt asks the compaction model to distinguish completed progress and decisions from remaining work and to retain the constraints, preferences, data, and references needed to continue. When eve compacts again, it passes the previous checkpoint separately and without the transcript's per-message truncation, then replaces it with the updated checkpoint. The summary uses the active turn model unless you override it. Tune when and how it kicks in under [`compaction`](../agent-config#compaction) in `agent.ts`: +The harness keeps a long session from overflowing the model's context window. Before comparing the conversation with `thresholdPercent` (`0.9` by default), it adds the estimated checkpoint prompt envelope. It then summarizes the older turns and continues the session. + +The default prompt preserves completed progress, decisions, remaining work, constraints, data, and references. You can replace those instructions with `compaction.prompt`. eve supplies the previous checkpoint and conversation transcript separately. The summary uses the active turn model unless you override it. Configure these settings under [`compaction`](../agent-config#compaction) in `agent.ts`: ```ts title="agent/agent.ts" export default defineAgent({ diff --git a/packages/eve/src/compiler/manifest.ts b/packages/eve/src/compiler/manifest.ts index 7b6c35f22..3cdcdc74c 100644 --- a/packages/eve/src/compiler/manifest.ts +++ b/packages/eve/src/compiler/manifest.ts @@ -398,6 +398,7 @@ const compiledAgentWorkflowDefinitionSchema = z const compiledAgentCompactionDefinitionSchema: z.ZodType = z .object({ model: compiledRuntimeModelReferenceSchema.optional(), + prompt: z.string().optional(), thresholdPercent: z.number().finite().min(0).max(1).optional(), }) .strict(); @@ -810,11 +811,11 @@ export function createCompiledAgentNodeManifest(input: { : [...input.config.build.externalDependencies], }, compaction: { + ...input.config.compaction, model: input.config.compaction?.model === undefined ? undefined : cloneCompiledRuntimeModelReference(input.config.compaction.model), - thresholdPercent: input.config.compaction?.thresholdPercent, }, description: input.config.description, dynamicModel: diff --git a/packages/eve/src/compiler/normalize-agent-config.ts b/packages/eve/src/compiler/normalize-agent-config.ts index 710f6fc68..fea8a83c2 100644 --- a/packages/eve/src/compiler/normalize-agent-config.ts +++ b/packages/eve/src/compiler/normalize-agent-config.ts @@ -66,17 +66,18 @@ export async function compileAgentConfig( sourcePath: configModulePath, value: authoredModel, }); - const compaction: { - model?: CompiledRuntimeModelReference; - thresholdPercent?: number; - } = {}; + const { + model: authoredCompactionModel, + modelContextWindowTokens: compactionModelContextWindowTokens, + ...authoredCompaction + } = definition.compaction ?? {}; + const compaction: Mutable> = { + ...authoredCompaction, + }; const compiledConfig: { build?: CompiledAgentDefinition["build"]; - compaction: { - model?: CompiledRuntimeModelReference; - thresholdPercent?: number; - }; + compaction: NonNullable; description?: string; dynamicModel?: CompiledAgentDefinition["dynamicModel"]; experimental?: CompiledAgentDefinition["experimental"]; @@ -148,22 +149,18 @@ export async function compileAgentConfig( }; } - if (definition.compaction?.model !== undefined) { + if (authoredCompactionModel !== undefined) { compaction.model = await normalizeAuthoredModelReference({ modelCatalog: context.modelCatalog, purpose: "the compaction summary model", - contextWindowTokens: definition.compaction.modelContextWindowTokens, + contextWindowTokens: compactionModelContextWindowTokens, providerOptions: definition.modelOptions?.providerOptions, source: configModule, sourcePath: configModulePath, - value: definition.compaction.model, + value: authoredCompactionModel, }); } - if (definition.compaction?.thresholdPercent !== undefined) { - compaction.thresholdPercent = definition.compaction.thresholdPercent; - } - return compiledConfig; } diff --git a/packages/eve/src/execution/create-session-step.ts b/packages/eve/src/execution/create-session-step.ts index 76933c7ad..beed07439 100644 --- a/packages/eve/src/execution/create-session-step.ts +++ b/packages/eve/src/execution/create-session-step.ts @@ -56,9 +56,6 @@ export async function createSessionStep(input: { // delegating parent: a child may narrow what its parent granted, never widen // it. Root runs have no inherited limits, so their configured values apply. const session = createSession({ - compactionOverrides: { - thresholdPercent: effectiveAgent.thresholdPercent, - }, continuationToken: input.continuationToken, limits: { // Inherited token limits are the parent's remaining quota share at diff --git a/packages/eve/src/execution/dispatch-runtime-actions-step.ts b/packages/eve/src/execution/dispatch-runtime-actions-step.ts index 06493bccc..29e6f25e8 100644 --- a/packages/eve/src/execution/dispatch-runtime-actions-step.ts +++ b/packages/eve/src/execution/dispatch-runtime-actions-step.ts @@ -77,9 +77,6 @@ export async function dispatchRuntimeActionsStep(input: { const bundle = ctx.require(BundleKey); const effectiveAgent = resolveEffectiveAgentRuntime(bundle, ctx); const session = hydrateDurableSession({ - compactionOverrides: { - thresholdPercent: effectiveAgent.thresholdPercent, - }, durable: durableSession, turnAgent: effectiveAgent.turnAgent, }); diff --git a/packages/eve/src/execution/dispatch-workflow-runtime-actions-step.ts b/packages/eve/src/execution/dispatch-workflow-runtime-actions-step.ts index 48d62b9c1..5ed3f446c 100644 --- a/packages/eve/src/execution/dispatch-workflow-runtime-actions-step.ts +++ b/packages/eve/src/execution/dispatch-workflow-runtime-actions-step.ts @@ -71,9 +71,6 @@ export async function dispatchWorkflowRuntimeActionsStep(input: { } const session = hydrateDurableSession({ - compactionOverrides: { - thresholdPercent: effectiveAgent.thresholdPercent, - }, durable: durableSession, turnAgent: effectiveAgent.turnAgent, }); diff --git a/packages/eve/src/execution/effective-agent-config.test.ts b/packages/eve/src/execution/effective-agent-config.test.ts index 368b4f449..a9736a112 100644 --- a/packages/eve/src/execution/effective-agent-config.test.ts +++ b/packages/eve/src/execution/effective-agent-config.test.ts @@ -3,20 +3,29 @@ import { describe, expect, it } from "vitest"; import { ContextContainer } from "#context/container.js"; import { DynamicSubagentAgentConfigKey } from "#context/keys.js"; import { resolveEffectiveAgentRuntime } from "#execution/effective-agent-config.js"; +import { normalizeDynamicSubagentAgentConfig } from "#runtime/subagents/dynamic-agent-config.js"; describe("resolveEffectiveAgentRuntime", () => { it("applies the selected subagent model and runtime settings", () => { const ctx = new ContextContainer(); - ctx.set(DynamicSubagentAgentConfigKey, { - compaction: { - model: { id: "anthropic/claude-sonnet-4.5" }, - thresholdPercent: 0.75, - }, - description: "Perform deep research.", - limits: { sessionTimeoutMs: 120_000 }, - model: { id: "anthropic/claude-opus-4.6" }, - reasoning: "high", - }); + ctx.set( + DynamicSubagentAgentConfigKey, + normalizeDynamicSubagentAgentConfig({ + name: "researcher", + value: { + compaction: { + model: "anthropic/claude-sonnet-4.5", + modelContextWindowTokens: 100_000, + prompt: "Preserve citations.", + thresholdPercent: 0.75, + }, + description: "Perform deep research.", + limits: { sessionTimeoutMs: 120_000 }, + model: "anthropic/claude-opus-4.6", + reasoning: "high", + }, + }), + ); const tools = [{ name: "search" }]; const effective = resolveEffectiveAgentRuntime( @@ -40,9 +49,12 @@ describe("resolveEffectiveAgentRuntime", () => { expect(effective).toMatchObject({ limits: { sessionTimeoutMs: 120_000 }, - thresholdPercent: 0.75, turnAgent: { - compactionModel: { id: "anthropic/claude-sonnet-4.5" }, + compaction: { + model: { contextWindowTokens: 100_000, id: "anthropic/claude-sonnet-4.5" }, + prompt: "Preserve citations.", + thresholdPercent: 0.75, + }, model: { id: "anthropic/claude-opus-4.6" }, reasoning: "high", }, diff --git a/packages/eve/src/execution/effective-agent-config.ts b/packages/eve/src/execution/effective-agent-config.ts index 3221a90b6..a690d7283 100644 --- a/packages/eve/src/execution/effective-agent-config.ts +++ b/packages/eve/src/execution/effective-agent-config.ts @@ -7,7 +7,6 @@ import type { DynamicSubagentAgentConfig } from "#runtime/subagents/dynamic-agen export interface EffectiveAgentRuntime { readonly limits?: AgentLimitsDefinition; - readonly thresholdPercent?: number; readonly turnAgent: RuntimeTurnAgent; } @@ -25,17 +24,15 @@ export function resolveEffectiveAgentRuntimeFromConfig( if (config === undefined) { return { limits: bundle.resolvedAgent.config.limits, - thresholdPercent: bundle.resolvedAgent.config.compaction?.thresholdPercent, turnAgent: bundle.turnAgent, }; } return { limits: config.limits, - thresholdPercent: config.compaction?.thresholdPercent, turnAgent: { ...bundle.turnAgent, - compactionModel: config.compaction?.model, + compaction: config.compaction, dynamicModel: undefined, model: config.model, outputSchema: config.outputSchema, diff --git a/packages/eve/src/execution/session.test.ts b/packages/eve/src/execution/session.test.ts index 2a3c77403..d89f0a717 100644 --- a/packages/eve/src/execution/session.test.ts +++ b/packages/eve/src/execution/session.test.ts @@ -15,9 +15,7 @@ function createTestTurnAgent(overrides?: Partial): RuntimeTurn return { id: "test-agent", instructions: ["You are a helpful assistant.", "Be concise."], - compactionModel: { - id: "summary-model", - }, + compaction: { model: { id: "summary-model" } }, model: { id: "test-model" }, tools: [ { @@ -49,6 +47,18 @@ describe("createCompactionConfig", () => { }); }); + it("preserves an authored compaction prompt", () => { + expect( + createCompactionConfig({ + prompt: "Preserve every unresolved customer question.", + }), + ).toEqual({ + prompt: "Preserve every unresolved customer question.", + recentWindowSize: 10, + threshold: 100_000, + }); + }); + it("uses the authored threshold percent when provided", () => { expect( createCompactionConfig({ @@ -161,13 +171,13 @@ describe("createSession", () => { }); }); - it("honors compactionOverrides.thresholdPercent", () => { + it("honors the turn agent compaction threshold", () => { const session = createSession({ - compactionOverrides: { thresholdPercent: 0.5 }, continuationToken: "root-token", sessionId: "sess-root", turnAgent: createTestTurnAgent({ - model: { id: "test-model", contextWindowTokens: 200_000 }, + compaction: { thresholdPercent: 0.5 }, + model: { contextWindowTokens: 200_000, id: "test-model" }, }), }); @@ -177,6 +187,48 @@ describe("createSession", () => { }); }); + it("rebuilds the compaction prompt while preserving durable accounting", () => { + const created = createSession({ + continuationToken: "root-token", + sessionId: "sess-root", + turnAgent: createTestTurnAgent({ compaction: { prompt: "Initial prompt" } }), + }); + const session = { + ...created, + compaction: { + ...created.compaction, + lastKnownInputTokens: 500, + lastKnownPromptMessageCount: 2, + }, + }; + + const durable = projectToDurableSession(session); + const hydrated = hydrateDurableSession({ + durable, + turnAgent: createTestTurnAgent({ compaction: { prompt: "Hydrated prompt" } }), + }); + const refreshed = refreshSessionFromTurnAgent({ + session: hydrated, + turnAgent: createTestTurnAgent({ compaction: { prompt: "Updated prompt" } }), + }); + + expect(created.compaction.prompt).toBe("Initial prompt"); + expect(durable.compaction).toEqual({ + lastKnownInputTokens: 500, + lastKnownPromptMessageCount: 2, + }); + expect(hydrated.compaction).toMatchObject({ + lastKnownInputTokens: 500, + lastKnownPromptMessageCount: 2, + prompt: "Hydrated prompt", + }); + expect(refreshed.compaction).toMatchObject({ + lastKnownInputTokens: 500, + lastKnownPromptMessageCount: 2, + prompt: "Updated prompt", + }); + }); + it("copies the compaction model into the refreshed session", () => { const session = createSession({ continuationToken: "root-token", @@ -187,9 +239,7 @@ describe("createSession", () => { const refreshed = refreshSessionFromTurnAgent({ session, turnAgent: createTestTurnAgent({ - compactionModel: { - id: "updated-summary-model", - }, + compaction: { model: { id: "updated-summary-model" } }, }), }); @@ -445,9 +495,6 @@ describe("refreshSessionFromTurnAgent", () => { }), }); const refreshed = refreshSessionFromTurnAgent({ - compactionOverrides: { - thresholdPercent: 0.5, - }, session: { ...session, compaction: { @@ -457,6 +504,7 @@ describe("refreshSessionFromTurnAgent", () => { }, }, turnAgent: createTestTurnAgent({ + compaction: { thresholdPercent: 0.5 }, model: { contextWindowTokens: 200_000, id: "updated-model" }, }), }); diff --git a/packages/eve/src/execution/session.ts b/packages/eve/src/execution/session.ts index 0f49fce71..4290c6015 100644 --- a/packages/eve/src/execution/session.ts +++ b/packages/eve/src/execution/session.ts @@ -26,6 +26,7 @@ export function createCompactionConfig( readonly contextWindowTokens?: number; readonly lastKnownInputTokens?: number; readonly lastKnownPromptMessageCount?: number; + readonly prompt?: string; readonly thresholdPercent?: number; } = {}, ) { @@ -35,11 +36,19 @@ export function createCompactionConfig( ? FALLBACK_COMPACTION_THRESHOLD : Math.max(1, Math.floor(input.contextWindowTokens * thresholdPercent)); - const config = { + const config: { + prompt?: string; + recentWindowSize: number; + threshold: number; + } = { recentWindowSize: DEFAULT_COMPACTION_RECENT_WINDOW_SIZE, threshold, }; + if (input.prompt !== undefined) { + config.prompt = input.prompt; + } + if (input.lastKnownInputTokens !== undefined) { return { ...config, @@ -53,9 +62,6 @@ export function createCompactionConfig( export interface CreateSessionInput { readonly continuationToken: string; - readonly compactionOverrides?: { - readonly thresholdPercent?: number; - }; /** * Optional root session id passed in by the runtime when this * session is a delegated subagent child. `undefined` for top-level @@ -79,7 +85,7 @@ export function createSession(input: CreateSessionInput): HarnessSession { -readonly [K in keyof HarnessSession]: HarnessSession[K]; } = { agent: { - compactionModelReference: turnAgent.compactionModel, + compactionModelReference: turnAgent.compaction?.model, dynamicModelDefaultReference: turnAgent.dynamicModel === undefined ? undefined : turnAgent.model, modelReference: turnAgent.model, @@ -89,7 +95,8 @@ export function createSession(input: CreateSessionInput): HarnessSession { }, compaction: createCompactionConfig({ contextWindowTokens: turnAgent.model.contextWindowTokens, - thresholdPercent: input.compactionOverrides?.thresholdPercent, + prompt: turnAgent.compaction?.prompt, + thresholdPercent: turnAgent.compaction?.thresholdPercent, }), continuationToken: input.continuationToken, history: [], @@ -121,14 +128,11 @@ export function createSession(input: CreateSessionInput): HarnessSession { export function refreshSessionFromTurnAgent(input: { readonly session: HarnessSession; readonly turnAgent: RuntimeTurnAgent; - readonly compactionOverrides?: { - readonly thresholdPercent?: number; - }; }): HarnessSession { return { ...input.session, agent: { - compactionModelReference: input.turnAgent.compactionModel, + compactionModelReference: input.turnAgent.compaction?.model, dynamicModelDefaultReference: input.turnAgent.dynamicModel === undefined ? undefined : input.turnAgent.model, modelReference: input.turnAgent.model, @@ -140,7 +144,8 @@ export function refreshSessionFromTurnAgent(input: { contextWindowTokens: input.turnAgent.model.contextWindowTokens, lastKnownInputTokens: input.session.compaction.lastKnownInputTokens, lastKnownPromptMessageCount: input.session.compaction.lastKnownPromptMessageCount, - thresholdPercent: input.compactionOverrides?.thresholdPercent, + prompt: input.turnAgent.compaction?.prompt, + thresholdPercent: input.turnAgent.compaction?.thresholdPercent, }), }; } @@ -235,9 +240,6 @@ export function projectToDurableSession(session: HarnessSession): DurableSession export function hydrateDurableSession(input: { readonly durable: DurableSession; readonly turnAgent: RuntimeTurnAgent; - readonly compactionOverrides?: { - readonly thresholdPercent?: number; - }; }): HarnessSession { const { durable, turnAgent } = input; const tools = createSessionToolDefinitions(turnAgent); @@ -246,7 +248,7 @@ export function hydrateDurableSession(input: { -readonly [K in keyof HarnessSession]: HarnessSession[K]; } = { agent: { - compactionModelReference: turnAgent.compactionModel, + compactionModelReference: turnAgent.compaction?.model, dynamicModelDefaultReference: turnAgent.dynamicModel === undefined ? undefined : turnAgent.model, modelReference: turnAgent.model, @@ -258,7 +260,8 @@ export function hydrateDurableSession(input: { contextWindowTokens: turnAgent.model.contextWindowTokens, lastKnownInputTokens: durable.compaction?.lastKnownInputTokens, lastKnownPromptMessageCount: durable.compaction?.lastKnownPromptMessageCount, - thresholdPercent: input.compactionOverrides?.thresholdPercent, + prompt: turnAgent.compaction?.prompt, + thresholdPercent: turnAgent.compaction?.thresholdPercent, }), continuationToken: durable.continuationToken, history: durable.history, diff --git a/packages/eve/src/execution/settle-cancelled-turn-step.ts b/packages/eve/src/execution/settle-cancelled-turn-step.ts index 0978153d8..a39f750fc 100644 --- a/packages/eve/src/execution/settle-cancelled-turn-step.ts +++ b/packages/eve/src/execution/settle-cancelled-turn-step.ts @@ -63,9 +63,6 @@ export async function settleCancelledTurnStep(input: { const instrumentation = getInstrumentationRuntime(); let session = hydrateDurableSession({ - compactionOverrides: { - thresholdPercent: effectiveAgent.thresholdPercent, - }, durable: durableSession, turnAgent: effectiveAgent.turnAgent, }); diff --git a/packages/eve/src/execution/subagent-event-proxy-step.ts b/packages/eve/src/execution/subagent-event-proxy-step.ts index 8416a7b2d..4e3088856 100644 --- a/packages/eve/src/execution/subagent-event-proxy-step.ts +++ b/packages/eve/src/execution/subagent-event-proxy-step.ts @@ -69,9 +69,6 @@ export async function emitProxiedSubagentEvent(input: { const bundle = ctx.require(BundleKey); const effectiveAgent = resolveEffectiveAgentRuntime(bundle, ctx); const session = hydrateDurableSession({ - compactionOverrides: { - thresholdPercent: effectiveAgent.thresholdPercent, - }, durable: input.durableSession, turnAgent: effectiveAgent.turnAgent, }); diff --git a/packages/eve/src/execution/workflow-steps.ts b/packages/eve/src/execution/workflow-steps.ts index 41508c7ad..4db24bbeb 100644 --- a/packages/eve/src/execution/workflow-steps.ts +++ b/packages/eve/src/execution/workflow-steps.ts @@ -222,9 +222,6 @@ export async function turnStep(rawInput: TurnStepInput): Promise => { const refreshedSession = refreshSessionFromTurnAgent({ - compactionOverrides: { - thresholdPercent: effectiveAgent.thresholdPercent, - }, session: lifecycleSession, turnAgent: effectiveAgent.turnAgent, }); diff --git a/packages/eve/src/harness/compaction-prompt.test.ts b/packages/eve/src/harness/compaction-prompt.test.ts index cf380b12f..2014fb51f 100644 --- a/packages/eve/src/harness/compaction-prompt.test.ts +++ b/packages/eve/src/harness/compaction-prompt.test.ts @@ -2,6 +2,7 @@ import type { ModelMessage } from "ai"; import { describe, expect, it } from "vitest"; import { COMPACTION_PROMPT_ENVELOPE, createCompactionPrompt } from "#harness/compaction-prompt.js"; +import { estimateTokens } from "#harness/token-estimate.js"; describe("createCompactionPrompt", () => { it("preserves the previous checkpoint without applying transcript truncation", () => { @@ -18,6 +19,20 @@ describe("createCompactionPrompt", () => { expect(result.prompt).toContain(markerAfterTextLimit); }); + it("uses a custom system prompt without changing the transcript envelope", () => { + const result = createCompactionPrompt({ + messages: [{ content: "New evidence", role: "user" }], + previousCheckpoint: undefined, + systemPrompt: "Preserve every unresolved customer question.", + }); + + expect(result.system).toBe("Preserve every unresolved customer question."); + expect(result.prompt).toContain(""); + expect(result.prompt).toContain("Conversation transcript:"); + expect(result.prompt).not.toContain("Make completed work explicit"); + expect(result.prompt).not.toContain("Preserve exact file paths"); + }); + it("passes tool payloads to the summarizer raw so it can judge what matters", () => { const messages: ModelMessage[] = [ { @@ -164,6 +179,28 @@ describe("createCompactionPrompt", () => { expect(result.prompt.split(taskTail)).toHaveLength(3); }); + it("includes the custom system prompt in the input budget", () => { + const systemPrompt = "Preserve domain state. ".repeat(200); + const result = createCompactionPrompt({ + messages: [ + { content: `${"old evidence ".repeat(800)}OLD_TAIL`, role: "user" }, + { content: `${"new evidence ".repeat(800)}NEW_TAIL`, role: "user" }, + ], + previousCheckpoint: undefined, + systemPrompt, + inputBudgetTokens: 2_500, + }); + + expect( + estimateTokens([ + { content: result.system, role: "system" }, + { content: result.prompt, role: "user" }, + ]), + ).toBeLessThanOrEqual(2_500); + expect(result.prompt).not.toContain("OLD_TAIL"); + expect(result.prompt).not.toContain("NEW_TAIL"); + }); + it("degrades the oldest conversational text first under budget pressure", () => { const oldest = `${"oldest message padding. ".repeat(400)}OLDEST_TAIL_MARKER`; const newest = `${"newest message padding. ".repeat(400)}NEWEST_TAIL_MARKER`; @@ -174,7 +211,7 @@ describe("createCompactionPrompt", () => { { content: newest, role: "user" }, ], // Fits one full entry plus a degraded one, but not both full. - transcriptBudgetTokens: 3_500, + inputBudgetTokens: 3_500, previousCheckpoint: undefined, }); diff --git a/packages/eve/src/harness/compaction-prompt.ts b/packages/eve/src/harness/compaction-prompt.ts index 53db893f9..bc4f43907 100644 --- a/packages/eve/src/harness/compaction-prompt.ts +++ b/packages/eve/src/harness/compaction-prompt.ts @@ -15,6 +15,12 @@ export const COMPACTION_RESUMPTION_MESSAGE = "Continue."; export const TODO_COMPACTION_PRESERVATION_LABEL = "[Your task list was preserved across context compaction]"; +const COMPACTION_CHECKPOINT_PROMPT = `Update the previous checkpoint with the newer information in the conversation. If there is no previous checkpoint, create one from the conversation. + +Make completed work explicit so the next model does not repeat it. Keep completed work separate from current and remaining work, and do not describe completed work as pending unless later messages show it must be redone. Preserve exact file paths, function names, commands, error messages, identifiers, and measured values when they are needed to continue. + +Large tool outputs are the main thing to compress: reduce each to the findings the next model needs — what was searched or read, what it established, and the exact identifiers involved — rather than reproducing the output. The next model cannot see the originals, so nothing it would need to act on may be lost.`; + const COMPACTION_SYSTEM_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task. Include: @@ -24,13 +30,9 @@ Include: - What remains to be done, with clear next steps - Any critical data, examples, or references needed to continue -Be concise, structured, and focused on helping the next LLM seamlessly continue the work. Write in the same language as the conversation. Do not continue the conversation, answer its questions, or invent facts. Only output the handoff summary.`; - -const COMPACTION_CHECKPOINT_PROMPT = `Update the previous checkpoint with the newer information in the conversation. If there is no previous checkpoint, create one from the conversation. - -Make completed work explicit so the next model does not repeat it. Keep completed work separate from current and remaining work, and do not describe completed work as pending unless later messages show it must be redone. Preserve exact file paths, function names, commands, error messages, identifiers, and measured values when they are needed to continue. +Be concise, structured, and focused on helping the next LLM seamlessly continue the work. Write in the same language as the conversation. Do not continue the conversation, answer its questions, or invent facts. Only output the handoff summary. -Large tool outputs are the main thing to compress: reduce each to the findings the next model needs — what was searched or read, what it established, and the exact identifiers involved — rather than reproducing the output. The next model cannot see the originals, so nothing it would need to act on may be lost.`; +${COMPACTION_CHECKPOINT_PROMPT}`; // Fallback cap for conversational text, applied oldest-first only when the // rendered transcript exceeds the caller's token budget. @@ -56,29 +58,31 @@ export const COMPACTION_PROMPT_ENVELOPE = { * Builds the compaction model input from framework-owned checkpoint state and * older messages. * - * Conversational text is rendered verbatim. When `transcriptBudgetTokens` is - * set and the rendered prompt exceeds it, conversational text is capped at + * Conversational text is rendered verbatim. When `inputBudgetTokens` is + * set and the combined system and user input exceeds it, conversational text is capped at * {@link DEGRADED_TEXT_LIMIT} starting from the oldest entries until the - * prompt fits; the previous checkpoint is never truncated. + * input fits; the previous checkpoint is never truncated. */ export function createCompactionPrompt(input: { readonly messages: readonly ModelMessage[]; readonly previousCheckpoint: string | undefined; - readonly transcriptBudgetTokens?: number; + readonly systemPrompt?: string; + readonly inputBudgetTokens?: number; }): CompactionPrompt { const entries = input.messages.map((message) => ({ content: renderCompactionMessageContent(message), role: message.role, })); - degradeOversizedTranscript(input, entries); + const system = input.systemPrompt ?? COMPACTION_SYSTEM_PROMPT; + degradeOversizedTranscript({ ...input, systemPrompt: system }, entries); return { prompt: formatCompactionPrompt({ previousCheckpoint: input.previousCheckpoint?.trim() ?? "(none)", transcript: formatCompactionTranscript(entries), }), - system: COMPACTION_SYSTEM_PROMPT, + system, }; } @@ -93,11 +97,12 @@ function degradeOversizedTranscript( input: { readonly messages: readonly ModelMessage[]; readonly previousCheckpoint: string | undefined; - readonly transcriptBudgetTokens?: number; + readonly systemPrompt: string; + readonly inputBudgetTokens?: number; }, entries: { content: string; role: ModelMessage["role"] }[], ): void { - const budget = input.transcriptBudgetTokens; + const budget = input.inputBudgetTokens; if (budget === undefined) { return; } @@ -106,7 +111,11 @@ function degradeOversizedTranscript( previousCheckpoint: input.previousCheckpoint?.trim() ?? "(none)", transcript: formatCompactionTranscript(entries), }); - let excessTokens = estimateTokens(fullPrompt) - budget; + let excessTokens = + estimateTokens([ + { content: input.systemPrompt, role: "system" }, + { content: fullPrompt, role: "user" }, + ]) - budget; for (let index = 0; index < entries.length && excessTokens > 0; index += 1) { const entry = entries[index]; @@ -136,9 +145,7 @@ ${input.previousCheckpoint} Conversation transcript: ${input.transcript} - - -${COMPACTION_CHECKPOINT_PROMPT}`; +`; } function formatCompactionTranscript(messages: readonly CompactionTranscriptEntry[]): string { diff --git a/packages/eve/src/harness/compaction.test.ts b/packages/eve/src/harness/compaction.test.ts index 9032e29b9..f1b5ff00b 100644 --- a/packages/eve/src/harness/compaction.test.ts +++ b/packages/eve/src/harness/compaction.test.ts @@ -194,6 +194,31 @@ describe("shouldCompact", () => { ).toBe(true); }); + it("accounts for a custom prompt in the compaction threshold", () => { + const messages: ModelMessage[] = [{ content: "Continue the investigation.", role: "user" }]; + const customPrompt = "Preserve every customer requirement. ".repeat(200); + const activeInputTokens = getInputTokenCount(messages, config); + const customEnvelopeTokens = estimateTokens([ + { content: customPrompt, role: "system" }, + { content: COMPACTION_PROMPT_ENVELOPE.prompt, role: "user" }, + ] satisfies ModelMessage[]); + + expect( + shouldCompact(messages, { + ...config, + prompt: customPrompt, + threshold: activeInputTokens + customEnvelopeTokens, + }), + ).toBe(false); + expect( + shouldCompact(messages, { + ...config, + prompt: customPrompt, + threshold: activeInputTokens + customEnvelopeTokens - 1, + }), + ).toBe(true); + }); + it("does not compact an empty history based on prompt overhead alone", () => { expect(shouldCompact([], { ...config, threshold: 0 })).toBe(false); }); @@ -554,11 +579,12 @@ describe("compactMessages: forced summary", () => { text: "forced checkpoint", } as Awaited>); const messages = [user("old message"), assistant("old reply")]; + const prompt = "Preserve every unresolved customer question."; const result = await compactMessages( messages, {} as Parameters[1], - { recentWindowSize: 10, threshold: ROOMY }, + { prompt, recentWindowSize: 10, threshold: ROOMY }, undefined, undefined, undefined, @@ -567,6 +593,7 @@ describe("compactMessages: forced summary", () => { ); expect(generateText).toHaveBeenCalledOnce(); + expect(generateText).toHaveBeenCalledWith(expect.objectContaining({ system: prompt })); expect(result).toContainEqual({ content: "forced checkpoint", role: "assistant" }); }); }); diff --git a/packages/eve/src/harness/compaction.ts b/packages/eve/src/harness/compaction.ts index 054d98c0f..00227a4ee 100644 --- a/packages/eve/src/harness/compaction.ts +++ b/packages/eve/src/harness/compaction.ts @@ -20,13 +20,14 @@ const COMPACTION_SUMMARY_RESERVE_TOKENS = 2_048; */ type ModelMessageContentPart = Exclude[number]; -// Static envelope estimate stays valid because createCompactionPrompt bounds -// its transcript to the caller's threshold budget, so the summarization call -// itself never grows past threshold + envelope + checkpoint. -const COMPACTION_PROMPT_OVERHEAD_TOKENS = estimateTokens([ - { content: COMPACTION_PROMPT_ENVELOPE.system, role: "system" }, - { content: COMPACTION_PROMPT_ENVELOPE.prompt, role: "user" }, -] satisfies ModelMessage[]); +// Include the active prompt envelope in the trigger count. The transcript is +// bounded separately by createCompactionPrompt. +function getCompactionPromptOverheadTokens(config: CompactionConfig): number { + return estimateTokens([ + { content: config.prompt ?? COMPACTION_PROMPT_ENVELOPE.system, role: "system" }, + { content: COMPACTION_PROMPT_ENVELOPE.prompt, role: "user" }, + ] satisfies ModelMessage[]); +} /** * Best available input-token count: the model-reported count from the last @@ -63,7 +64,8 @@ export function shouldCompact( ): boolean { return ( messages.length > 0 && - getInputTokenCount(messages, config) + COMPACTION_PROMPT_OVERHEAD_TOKENS > config.threshold + getInputTokenCount(messages, config) + getCompactionPromptOverheadTokens(config) > + config.threshold ); } @@ -162,7 +164,7 @@ function evaluateThreshold( config: CompactionConfig, ruler: "estimate" | "should-compact", ): { readonly estimatedTokens: number; readonly type: "over-limit" | "within-limit" } { - const overhead = ruler === "should-compact" ? COMPACTION_PROMPT_OVERHEAD_TOKENS : 0; + const overhead = ruler === "should-compact" ? getCompactionPromptOverheadTokens(config) : 0; const estimatedTokens = estimateTokens(messages) + overhead; return { estimatedTokens, @@ -210,7 +212,8 @@ export async function compactMessages( const summaryPrompt = createCompactionPrompt({ messages: older, previousCheckpoint, - transcriptBudgetTokens: config.threshold, + systemPrompt: config.prompt, + inputBudgetTokens: config.threshold, }); const result = await generateText({ diff --git a/packages/eve/src/harness/tool-loop-compaction-accounting.test.ts b/packages/eve/src/harness/tool-loop-compaction-accounting.test.ts index a77054094..44a681fe2 100644 --- a/packages/eve/src/harness/tool-loop-compaction-accounting.test.ts +++ b/packages/eve/src/harness/tool-loop-compaction-accounting.test.ts @@ -123,6 +123,7 @@ function expectStepFn(value: StepNext): StepFn { describe("tool-loop structured compaction accounting", () => { it("compacts before the continuation step when structured tool results were appended", async () => { + const compactionPrompt = "Preserve every unresolved customer question. ".repeat(20); vi.mocked(generateText).mockResolvedValue({ text: "summary", } as Awaited>); @@ -205,6 +206,7 @@ describe("tool-loop structured compaction accounting", () => { const first = await runStep( createTestSession({ compaction: { + prompt: compactionPrompt, recentWindowSize: 10, threshold: 500, }, @@ -216,11 +218,15 @@ describe("tool-loop structured compaction accounting", () => { expect(first.session.compaction).toMatchObject({ lastKnownInputTokens: 100, lastKnownPromptMessageCount: 1, + prompt: compactionPrompt, }); const second = await expectStepFn(first.next)(first.session); expect(vi.mocked(generateText)).toHaveBeenCalledTimes(1); + expect(vi.mocked(generateText)).toHaveBeenCalledWith( + expect.objectContaining({ system: compactionPrompt }), + ); expect(second.session.history[0]).toEqual({ content: "Summary of our conversation so far:", role: "user", diff --git a/packages/eve/src/harness/tool-loop.test.ts b/packages/eve/src/harness/tool-loop.test.ts index 7a1abeaa8..b120a2975 100644 --- a/packages/eve/src/harness/tool-loop.test.ts +++ b/packages/eve/src/harness/tool-loop.test.ts @@ -7797,6 +7797,7 @@ describe("createToolLoopHarness", () => { compaction: { lastKnownInputTokens: 9000, lastKnownPromptMessageCount: 2, + prompt: "Preserve every unresolved customer question.", recentWindowSize: 10, threshold: 100_000, }, @@ -7810,9 +7811,16 @@ describe("createToolLoopHarness", () => { expect(result.next).toBeNull(); expect(result.session.history).toEqual(compactedHistory); - expect(result.session.compaction).toEqual({ recentWindowSize: 10, threshold: 100_000 }); + expect(result.session.compaction).toEqual({ + prompt: "Preserve every unresolved customer question.", + recentWindowSize: 10, + threshold: 100_000, + }); expect(shouldCompact).not.toHaveBeenCalled(); expect(compactMessages).toHaveBeenCalledOnce(); + expect(vi.mocked(compactMessages).mock.calls[0]?.[2].prompt).toBe( + "Preserve every unresolved customer question.", + ); expect(onCompaction).toHaveBeenCalledOnce(); expect(ToolLoopAgent).not.toHaveBeenCalled(); expect(getCompatibilityEventTypes(events)).toEqual([ diff --git a/packages/eve/src/harness/tool-loop.ts b/packages/eve/src/harness/tool-loop.ts index 8d809b65d..492dca7ce 100644 --- a/packages/eve/src/harness/tool-loop.ts +++ b/packages/eve/src/harness/tool-loop.ts @@ -599,10 +599,7 @@ export function createToolLoopHarness(config: ToolLoopHarnessConfig): StepFn { session = { ...compacted.session, - compaction: { - recentWindowSize: compacted.session.compaction.recentWindowSize, - threshold: compacted.session.compaction.threshold, - }, + compaction: resetCompactionAccounting(compacted.session.compaction), history: compacted.messages, }; } catch (error) { @@ -2522,27 +2519,31 @@ function parkOnWorkflowInterrupt(input: { return { next: null, session: setHarnessEmissionState(parkedSession, input.emissionState) }; } +function resetCompactionAccounting(current: CompactionConfig): CompactionConfig { + const { + lastKnownInputTokens: _lastKnownInputTokens, + lastKnownPromptMessageCount: _lastKnownPromptMessageCount, + ...config + } = current; + return config; +} + function createNextCompactionConfig( current: CompactionConfig, promptMessages: readonly ModelMessage[], result: HarnessStepResult, ): CompactionConfig { - const next: { - lastKnownInputTokens?: number; - lastKnownPromptMessageCount?: number; - recentWindowSize: number; - threshold: number; - } = { - recentWindowSize: current.recentWindowSize, - threshold: current.threshold, - }; + const next = resetCompactionAccounting(current); - if (result.usage?.inputTokens !== undefined) { - next.lastKnownInputTokens = result.usage.inputTokens; - next.lastKnownPromptMessageCount = promptMessages.length; + if (result.usage?.inputTokens === undefined) { + return next; } - return next; + return { + ...next, + lastKnownInputTokens: result.usage.inputTokens, + lastKnownPromptMessageCount: promptMessages.length, + }; } /** diff --git a/packages/eve/src/harness/types.ts b/packages/eve/src/harness/types.ts index f6909c404..ac6dd8c3f 100644 --- a/packages/eve/src/harness/types.ts +++ b/packages/eve/src/harness/types.ts @@ -32,6 +32,7 @@ export type SessionStateMap = Readonly>; export interface CompactionConfig { readonly lastKnownInputTokens?: number; readonly lastKnownPromptMessageCount?: number; + readonly prompt?: string; readonly recentWindowSize: number; readonly threshold: number; } diff --git a/packages/eve/src/internal/authored-definition/core.test.ts b/packages/eve/src/internal/authored-definition/core.test.ts index cc22eb334..63f8c5ccb 100644 --- a/packages/eve/src/internal/authored-definition/core.test.ts +++ b/packages/eve/src/internal/authored-definition/core.test.ts @@ -76,6 +76,20 @@ describe("normalizeAgentDefinition", () => { ).toThrow('"compaction.model" does not support defineDynamic'); }); + it("accepts a custom compaction prompt", () => { + const definition = normalizeAgentDefinition( + { + compaction: { + prompt: "Preserve every unresolved customer question.", + }, + model: "openai/gpt-5.5", + }, + FAILURE_MESSAGE, + ); + + expect(definition.compaction?.prompt).toBe("Preserve every unresolved customer question."); + }); + it("rejects unsupported reasoning effort", () => { expect(() => normalizeAgentDefinition( diff --git a/packages/eve/src/internal/authored-definition/core.ts b/packages/eve/src/internal/authored-definition/core.ts index 348ed5a2a..f69d0845c 100644 --- a/packages/eve/src/internal/authored-definition/core.ts +++ b/packages/eve/src/internal/authored-definition/core.ts @@ -296,7 +296,11 @@ function normalizeAgentCompactionDefinition( message: string, ): NonNullable { const record = expectObjectRecord(value, message); - expectOnlyKnownKeys(record, ["model", "modelContextWindowTokens", "thresholdPercent"], message); + expectOnlyKnownKeys( + record, + ["model", "modelContextWindowTokens", "prompt", "thresholdPercent"], + message, + ); const normalizedDefinition: Mutable> = {}; if (record.model !== undefined) { @@ -315,6 +319,10 @@ function normalizeAgentCompactionDefinition( ); } + if (record.prompt !== undefined) { + normalizedDefinition.prompt = expectString(record.prompt, message); + } + if (record.thresholdPercent !== undefined) { const thresholdPercent = record.thresholdPercent; diff --git a/packages/eve/src/runtime/agent/bootstrap.ts b/packages/eve/src/runtime/agent/bootstrap.ts index e0396746e..2aac36c6b 100644 --- a/packages/eve/src/runtime/agent/bootstrap.ts +++ b/packages/eve/src/runtime/agent/bootstrap.ts @@ -26,6 +26,12 @@ export type RuntimeDynamicModelReference = Readonly< } >; +export interface RuntimeCompactionDefinition { + readonly model?: RuntimeModelReference; + readonly prompt?: string; + readonly thresholdPercent?: number; +} + /** * Minimal runtime-owned agent shape prepared for one harness turn. */ @@ -33,12 +39,7 @@ export interface RuntimeTurnAgent { readonly availableSkills?: readonly AvailableSkillDescription[]; readonly id: string; readonly instructions: readonly string[]; - /** - * Optional model used only for compaction summaries. - * - * When omitted, the harness uses the active turn model for compaction. - */ - readonly compactionModel?: RuntimeModelReference; + readonly compaction?: RuntimeCompactionDefinition; readonly dynamicModel?: RuntimeDynamicModelReference; readonly model: RuntimeModelReference; readonly nodeId?: string; @@ -76,7 +77,7 @@ export function createResolvedRuntimeTurnAgent(input: { toolsAvailable: input.tools.length > 0, workspaceSpec: agent.workspaceSpec, }), - compactionModel: agent.config.compaction?.model, + compaction: agent.config.compaction, dynamicModel: agent.config.dynamicModel, model: agent.config.model, nodeId: input.nodeId, diff --git a/packages/eve/src/runtime/resolve-agent.ts b/packages/eve/src/runtime/resolve-agent.ts index 22a61940b..84ae59c86 100644 --- a/packages/eve/src/runtime/resolve-agent.ts +++ b/packages/eve/src/runtime/resolve-agent.ts @@ -150,6 +150,28 @@ function createResolvedInstructionsDefinition( }; } +function createResolvedModelReference( + model: CompiledAgentNodeManifest["config"]["model"], +): ResolvedAgent["config"]["model"] { + return model.source === undefined + ? { + contextWindowTokens: model.contextWindowTokens, + id: model.id, + providerOptions: model.providerOptions, + } + : { + contextWindowTokens: model.contextWindowTokens, + id: model.id, + providerOptions: model.providerOptions, + source: { + exportName: model.source.exportName, + logicalPath: model.source.logicalPath, + sourceId: model.source.sourceId, + sourceKind: "module", + }, + }; +} + function createResolvedAgentConfig(manifest: CompiledAgentNodeManifest): ResolvedAgent["config"] { const config: { compaction?: ResolvedAgent["config"]["compaction"]; @@ -162,59 +184,16 @@ function createResolvedAgentConfig(manifest: CompiledAgentNodeManifest): Resolve source?: ResolvedAgent["config"]["source"]; limits?: ResolvedAgent["config"]["limits"]; } = { - model: - manifest.config.model.source === undefined - ? { - id: manifest.config.model.id, - contextWindowTokens: manifest.config.model.contextWindowTokens, - providerOptions: manifest.config.model.providerOptions, - } - : { - contextWindowTokens: manifest.config.model.contextWindowTokens, - id: manifest.config.model.id, - providerOptions: manifest.config.model.providerOptions, - source: { - exportName: manifest.config.model.source.exportName, - sourceKind: "module" as const, - logicalPath: manifest.config.model.source.logicalPath, - sourceId: manifest.config.model.source.sourceId, - }, - }, + model: createResolvedModelReference(manifest.config.model), name: manifest.config.name, }; if (manifest.config.compaction !== undefined) { - const compaction: { - model?: ResolvedAgent["config"]["model"]; - thresholdPercent?: number; - } = {}; - - if (manifest.config.compaction.model !== undefined) { - compaction.model = - manifest.config.compaction.model.source === undefined - ? { - contextWindowTokens: manifest.config.compaction.model.contextWindowTokens, - id: manifest.config.compaction.model.id, - providerOptions: manifest.config.compaction.model.providerOptions, - } - : { - contextWindowTokens: manifest.config.compaction.model.contextWindowTokens, - id: manifest.config.compaction.model.id, - providerOptions: manifest.config.compaction.model.providerOptions, - source: { - exportName: manifest.config.compaction.model.source.exportName, - sourceKind: "module" as const, - logicalPath: manifest.config.compaction.model.source.logicalPath, - sourceId: manifest.config.compaction.model.source.sourceId, - }, - }; - } - - if (manifest.config.compaction.thresholdPercent !== undefined) { - compaction.thresholdPercent = manifest.config.compaction.thresholdPercent; - } - - config.compaction = compaction; + const { model, ...compaction } = manifest.config.compaction; + config.compaction = { + ...compaction, + model: model === undefined ? undefined : createResolvedModelReference(model), + }; } if (manifest.config.dynamicModel !== undefined) { diff --git a/packages/eve/src/runtime/subagents/dynamic-agent-config.ts b/packages/eve/src/runtime/subagents/dynamic-agent-config.ts index c65db0f43..ea78e52cd 100644 --- a/packages/eve/src/runtime/subagents/dynamic-agent-config.ts +++ b/packages/eve/src/runtime/subagents/dynamic-agent-config.ts @@ -12,6 +12,7 @@ import { serializeOutputSchema } from "#shared/tool-schema.js"; export interface DynamicSubagentAgentConfig { readonly compaction?: { readonly model?: DynamicSubagentModelReference; + readonly prompt?: string; readonly thresholdPercent?: number; }; readonly description: string; @@ -65,23 +66,21 @@ export function normalizeDynamicSubagentAgentConfig(input: { }; if (definition.compaction !== undefined) { - const compaction: { - model?: DynamicSubagentModelReference; - thresholdPercent?: number; - } = {}; - if (definition.compaction.model !== undefined) { - compaction.model = normalizeModelReference({ - contextWindowTokens: definition.compaction.modelContextWindowTokens, - model: definition.compaction.model, - name: input.name, - providerOptions: definition.modelOptions?.providerOptions, - }); - } - if (definition.compaction.thresholdPercent !== undefined) { - compaction.thresholdPercent = definition.compaction.thresholdPercent; - } - config.compaction = compaction; + const { model, modelContextWindowTokens, ...compaction } = definition.compaction; + config.compaction = { + ...compaction, + model: + model === undefined + ? undefined + : normalizeModelReference({ + contextWindowTokens: modelContextWindowTokens, + model, + name: input.name, + providerOptions: definition.modelOptions?.providerOptions, + }), + }; } + if (definition.limits !== undefined) { config.limits = definition.limits; } diff --git a/packages/eve/src/shared/agent-definition.ts b/packages/eve/src/shared/agent-definition.ts index e79311fb7..fe317ba4e 100644 --- a/packages/eve/src/shared/agent-definition.ts +++ b/packages/eve/src/shared/agent-definition.ts @@ -108,6 +108,8 @@ export interface InternalAgentCompactionDefinition { * When omitted, eve uses the active turn model for the summary call. */ model?: InternalAgentModelDefinition; + /** Instructions used to generate the compaction summary. */ + prompt?: string; /** * Fraction of the primary model context window that triggers compaction. * @@ -137,6 +139,13 @@ export interface PublicAgentCompactionDefinition { * When omitted, eve uses the active turn model for the summary call. */ readonly model?: PublicAgentStaticModelDefinition; + /** + * Instructions used to generate the compaction summary. + * + * When omitted, eve uses its default handoff-summary prompt. The conversation + * transcript and previous checkpoint are supplied separately by the harness. + */ + readonly prompt?: string; /** * Fraction of the primary model context window that triggers compaction. * diff --git a/packages/eve/test/scenarios/runtime-loaders.scenario.test.ts b/packages/eve/test/scenarios/runtime-loaders.scenario.test.ts index a1540b719..249b66e82 100644 --- a/packages/eve/test/scenarios/runtime-loaders.scenario.test.ts +++ b/packages/eve/test/scenarios/runtime-loaders.scenario.test.ts @@ -291,6 +291,7 @@ describe("runtime compiled artifact loaders", () => { ' model: "openai/gpt-5.4",', " compaction: {", ' model: "openai/gpt-5.4-mini",', + ' prompt: "Preserve exact customer requirements.",', " thresholdPercent: 0.75,", " },", "};", @@ -316,6 +317,7 @@ describe("runtime compiled artifact loaders", () => { contextWindowTokens: expect.any(Number), id: "openai/gpt-5.4-mini", }, + prompt: "Preserve exact customer requirements.", thresholdPercent: 0.75, }, model: {