feat(coding-agent): add compaction reason and willRetry to extension compact events (#5962)

session_before_compact and session_compact now carry
reason ("manual" | "threshold" | "overflow") and willRetry, matching
what the RPC protocol already exposes via auto_compaction_start/end.
Extensions can now distinguish manual /compact from threshold
auto-compaction and overflow recovery through the public API instead
of monkeypatching AgentSession internals.
This commit is contained in:
Enrico
2026-06-22 10:36:16 +02:00
committed by GitHub
parent 7d0497fdb7
commit 5b9b70d29c
5 changed files with 120 additions and 2 deletions
+3 -1
View File
@@ -276,7 +276,7 @@ Fired before auto-compaction or `/compact`. Can cancel or provide custom summary
```typescript
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, signal } = event;
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// preparation.messagesToSummarize - messages to summarize
// preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
@@ -287,6 +287,8 @@ pi.on("session_before_compact", async (event, ctx) => {
// preparation.settings - compaction settings
// branchEntries - all entries on current branch (for custom state)
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction (overflow recovery)
// signal - AbortSignal (pass to LLM calls)
// Cancel:
+6 -1
View File
@@ -437,7 +437,10 @@ Fired on compaction. See [compaction.md](compaction.md) for details.
```typescript
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, signal } = event;
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction (overflow recovery)
// Cancel:
return { cancel: true };
@@ -455,6 +458,8 @@ pi.on("session_before_compact", async (event, ctx) => {
pi.on("session_compact", async (event, ctx) => {
// event.compactionEntry - the saved compaction
// event.fromExtension - whether extension provided it
// event.reason - "manual" (/compact), "threshold", or "overflow"
// event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});
```
@@ -1684,6 +1684,8 @@ export class AgentSession {
preparation,
branchEntries: pathEntries,
customInstructions,
reason: "manual",
willRetry: false,
signal: this._compactionAbortController.signal,
})) as SessionBeforeCompactResult | undefined;
@@ -1747,6 +1749,8 @@ export class AgentSession {
type: "session_compact",
compactionEntry: savedCompactionEntry,
fromExtension,
reason: "manual",
willRetry: false,
});
}
@@ -1945,6 +1949,8 @@ export class AgentSession {
preparation,
branchEntries: pathEntries,
customInstructions: undefined,
reason,
willRetry,
signal: this._autoCompactionAbortController.signal,
})) as SessionBeforeCompactResult | undefined;
@@ -2022,6 +2028,8 @@ export class AgentSession {
type: "session_compact",
compactionEntry: savedCompactionEntry,
fromExtension,
reason,
willRetry,
});
}
@@ -571,6 +571,10 @@ export interface SessionBeforeCompactEvent {
preparation: CompactionPreparation;
branchEntries: SessionEntry[];
customInstructions?: string;
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
reason: "manual" | "threshold" | "overflow";
/** True when the aborted turn is retried after this compaction (overflow recovery) */
willRetry: boolean;
signal: AbortSignal;
}
@@ -579,6 +583,10 @@ export interface SessionCompactEvent {
type: "session_compact";
compactionEntry: CompactionEntry;
fromExtension: boolean;
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
reason: "manual" | "threshold" | "overflow";
/** True when the aborted turn is retried after this compaction (overflow recovery) */
willRetry: boolean;
}
/** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */
@@ -0,0 +1,95 @@
import { fauxAssistantMessage } from "@earendil-works/pi-ai";
import { afterEach, describe, expect, it } from "vitest";
import type { ExtensionFactory } from "../../../src/index.ts";
import { createHarness, type Harness } from "../harness.ts";
type SessionWithCompactionInternals = {
_runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise<boolean>;
};
interface RecordedCompactionEvent {
type: "session_before_compact" | "session_compact";
reason: "manual" | "threshold" | "overflow";
willRetry: boolean;
}
function recordingExtension(recorded: RecordedCompactionEvent[]): ExtensionFactory {
return (pi) => {
pi.on("session_before_compact", async (event) => {
recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry });
return {
compaction: {
summary: "summary from extension",
firstKeptEntryId: event.preparation.firstKeptEntryId,
tokensBefore: event.preparation.tokensBefore,
details: {},
},
};
});
pi.on("session_compact", async (event) => {
recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry });
});
};
}
async function createCompactionHarness(recorded: RecordedCompactionEvent[]): Promise<Harness> {
const harness = await createHarness({
settings: { compaction: { keepRecentTokens: 1 } },
extensionFactories: [recordingExtension(recorded)],
});
harness.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two")]);
await harness.session.prompt("first");
await harness.session.prompt("second");
return harness;
}
describe("issue #5217 compaction reason on extension events", () => {
const harnesses: Harness[] = [];
afterEach(() => {
while (harnesses.length > 0) {
harnesses.pop()?.cleanup();
}
});
it("reports manual reason for compact()", async () => {
const recorded: RecordedCompactionEvent[] = [];
const harness = await createCompactionHarness(recorded);
harnesses.push(harness);
await harness.session.compact();
expect(recorded).toEqual([
{ type: "session_before_compact", reason: "manual", willRetry: false },
{ type: "session_compact", reason: "manual", willRetry: false },
]);
});
it("reports threshold reason for auto-compaction", async () => {
const recorded: RecordedCompactionEvent[] = [];
const harness = await createCompactionHarness(recorded);
harnesses.push(harness);
const sessionInternals = harness.session as unknown as SessionWithCompactionInternals;
await sessionInternals._runAutoCompaction("threshold", false);
expect(recorded).toEqual([
{ type: "session_before_compact", reason: "threshold", willRetry: false },
{ type: "session_compact", reason: "threshold", willRetry: false },
]);
});
it("reports overflow reason and willRetry for overflow recovery", async () => {
const recorded: RecordedCompactionEvent[] = [];
const harness = await createCompactionHarness(recorded);
harnesses.push(harness);
const sessionInternals = harness.session as unknown as SessionWithCompactionInternals;
await sessionInternals._runAutoCompaction("overflow", true);
expect(recorded).toEqual([
{ type: "session_before_compact", reason: "overflow", willRetry: true },
{ type: "session_compact", reason: "overflow", willRetry: true },
]);
});
});