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:
@@ -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:
|
||||
|
||||
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user