fix(opencode): serialize orphaned compaction history (#40800)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
4168574561
commit
0ac33c552f
@@ -49,6 +49,42 @@ type CompletedCompaction = {
|
|||||||
summary: string | undefined
|
summary: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const truncate = (value: string) =>
|
||||||
|
value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]`
|
||||||
|
|
||||||
|
const serialize = (message: SessionV1.WithParts) => {
|
||||||
|
if (message.info.role === "user") {
|
||||||
|
const text = message.parts
|
||||||
|
.filter((part): part is SessionV1.TextPart => part.type === "text" && !part.ignored)
|
||||||
|
.map((part) => part.text)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
const files = message.parts.flatMap((part) =>
|
||||||
|
part.type === "file" ? [`[Attached ${part.mime}: ${part.filename ?? "file"}]`] : [],
|
||||||
|
)
|
||||||
|
return [...(text ? [`[User]: ${text}`] : []), ...files].join("\n")
|
||||||
|
}
|
||||||
|
return message.parts
|
||||||
|
.flatMap((part) => {
|
||||||
|
if (part.type === "text") return part.text ? [`[Assistant]: ${part.text}`] : []
|
||||||
|
if (part.type === "reasoning") return part.text ? [`[Assistant reasoning]: ${part.text}`] : []
|
||||||
|
if (part.type !== "tool") return []
|
||||||
|
const call = `[Assistant tool call]: ${part.tool}(${JSON.stringify(part.state.input)})`
|
||||||
|
if (part.state.status === "completed") {
|
||||||
|
const attachments = (part.state.attachments ?? []).map(
|
||||||
|
(item) => `[Attached ${item.mime}: ${item.filename ?? "file"}]`,
|
||||||
|
)
|
||||||
|
const output = part.state.time.compacted
|
||||||
|
? "[Old tool result content cleared]"
|
||||||
|
: truncate([part.state.output, ...attachments].join("\n"))
|
||||||
|
return [call, `[Tool result]: ${output}`]
|
||||||
|
}
|
||||||
|
if (part.state.status === "error") return [call, `[Tool error]: ${part.state.error}`]
|
||||||
|
return [call]
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
function summaryText(message: SessionV1.WithParts) {
|
function summaryText(message: SessionV1.WithParts) {
|
||||||
const text = message.parts
|
const text = message.parts
|
||||||
.filter((part): part is SessionV1.TextPart => part.type === "text")
|
.filter((part): part is SessionV1.TextPart => part.type === "text")
|
||||||
@@ -348,10 +384,7 @@ const layer = Layer.effect(
|
|||||||
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
|
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
|
||||||
const msgs = structuredClone(selected.head)
|
const msgs = structuredClone(selected.head)
|
||||||
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||||
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
|
const conversation = msgs.map(serialize).filter(Boolean).join("\n\n")
|
||||||
stripMedia: true,
|
|
||||||
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
|
|
||||||
})
|
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
const msg: SessionV1.Assistant = {
|
const msg: SessionV1.Assistant = {
|
||||||
id: MessageID.ascending(),
|
id: MessageID.ascending(),
|
||||||
@@ -392,10 +425,16 @@ const layer = Layer.effect(
|
|||||||
tools: {},
|
tools: {},
|
||||||
system: [],
|
system: [],
|
||||||
messages: [
|
messages: [
|
||||||
...modelMessages,
|
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: nextPrompt }],
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: [nextPrompt, "The following is the conversation history:", conversation]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
model,
|
model,
|
||||||
|
|||||||
@@ -1362,10 +1362,10 @@ describe("session.compaction.process", () => {
|
|||||||
"summarizes only the head while keeping recent tail out of summary input",
|
"summarizes only the head while keeping recent tail out of summary input",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
let captured = ""
|
let messages: LLM.StreamInput["messages"] = []
|
||||||
stub.push(
|
stub.push(
|
||||||
reply("summary", (input) => {
|
reply("summary", (input) => {
|
||||||
captured = JSON.stringify(input.messages)
|
messages = input.messages
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
@@ -1386,7 +1386,10 @@ describe("session.compaction.process", () => {
|
|||||||
auto: false,
|
auto: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(captured).toContain("older context")
|
const captured = JSON.stringify(messages)
|
||||||
|
expect(messages).toHaveLength(1)
|
||||||
|
expect(messages[0]?.role).toBe("user")
|
||||||
|
expect(captured).toContain("[User]: older context")
|
||||||
expect(captured).not.toContain("keep this turn")
|
expect(captured).not.toContain("keep this turn")
|
||||||
expect(captured).not.toContain("and this one too")
|
expect(captured).not.toContain("and this one too")
|
||||||
expect(captured).not.toContain("What did we do so far?")
|
expect(captured).not.toContain("What did we do so far?")
|
||||||
@@ -1437,6 +1440,74 @@ describe("session.compaction.process", () => {
|
|||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
itCompaction.instance(
|
||||||
|
"serializes repeated compaction history as one user message",
|
||||||
|
() => {
|
||||||
|
const stub = llm()
|
||||||
|
let captured: LLM.StreamInput["messages"] = []
|
||||||
|
stub.push(
|
||||||
|
reply("summary two", (input) => {
|
||||||
|
captured = input.messages
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const ssn = yield* SessionNs.Service
|
||||||
|
const test = yield* TestInstance
|
||||||
|
const session = yield* ssn.create({})
|
||||||
|
const turn = yield* createUserMessage(session.id, "original request")
|
||||||
|
const kept = yield* createAssistantMessage(session.id, turn.id, test.directory)
|
||||||
|
yield* ssn.updatePart({
|
||||||
|
id: PartID.ascending(),
|
||||||
|
messageID: kept.id,
|
||||||
|
sessionID: session.id,
|
||||||
|
type: "tool",
|
||||||
|
callID: "read-call",
|
||||||
|
tool: "read",
|
||||||
|
state: {
|
||||||
|
status: "completed",
|
||||||
|
input: { filePath: "src/index.ts" },
|
||||||
|
output: "file contents",
|
||||||
|
title: "src/index.ts",
|
||||||
|
metadata: {},
|
||||||
|
time: { start: Date.now(), end: Date.now() },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const previous = yield* ssn.updateMessage({
|
||||||
|
id: MessageID.ascending(),
|
||||||
|
role: "user",
|
||||||
|
model: ref,
|
||||||
|
sessionID: session.id,
|
||||||
|
agent: "build",
|
||||||
|
time: { created: Date.now() },
|
||||||
|
})
|
||||||
|
yield* ssn.updatePart({
|
||||||
|
id: PartID.ascending(),
|
||||||
|
messageID: previous.id,
|
||||||
|
sessionID: session.id,
|
||||||
|
type: "compaction",
|
||||||
|
auto: false,
|
||||||
|
tail_start_id: kept.id,
|
||||||
|
})
|
||||||
|
yield* createSummaryAssistantMessage(session.id, previous.id, test.directory, "summary one")
|
||||||
|
yield* createCompactionMarker(session.id)
|
||||||
|
|
||||||
|
const msgs = MessageV2.filterCompacted(yield* MessageV2.stream(session.id))
|
||||||
|
const parent = msgs.at(-1)?.info.id
|
||||||
|
expect(parent).toBeTruthy()
|
||||||
|
yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false })
|
||||||
|
|
||||||
|
expect(captured).toHaveLength(1)
|
||||||
|
expect(captured[0]?.role).toBe("user")
|
||||||
|
expect(JSON.stringify(captured)).toContain('[Assistant tool call]: read({\\"filePath\\":\\"src/index.ts\\"})')
|
||||||
|
expect(JSON.stringify(captured)).toContain("[Tool result]: file contents")
|
||||||
|
expect(JSON.stringify(captured)).not.toContain('\\"role\\":\\"assistant\\"')
|
||||||
|
}).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 0 }) }))
|
||||||
|
},
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
itCompaction.instance("keeps recent pre-compaction turns across repeated compactions", () => {
|
itCompaction.instance("keeps recent pre-compaction turns across repeated compactions", () => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
stub.push(reply("summary one"))
|
stub.push(reply("summary one"))
|
||||||
|
|||||||
Reference in New Issue
Block a user