feat: add experimental.compaction.autocontinue hook to disable auto continuing after compaction (#22361)

This commit is contained in:
Aiden Cline
2026-04-13 20:14:53 -05:00
committed by GitHub
parent 10ba68c772
commit 34e2429c49
3 changed files with 120 additions and 25 deletions
@@ -310,6 +310,25 @@ When constructing the summary, try to stick to this template:
} }
if (!replay) { if (!replay) {
const info = yield* provider.getProvider(userMessage.model.providerID)
if (
(yield* plugin.trigger(
"experimental.compaction.autocontinue",
{
sessionID: input.sessionID,
agent: userMessage.agent,
model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID),
provider: {
source: info.source,
info,
options: info.options,
},
message: userMessage,
overflow: input.overflow === true,
},
{ enabled: true },
)).enabled
) {
const continueMsg = yield* session.updateMessage({ const continueMsg = yield* session.updateMessage({
id: MessageID.ascending(), id: MessageID.ascending(),
role: "user", role: "user",
@@ -337,6 +356,7 @@ When constructing the summary, try to stick to this template:
}) })
} }
} }
}
if (processor.message.error) return "stop" if (processor.message.error) return "stop"
if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID }) if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
@@ -244,6 +244,20 @@ function plugin(ready: ReturnType<typeof defer>) {
}) })
} }
function autocontinue(enabled: boolean) {
return Layer.mock(Plugin.Service)({
trigger: <Name extends string, Input, Output>(name: Name, _input: Input, output: Output) => {
if (name !== "experimental.compaction.autocontinue") return Effect.succeed(output)
return Effect.sync(() => {
;(output as { enabled: boolean }).enabled = enabled
return output
})
},
list: () => Effect.succeed([]),
init: () => Effect.void,
})
}
describe("session.compaction.isOverflow", () => { describe("session.compaction.isOverflow", () => {
test("returns true when token count exceeds usable context", async () => { test("returns true when token count exceeds usable context", async () => {
await using tmp = await tmpdir() await using tmp = await tmpdir()
@@ -671,6 +685,49 @@ describe("session.compaction.process", () => {
}) })
}) })
test("allows plugins to disable synthetic continue prompt", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const msg = await user(session.id, "hello")
const rt = runtime("continue", autocontinue(false), wide())
try {
const msgs = await Session.messages({ sessionID: session.id })
const result = await rt.runPromise(
SessionCompaction.Service.use((svc) =>
svc.process({
parentID: msg.id,
messages: msgs,
sessionID: session.id,
auto: true,
}),
),
)
const all = await Session.messages({ sessionID: session.id })
const last = all.at(-1)
expect(result).toBe("continue")
expect(last?.info.role).toBe("assistant")
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)
} finally {
await rt.dispose()
}
},
})
})
test("replays the prior user turn on overflow when earlier context exists", async () => { test("replays the prior user turn on overflow when earlier context exists", async () => {
await using tmp = await tmpdir() await using tmp = await tmpdir()
await Instance.provide({ await Instance.provide({
+18
View File
@@ -304,6 +304,24 @@ export interface Hooks {
input: { sessionID: string }, input: { sessionID: string },
output: { context: string[]; prompt?: string }, output: { context: string[]; prompt?: string },
) => Promise<void> ) => Promise<void>
/**
* Called after compaction succeeds and before a synthetic user
* auto-continue message is added.
*
* - `enabled`: Defaults to `true`. Set to `false` to skip the synthetic
* user "continue" turn.
*/
"experimental.compaction.autocontinue"?: (
input: {
sessionID: string
agent: string
model: Model
provider: ProviderContext
message: UserMessage
overflow: boolean
},
output: { enabled: boolean },
) => Promise<void>
"experimental.text.complete"?: ( "experimental.text.complete"?: (
input: { sessionID: string; messageID: string; partID: string }, input: { sessionID: string; messageID: string; partID: string },
output: { text: string }, output: { text: string },