diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 0437c4c6..fff271cd 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 2209fda5..2c9349ea 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 1b17764a..d0f25181 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 717fbc24..bcc19005 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 00000000..718d0f63 --- /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 00000000..c5fd8c12 --- /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) + }, + ) +}