summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-10-22 18:33:33 -0400
committerDax Raad <[email protected]>2025-10-22 18:33:46 -0400
commit1f80de2fa6c627da049b6dcda29cdd66c67c160b (patch)
treea192c41e10989d6f978f0d482e69ff9924a030d3 /packages
parentf194a784b00b2e21067cc15bced152cfec7bb810 (diff)
downloadopencode-1f80de2fa6c627da049b6dcda29cdd66c67c160b.tar.gz
opencode-1f80de2fa6c627da049b6dcda29cdd66c67c160b.zip
core: add experimental turn summarization to compact conversation history
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/flag/flag.ts1
-rw-r--r--packages/opencode/src/session/compaction.ts2
-rw-r--r--packages/opencode/src/session/message-v2.ts10
-rw-r--r--packages/opencode/src/session/prompt.ts8
-rw-r--r--packages/opencode/src/session/prompt/summarize-turn.txt5
-rw-r--r--packages/opencode/src/session/summary.ts46
6 files changed, 69 insertions, 3 deletions
diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts
index 0437c4c69..fff271cd2 100644
--- a/packages/opencode/src/flag/flag.ts
+++ b/packages/opencode/src/flag/flag.ts
@@ -12,6 +12,7 @@ export namespace Flag {
// Experimental
export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER")
+ export const OPENCODE_EXPERIMENTAL_TURN_SUMMARY = truthy("OPENCODE_EXPERIMENTAL_TURN_SUMMARY")
export const OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP = truthy("OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP")
function truthy(key: string) {
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index 2209fda56..2c9349ea6 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -98,7 +98,7 @@ export namespace SessionCompaction {
draft.time.compacting = undefined
})
})
- const toSummarize = await Session.messages(input.sessionID).then(MessageV2.filterSummarized)
+ const toSummarize = await Session.messages(input.sessionID).then(MessageV2.filterCompacted)
const model = await Provider.getModel(input.providerID, input.modelID)
const system = [
...SystemPrompt.summarize(model.providerID),
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 1b17764a5..d0f25181f 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -5,6 +5,8 @@ import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
import { Identifier } from "../id/id"
import { LSP } from "../lsp"
+import { Snapshot } from "@/snapshot"
+import { fn } from "@/util/fn"
export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
@@ -241,6 +243,12 @@ export namespace MessageV2 {
time: z.object({
created: z.number(),
}),
+ summary: z
+ .object({
+ diffs: Snapshot.FileDiff.array(),
+ text: z.string(),
+ })
+ .optional(),
}).meta({
ref: "UserMessage",
})
@@ -597,7 +605,7 @@ export namespace MessageV2 {
return convertToModelMessages(result)
}
- export function filterSummarized(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) {
+ export function filterCompacted(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) {
const i = msgs.findLastIndex((m) => m.info.role === "assistant" && !!m.info.summary)
if (i === -1) return msgs.slice()
return msgs.slice(i)
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 717fbc241..bcc19005d 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -50,6 +50,7 @@ import { spawn } from "child_process"
import { Command } from "../command"
import { $, fileURLToPath } from "bun"
import { ConfigMarkdown } from "../config/markdown"
+import { MessageSummary } from "./summary"
export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
@@ -345,6 +346,11 @@ export namespace SessionPrompt {
}
state().queued.delete(input.sessionID)
SessionCompaction.prune(input)
+ MessageSummary.summarize({
+ sessionID: input.sessionID,
+ messageID: result.info.parentID,
+ providerID: model.providerID,
+ })
return result
}
}
@@ -355,7 +361,7 @@ export namespace SessionPrompt {
providerID: string
signal: AbortSignal
}) {
- let msgs = await Session.messages(input.sessionID).then(MessageV2.filterSummarized)
+ let msgs = await Session.messages(input.sessionID).then(MessageV2.filterCompacted)
const lastAssistant = msgs.findLast((msg) => msg.info.role === "assistant")
if (
lastAssistant?.info.role === "assistant" &&
diff --git a/packages/opencode/src/session/prompt/summarize-turn.txt b/packages/opencode/src/session/prompt/summarize-turn.txt
new file mode 100644
index 000000000..718d0f637
--- /dev/null
+++ b/packages/opencode/src/session/prompt/summarize-turn.txt
@@ -0,0 +1,5 @@
+Your job is to generate a summary of what happened in this conversation and why.
+
+Keep the results to 2-3 sentences.
+
+Output the message summary now:
diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts
new file mode 100644
index 000000000..c5fd8c123
--- /dev/null
+++ b/packages/opencode/src/session/summary.ts
@@ -0,0 +1,46 @@
+import { Provider } from "@/provider/provider"
+import { fn } from "@/util/fn"
+import z from "zod"
+import { Session } from "."
+import { generateText } from "ai"
+import { MessageV2 } from "./message-v2"
+import SUMMARIZE_TURN from "./prompt/summarize-turn.txt"
+import { Flag } from "@/flag/flag"
+
+export namespace MessageSummary {
+ export const summarize = fn(
+ z.object({
+ sessionID: z.string(),
+ messageID: z.string(),
+ providerID: z.string(),
+ }),
+ async (input) => {
+ if (!Flag.OPENCODE_EXPERIMENTAL_TURN_SUMMARY) return
+ const messages = await Session.messages(input.sessionID).then((msgs) =>
+ msgs.filter(
+ (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
+ ),
+ )
+ const small = await Provider.getSmallModel(input.providerID)
+ if (!small) return
+
+ const result = await generateText({
+ model: small.language,
+ messages: [
+ {
+ role: "system",
+ content: SUMMARIZE_TURN,
+ },
+ ...MessageV2.toModelMessage(messages),
+ ],
+ })
+
+ const userMsg = messages.find((m) => m.info.id === input.messageID)!
+ userMsg.info.summary = {
+ text: result.text,
+ diffs: [],
+ }
+ await Session.updateMessage(userMsg.info)
+ },
+ )
+}