mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 18:24:21 +01:00
core: add experimental turn summarization to compact conversation history
This commit is contained in:
@@ -12,6 +12,7 @@ export namespace Flag {
|
|||||||
|
|
||||||
// Experimental
|
// Experimental
|
||||||
export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER")
|
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")
|
export const OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP = truthy("OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP")
|
||||||
|
|
||||||
function truthy(key: string) {
|
function truthy(key: string) {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export namespace SessionCompaction {
|
|||||||
draft.time.compacting = undefined
|
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 model = await Provider.getModel(input.providerID, input.modelID)
|
||||||
const system = [
|
const system = [
|
||||||
...SystemPrompt.summarize(model.providerID),
|
...SystemPrompt.summarize(model.providerID),
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Message } from "./message"
|
|||||||
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
|
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
|
import { Snapshot } from "@/snapshot"
|
||||||
|
import { fn } from "@/util/fn"
|
||||||
|
|
||||||
export namespace MessageV2 {
|
export namespace MessageV2 {
|
||||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||||
@@ -241,6 +243,12 @@ export namespace MessageV2 {
|
|||||||
time: z.object({
|
time: z.object({
|
||||||
created: z.number(),
|
created: z.number(),
|
||||||
}),
|
}),
|
||||||
|
summary: z
|
||||||
|
.object({
|
||||||
|
diffs: Snapshot.FileDiff.array(),
|
||||||
|
text: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
}).meta({
|
}).meta({
|
||||||
ref: "UserMessage",
|
ref: "UserMessage",
|
||||||
})
|
})
|
||||||
@@ -597,7 +605,7 @@ export namespace MessageV2 {
|
|||||||
return convertToModelMessages(result)
|
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)
|
const i = msgs.findLastIndex((m) => m.info.role === "assistant" && !!m.info.summary)
|
||||||
if (i === -1) return msgs.slice()
|
if (i === -1) return msgs.slice()
|
||||||
return msgs.slice(i)
|
return msgs.slice(i)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { spawn } from "child_process"
|
|||||||
import { Command } from "../command"
|
import { Command } from "../command"
|
||||||
import { $, fileURLToPath } from "bun"
|
import { $, fileURLToPath } from "bun"
|
||||||
import { ConfigMarkdown } from "../config/markdown"
|
import { ConfigMarkdown } from "../config/markdown"
|
||||||
|
import { MessageSummary } from "./summary"
|
||||||
|
|
||||||
export namespace SessionPrompt {
|
export namespace SessionPrompt {
|
||||||
const log = Log.create({ service: "session.prompt" })
|
const log = Log.create({ service: "session.prompt" })
|
||||||
@@ -345,6 +346,11 @@ export namespace SessionPrompt {
|
|||||||
}
|
}
|
||||||
state().queued.delete(input.sessionID)
|
state().queued.delete(input.sessionID)
|
||||||
SessionCompaction.prune(input)
|
SessionCompaction.prune(input)
|
||||||
|
MessageSummary.summarize({
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
messageID: result.info.parentID,
|
||||||
|
providerID: model.providerID,
|
||||||
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,7 +361,7 @@ export namespace SessionPrompt {
|
|||||||
providerID: string
|
providerID: string
|
||||||
signal: AbortSignal
|
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")
|
const lastAssistant = msgs.findLast((msg) => msg.info.role === "assistant")
|
||||||
if (
|
if (
|
||||||
lastAssistant?.info.role === "assistant" &&
|
lastAssistant?.info.role === "assistant" &&
|
||||||
|
|||||||
5
packages/opencode/src/session/prompt/summarize-turn.txt
Normal file
5
packages/opencode/src/session/prompt/summarize-turn.txt
Normal 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:
|
||||||
46
packages/opencode/src/session/summary.ts
Normal file
46
packages/opencode/src/session/summary.ts
Normal 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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user