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
|
```typescript
|
||||||
pi.on("session_before_compact", async (event, ctx) => {
|
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.messagesToSummarize - messages to summarize
|
||||||
// preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
|
// preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
|
||||||
@@ -287,6 +287,8 @@ pi.on("session_before_compact", async (event, ctx) => {
|
|||||||
// preparation.settings - compaction settings
|
// preparation.settings - compaction settings
|
||||||
|
|
||||||
// branchEntries - all entries on current branch (for custom state)
|
// 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)
|
// signal - AbortSignal (pass to LLM calls)
|
||||||
|
|
||||||
// Cancel:
|
// Cancel:
|
||||||
|
|||||||
@@ -437,7 +437,10 @@ Fired on compaction. See [compaction.md](compaction.md) for details.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
pi.on("session_before_compact", async (event, ctx) => {
|
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:
|
// Cancel:
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
@@ -455,6 +458,8 @@ pi.on("session_before_compact", async (event, ctx) => {
|
|||||||
pi.on("session_compact", async (event, ctx) => {
|
pi.on("session_compact", async (event, ctx) => {
|
||||||
// event.compactionEntry - the saved compaction
|
// event.compactionEntry - the saved compaction
|
||||||
// event.fromExtension - whether extension provided it
|
// 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,
|
preparation,
|
||||||
branchEntries: pathEntries,
|
branchEntries: pathEntries,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
|
reason: "manual",
|
||||||
|
willRetry: false,
|
||||||
signal: this._compactionAbortController.signal,
|
signal: this._compactionAbortController.signal,
|
||||||
})) as SessionBeforeCompactResult | undefined;
|
})) as SessionBeforeCompactResult | undefined;
|
||||||
|
|
||||||
@@ -1747,6 +1749,8 @@ export class AgentSession {
|
|||||||
type: "session_compact",
|
type: "session_compact",
|
||||||
compactionEntry: savedCompactionEntry,
|
compactionEntry: savedCompactionEntry,
|
||||||
fromExtension,
|
fromExtension,
|
||||||
|
reason: "manual",
|
||||||
|
willRetry: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1945,6 +1949,8 @@ export class AgentSession {
|
|||||||
preparation,
|
preparation,
|
||||||
branchEntries: pathEntries,
|
branchEntries: pathEntries,
|
||||||
customInstructions: undefined,
|
customInstructions: undefined,
|
||||||
|
reason,
|
||||||
|
willRetry,
|
||||||
signal: this._autoCompactionAbortController.signal,
|
signal: this._autoCompactionAbortController.signal,
|
||||||
})) as SessionBeforeCompactResult | undefined;
|
})) as SessionBeforeCompactResult | undefined;
|
||||||
|
|
||||||
@@ -2022,6 +2028,8 @@ export class AgentSession {
|
|||||||
type: "session_compact",
|
type: "session_compact",
|
||||||
compactionEntry: savedCompactionEntry,
|
compactionEntry: savedCompactionEntry,
|
||||||
fromExtension,
|
fromExtension,
|
||||||
|
reason,
|
||||||
|
willRetry,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -571,6 +571,10 @@ export interface SessionBeforeCompactEvent {
|
|||||||
preparation: CompactionPreparation;
|
preparation: CompactionPreparation;
|
||||||
branchEntries: SessionEntry[];
|
branchEntries: SessionEntry[];
|
||||||
customInstructions?: string;
|
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;
|
signal: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,6 +583,10 @@ export interface SessionCompactEvent {
|
|||||||
type: "session_compact";
|
type: "session_compact";
|
||||||
compactionEntry: CompactionEntry;
|
compactionEntry: CompactionEntry;
|
||||||
fromExtension: boolean;
|
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. */
|
/** 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