Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,16 @@ export const layer = Layer.effect(
}

if (!replay) {
const lastNonSummaryAssistant = [...input.messages]
.reverse()
.find((m) => m.info.role === "assistant" && !m.info.summary)?.info as MessageV2.Assistant | undefined
const shouldAutoContinue =
!lastNonSummaryAssistant?.finish ||
lastNonSummaryAssistant.finish === "tool-calls" ||
lastNonSummaryAssistant.finish === "unknown"

if (!shouldAutoContinue) return result

const info = yield* provider.getProvider(userMessage.model.providerID)
if (
(yield* plugin.trigger(
Expand Down
22 changes: 13 additions & 9 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const hasToolCalls =
lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false

if (lastFinished && lastFinished.summary !== true) {
const modelForCompaction = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID).pipe(
Effect.exit,
)
if (
Exit.isSuccess(modelForCompaction) &&
(yield* compaction.isOverflow({ tokens: lastFinished.tokens, model: modelForCompaction.value }))
) {
yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })
continue
}
}

if (
lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) &&
Expand Down Expand Up @@ -1707,15 +1720,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
continue
}

if (
lastFinished &&
lastFinished.summary !== true &&
(yield* compaction.isOverflow({ tokens: lastFinished.tokens, model }))
) {
yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })
continue
}

const agent = yield* agents.get(lastUser.agent)
if (!agent) {
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
Expand Down
34 changes: 34 additions & 0 deletions packages/opencode/test/session/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,40 @@ describe("session.compaction.process", () => {
}),
)

itCompaction.instance(
"does not add synthetic continue prompt when auto compacting after a finished response",
Effect.gen(function* () {
const test = yield* TestInstance
const ssn = yield* SessionNs.Service
const session = yield* ssn.create({})
const msg = yield* createUserMessage(session.id, "hello")
yield* createAssistantMessage(session.id, msg.id, test.directory)
yield* createSummaryCompaction(session.id)
const msgs = yield* ssn.messages({ sessionID: session.id })
const parent = msgs.at(-1)?.info.id
expect(parent).toBeTruthy()

const result = yield* SessionCompaction.use.process({
parentID: parent!,
messages: msgs,
sessionID: session.id,
auto: true,
})

const all = yield* ssn.messages({ sessionID: session.id })
expect(result).toBe("continue")
expect(
all.some(
(msg) =>
msg.info.role === "user" &&
msg.parts.some(
(part) => part.type === "text" && part.synthetic && part.text.includes("Continue if you have next steps"),
),
),
).toBe(false)
}).pipe(withCompaction()),
)

itCompaction.instance(
"persists tail_start_id for retained recent turns",
Effect.gen(function* () {
Expand Down
59 changes: 53 additions & 6 deletions packages/opencode/test/session/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,22 @@ const blockingProcessor = Layer.succeed(
}),
)

function makeHttp(input?: { processor?: "blocking" }) {
let fakeCompactionCreateCount = 0
const fakeOverflowCompaction = Layer.succeed(
SessionCompaction.Service,
SessionCompaction.Service.of({
isOverflow: () => Effect.succeed(true),
prune: () => Effect.void,
process: () => Effect.succeed("stop" as const),
create: () =>
Effect.sync(() => {
fakeCompactionCreateCount++
throw new Error("fake compaction create called")
}),
}),
)

function makeHttp(input?: { processor?: "blocking"; compaction?: Layer.Layer<SessionCompaction.Service> }) {
const deps = Layer.mergeAll(
Session.defaultLayer,
Snapshot.defaultLayer,
Expand Down Expand Up @@ -208,11 +223,13 @@ function makeHttp(input?: { processor?: "blocking" }) {
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(deps),
)
const compact = SessionCompaction.layer.pipe(
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(proc),
Layer.provideMerge(deps),
)
const compact =
input?.compaction ??
SessionCompaction.layer.pipe(
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(proc),
Layer.provideMerge(deps),
)
return Layer.mergeAll(
TestLLMServer.layer,
SessionPrompt.layer.pipe(
Expand All @@ -235,6 +252,7 @@ function makeHttp(input?: { processor?: "blocking" }) {

const it = testEffect(makeHttp())
const race = testEffect(makeHttp({ processor: "blocking" }))
const overflow = testEffect(makeHttp({ compaction: fakeOverflowCompaction }))
const unix = process.platform !== "win32" ? it.instance : it.instance.skip

// Config that registers a custom "test" provider with a "test-model" model
Expand Down Expand Up @@ -477,6 +495,35 @@ it.instance(
{ git: true },
)

overflow.instance(
"loop creates compaction before exiting when the last finished assistant overflowed",
() =>
Effect.gen(function* () {
fakeCompactionCreateCount = 0
yield* useServerConfig(providerCfg)
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({ title: "Overflow finished" })
const { assistant } = yield* seed(chat.id, { finish: "stop" })

yield* sessions.updateMessage({
...assistant,
tokens: {
total: 120_000,
input: 120_000,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
})

const exit = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
expect(fakeCompactionCreateCount).toBe(1)
}),
{ git: true },
)

it.instance(
"prompt emits v2 prompted and synthetic events",
() =>
Expand Down
Loading