core: add experimental turn summarization to compact conversation history

This commit is contained in:
Dax Raad
2025-10-22 18:33:33 -04:00
parent f194a784b0
commit 1f80de2fa6
6 changed files with 69 additions and 3 deletions

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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)

View File

@@ -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" &&

View File

@@ -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:

View File

@@ -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)
},
)
}