Skip to content

Commit 052b2f3

Browse files
committed
refactor: deduplicate compaction prompt between system and user messages
Move the authoritative structured summary template into the system prompt (compaction.txt) and simplify the inline defaultPrompt in compaction.ts to a short user-side reminder. This eliminates the duplicated section headings and template structure that existed in both places.
1 parent 001aa6c commit 052b2f3

4 files changed

Lines changed: 145 additions & 43 deletions

File tree

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
You are a helpful AI assistant tasked with summarizing conversations.
1+
You are summarizing a conversation for continuation by another agent.
22

3-
When asked to summarize, provide a detailed but concise summary of the conversation.
4-
Focus on information that would be helpful for continuing the conversation, including:
5-
- What was done
6-
- What is currently being worked on
7-
- Which files are being modified
8-
- What needs to be done next
9-
- Key user requests, constraints, or preferences that should persist
10-
- Important technical decisions and why they were made
3+
Produce a structured summary with these sections:
114

12-
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
5+
## Goal
6+
[What goal(s) is the user trying to accomplish?]
137

14-
Do not respond to any questions in the conversation, only output the summary.
8+
## Instructions
9+
[Important instructions from the user. Include plan/spec details if any.]
10+
11+
## Discoveries
12+
[Notable findings that would help the next agent continue]
13+
14+
## Accomplished
15+
[What has been completed, what is in progress, what remains]
16+
17+
## Relevant files
18+
[Structured list of files read, edited, or created]
19+
20+
## Unresolved issues
21+
[Any errors or blockers that need attention]
22+
23+
Be thorough but concise. Focus on information critical for continuation.
24+
Do not respond to questions in the conversation. Output only the summary.
25+
Do not call any tools.

packages/opencode/src/session/compaction.ts

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { InstanceState } from "@/effect/instance-state"
2121
import { isOverflow as overflow } from "./overflow"
2222
import { SessionCompactionPolicy } from "./compaction-policy"
2323

24-
export namespace SessionCompaction {
24+
export namespace SessionCompaction {
2525
const log = Log.create({ service: "session.compaction" })
2626

2727
const COMPACTION_MAX_RETRIES = 2
@@ -68,6 +68,7 @@ import { SessionCompactionPolicy } from "./compaction-policy"
6868
if (part.state.time.compacted) continue
6969
for (const att of part.state.attachments ?? []) {
7070
if (MessageV2.isMedia(att.mime)) continue
71+
if (att.mime !== "application/x-directory" && !att.mime.startsWith("text/")) continue
7172
if (result.length >= POST_COMPACT_MAX_ATTACHMENTS) return result
7273
result.push(att)
7374
}
@@ -241,34 +242,11 @@ import { SessionCompactionPolicy } from "./compaction-policy"
241242
{ sessionID: input.sessionID },
242243
{ context: [], prompt: undefined },
243244
)
244-
const defaultPrompt = `Provide a detailed prompt for continuing our conversation above.
245-
Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.
246-
The summary that you construct will be used so that another agent can read it and continue the work.
247-
Do not call any tools. Respond only with the summary text.
248-
249-
When constructing the summary, try to stick to this template:
250-
---
251-
## Goal
252-
253-
[What goal(s) is the user trying to accomplish?]
254-
255-
## Instructions
256-
257-
- [What important instructions did the user give you that are relevant]
258-
- [If there is a plan or spec, include information about it so next agent can continue using it]
259-
260-
## Discoveries
261-
262-
[What notable things were learned during this conversation that would be useful for the next agent to know when continuing the work]
263-
264-
## Accomplished
265-
266-
[What work has been completed, what work is still in progress, and what work is left?]
267-
268-
## Relevant files / directories
269-
270-
[Construct a structured list of relevant files that have been read, edited, or created that pertain to the task at hand. If all the files in a directory are relevant, include the path to the directory.]
271-
---`
245+
const defaultPrompt = [
246+
"Summarize the conversation for another agent continuing this work.",
247+
"Focus on goals, constraints, discoveries, progress, relevant files, and unresolved issues.",
248+
"Do not call tools. Output only the summary.",
249+
].join("\n")
272250

273251
const prompt = compacting.prompt ?? [defaultPrompt, ...compacting.context].join("\n\n")
274252
const ctx = yield* InstanceState.context

packages/opencode/test/session/prompt-effect.test.ts

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ import { LLM } from "../../src/session/llm"
2121
import { MessageV2 } from "../../src/session/message-v2"
2222
import { AppFileSystem } from "../../src/filesystem"
2323
import { SessionCompaction } from "../../src/session/compaction"
24-
import { Instruction } from "../../src/instruction"
25-
import { Session } from "../../src/session"
24+
import { Instruction } from "../../src/session/instruction"
2625
import { SessionCompactionPolicy } from "../../src/session/compaction-policy"
2726
import { SessionProcessor } from "../../src/session/processor"
2827
import { SessionPrompt } from "../../src/session/prompt"
@@ -175,7 +174,7 @@ function makeHttp() {
175174
Layer.provideMerge(proc),
176175
Layer.provideMerge(registry),
177176
Layer.provideMerge(trunc),
178-
Layer.provide(Instruction.defaultLayer),
177+
Layer.provideMerge(Instruction.defaultLayer),
179178
Layer.provideMerge(deps),
180179
),
181180
)
@@ -231,6 +230,36 @@ function providerCfg(url: string) {
231230
}
232231
}
233232

233+
function compactCfg(url: string) {
234+
return {
235+
...providerCfg(url),
236+
compaction: {
237+
max_failures: 3,
238+
max_retries: 2,
239+
max_output_tokens: 20,
240+
post_budget: 100,
241+
restore_attachments: true,
242+
},
243+
provider: {
244+
...cfg.provider,
245+
test: {
246+
...cfg.provider.test,
247+
options: {
248+
...cfg.provider.test.options,
249+
baseURL: url,
250+
},
251+
models: {
252+
...cfg.provider.test.models,
253+
"test-model": {
254+
...cfg.provider.test.models["test-model"],
255+
limit: { context: 80, output: 10 },
256+
},
257+
},
258+
},
259+
},
260+
}
261+
}
262+
234263
const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: string) {
235264
const session = yield* Session.Service
236265
const msg = yield* session.updateMessage({
@@ -280,6 +309,37 @@ const seed = Effect.fn("test.seed")(function* (sessionID: SessionID, opts?: { fi
280309
return { user: msg, assistant }
281310
})
282311

312+
const attach = Effect.fn("test.attach")(function* (sessionID: SessionID, messageID: MessageID, url: string) {
313+
const session = yield* Session.Service
314+
yield* session.updatePart({
315+
id: PartID.ascending(),
316+
messageID,
317+
sessionID,
318+
type: "tool",
319+
callID: "call-1",
320+
tool: "read",
321+
state: {
322+
status: "completed",
323+
input: {},
324+
output: "sensitive-old-tool-output",
325+
title: "done",
326+
metadata: {},
327+
time: { start: Date.now(), end: Date.now() },
328+
attachments: [
329+
{
330+
id: PartID.ascending(),
331+
messageID,
332+
sessionID,
333+
type: "file",
334+
mime: "application/json",
335+
filename: "note.json",
336+
url,
337+
},
338+
],
339+
},
340+
})
341+
})
342+
283343
const addSubtask = (sessionID: SessionID, messageID: MessageID, model = ref) =>
284344
Effect.gen(function* () {
285345
const session = yield* Session.Service
@@ -456,6 +516,57 @@ it.live("loop continues when finish is tool-calls", () =>
456516
),
457517
)
458518

519+
it.live("loop auto-compacts through prompt flow and skips unsupported restored attachments", () =>
520+
provideTmpdirServer(
521+
Effect.fnUntraced(function* ({ llm }) {
522+
const prompt = yield* SessionPrompt.Service
523+
const sessions = yield* Session.Service
524+
const chat = yield* sessions.create({
525+
title: "Compaction",
526+
permission: [{ permission: "*", pattern: "*", action: "allow" }],
527+
})
528+
const prev = yield* seed(chat.id, { finish: "stop" })
529+
yield* attach(chat.id, prev.assistant.id, "https://example.com/note.json")
530+
yield* prompt.prompt({
531+
sessionID: chat.id,
532+
agent: "build",
533+
noReply: true,
534+
parts: [{ type: "text", text: "continue" }],
535+
})
536+
537+
yield* llm.text("too large", { usage: { input: 61, output: 10 } })
538+
yield* llm.text("compact summary", { usage: { input: 20, output: 8 } })
539+
yield* llm.text("resumed", { usage: { input: 10, output: 3 } })
540+
541+
const result = yield* prompt.loop({ sessionID: chat.id })
542+
expect(yield* llm.calls).toBe(3)
543+
544+
expect(result.info.role).toBe("assistant")
545+
expect(result.parts.some((part) => part.type === "text" && part.text === "resumed")).toBe(true)
546+
547+
const msgs = yield* sessions.messages({ sessionID: chat.id })
548+
const summary = msgs.find((msg) => msg.info.role === "assistant" && msg.info.summary)
549+
expect(summary?.parts.some((part) => part.type === "text" && part.text === "compact summary")).toBe(true)
550+
551+
const next = msgs.findLast(
552+
(msg) =>
553+
msg.info.role === "user" &&
554+
msg.parts.some((part) => part.type === "text" && part.synthetic),
555+
)
556+
expect(next?.parts.some((part) => part.type === "file")).toBe(false)
557+
558+
const inputs = yield* llm.inputs
559+
expect(inputs).toHaveLength(3)
560+
expect(JSON.stringify(inputs[1]?.messages)).toContain("Summarize the conversation")
561+
expect(JSON.stringify(inputs[1]?.messages)).toContain("sensitive-old-tool-output")
562+
expect(JSON.stringify(inputs[2]?.messages)).toContain("compact summary")
563+
expect(JSON.stringify(inputs[2]?.messages)).not.toContain("note.json")
564+
expect(JSON.stringify(inputs[2]?.messages)).not.toContain("sensitive-old-tool-output")
565+
}),
566+
{ git: true, config: compactCfg },
567+
),
568+
)
569+
459570
it.live("failed subtask preserves metadata on error tool state", () =>
460571
provideTmpdirServer(
461572
Effect.fnUntraced(function* ({ llm }) {

packages/opencode/test/session/snapshot-tool-race.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Plugin } from "../../src/plugin"
4040
import { Provider as ProviderSvc } from "../../src/provider/provider"
4141
import { SessionCompaction } from "../../src/session/compaction"
4242
import { Instruction } from "../../src/session/instruction"
43+
import { SessionCompactionPolicy } from "../../src/session/compaction-policy"
4344
import { SessionProcessor } from "../../src/session/processor"
4445
import { SessionStatus } from "../../src/session/status"
4546
import { Shell } from "../../src/shell/shell"
@@ -118,6 +119,7 @@ function makeHttp() {
118119
Plugin.defaultLayer,
119120
Config.defaultLayer,
120121
ProviderSvc.defaultLayer,
122+
SessionCompactionPolicy.defaultLayer,
121123
filetime,
122124
lsp,
123125
mcp,

0 commit comments

Comments
 (0)