Просмотр исходного кода

core: add experimental turn summarization to compact conversation history

Dax Raad 4 месяцев назад
Родитель
Сommit
1f80de2fa6

+ 1 - 0
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) {

+ 1 - 1
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),

+ 9 - 1
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)

+ 7 - 1
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" &&

+ 5 - 0
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:

+ 46 - 0
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)
+    },
+  )
+}