diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 9a763c7ea9b8..8aedba66f243 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -34,7 +34,7 @@ const PROMPT_COMPACTION = `You are an anchored context summarization assistant f Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work. -If the prompt includes a block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts. +If the prompt includes a block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts. Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs. diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 0ea840481843..5b84aa90cad1 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -64,6 +64,15 @@ Rules: "compaction" case), and that marker must stay -- TKT-379, diary 2610: hiding it drops history_search's call rate from 100% to 20% because the model receives no cue that older detail might exist at all.` +const SUMMARY_UPDATE_INSTRUCTIONS = `The summarizes everything that happened before the . Construct a new summary that combines both. The is discarded after this: anything you do not carry into the new summary is lost. + +When combining: +- Carry forward objectives, constraints, user directives, decisions, and parallel workstreams from the even when the does not mention them. Drop only what is finished and no longer needed. +- The is more recent than the . Where they conflict, the conversation wins: state the corrected fact and drop the old claim. +- Add new progress, decisions, constraints, and context from the conversation. +- Move completed work from "Active" to "Completed". +- If a blocker has been resolved, update the summary to reflect that while keeping any details still needed to continue the work. +- Update "Objective" and "Next Move" to reflect the current work state.` export type Entry = { readonly seq: number @@ -250,14 +259,21 @@ const select = ( } } -export const buildPrompt = (input: { readonly previousSummary?: string; readonly context: readonly string[] }) => - [ - input.previousSummary - ? `Update the anchored summary below using the conversation history above.\nPreserve still-true details, remove stale details, and merge in the new facts.\n\n${input.previousSummary}\n` - : "Create a new anchored summary from the conversation history.", +export const buildPrompt = (input: { readonly previousSummary?: string; readonly context: readonly string[] }) => { + const conversation = `Here is the conversation so far:\n\n\n${input.context.join("\n\n")}\n` + if (!input.previousSummary) + return [ + conversation, + "Create a new anchored summary from the conversation history in the tags above so another coding agent can continue the work.", + SUMMARY_TEMPLATE, + ].join("\n\n") + return [ + conversation, + `Here is the summary of the conversation before the above:\n\n\n${input.previousSummary}\n`, + SUMMARY_UPDATE_INSTRUCTIONS, SUMMARY_TEMPLATE, - ...input.context, ].join("\n\n") +} export const make = (dependencies: Dependencies) => { const config = settings(dependencies.config) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 9e344d780d29..3167b510f7d9 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -35,12 +35,45 @@ function assistant( test("compaction prompt preserves detailed work state and relevant files", () => { const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] }) + expect(prompt).toStartWith( + "Here is the conversation so far:\n\n\nconversation history\n", + ) + expect(prompt.indexOf("")).toBeLessThan(prompt.indexOf("Create a new anchored summary")) + expect(prompt).toContain("conversation history in the tags above") expect(prompt).toContain("## Work State\n### Completed") expect(prompt).toContain("### Active") expect(prompt).toContain("### Blocked") expect(prompt).toContain("## Relevant Files") }) +test("compaction prompt gives update instructions for a prior summary", () => { + const prompt = SessionCompaction.buildPrompt({ + context: ["new conversation"], + previousSummary: "existing summary", + }) + + expect(prompt.indexOf("")).toBeLessThan(prompt.indexOf("")) + expect(prompt.indexOf("")).toBeLessThan(prompt.indexOf("The summarizes")) + expect(prompt).toContain( + "Carry forward objectives, constraints, user directives, decisions, and parallel workstreams from the ", + ) + expect(prompt).toContain('Move completed work from "Active" to "Completed".') + expect(prompt).toContain('Update "Objective" and "Next Move" to reflect the current work state.') +}) + +// TKT-379, diary 2610: hiding that context was compacted drops history_search's call rate from +// 100% to 20% -- the model needs the cue that older detail might exist. Upstream's own version of +// this prompt instructs the OPPOSITE ("do not mention...that context was compacted"); this pins +// that we deliberately do not take that instruction when porting their prompt-clarity +// improvements (anomalyco/opencode@dab2637, "fix(compaction): adjust instructions and structure +// to be more clear to smaller models"). +test("compaction prompt still requires the compaction marker to stay visible (TKT-379 -- do not adopt upstream's opposite instruction)", () => { + const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] }) + + expect(prompt).toContain("that marker must stay -- TKT-379") + expect(prompt).not.toContain("Do not mention the summary process or that context was compacted.") +}) + test("exceedsCapacity applies the estimator safety factor to the estimated portion -- a request the raw estimate clears still exceeds capacity once inflated by 1.2x (TKT-377, diary 2584/2594: char/4 under-counts structured tool output by up to 31%)", () => { // context=100000, buffer=1000 -> usable capacity is 99000. estimatedTokens=90000 clears that // raw (90000 <= 99000), but 90000 * 1.2 = 108000 exceeds it -- exactly the gap the live diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index d276fa7d0c86..5f3eb5f7b98e 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1143,7 +1143,7 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(2) expect(userTexts(requests[0])[0]).toContain( - "\n## Objective\n- Preserve the task\n", + "\n## Objective\n- Preserve the task\n", ) expect(userTexts(requests[0])[0]).toContain("Recent exact request") expect((yield* (yield* SessionStore.Service).context(sessionID))[0]).toMatchObject({ diff --git a/packages/opencode/src/agent/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt index c7cb838bbaa0..d88d6345f37c 100644 --- a/packages/opencode/src/agent/prompt/compaction.txt +++ b/packages/opencode/src/agent/prompt/compaction.txt @@ -2,7 +2,7 @@ You are an anchored context summarization assistant for coding sessions. Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work. -If the prompt includes a block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts. +If the prompt includes a block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts. Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs. diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index d35eee63df7e..141c2200c7b8 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -1484,7 +1484,7 @@ describe("session.compaction.process", () => { expect(parent).toBeTruthy() yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false }) - expect(captured).toContain("") + expect(captured).toContain("") expect(captured).toContain("summary one") expect(captured.match(/summary one/g)?.length).toBe(1) expect(captured).toContain("## Important Details")