Refactor SessionEventBase to pass sessionManager and modelRegistry

Breaking changes to hook types:
- SessionEventBase now passes sessionManager and modelRegistry directly
- before_compact: passes preparation, previousCompactions (newest first)
- before_switch: has targetSessionFile; switch: has previousSessionFile
- Removed resolveApiKey (use modelRegistry.getApiKey())
- getSessionFile() returns string | undefined for in-memory sessions

Updated:
- All session event emissions in agent-session.ts
- Hook examples (custom-compaction.ts, auto-commit-on-exit.ts, confirm-destructive.ts)
- Tests (compaction-hooks.test.ts, compaction-hooks-example.test.ts)
- export-html.ts guards for in-memory sessions
This commit is contained in:
Mario Zechner 2025-12-26 22:22:43 +01:00
parent d96375b5e5
commit 9bba388ec5
14 changed files with 145 additions and 177 deletions

View file

@ -13,24 +13,23 @@ describe("Documentation example", () => {
if (event.reason !== "before_compact") return;
// After narrowing, these should all be accessible
const messages = event.messagesToSummarize;
const messagesToKeep = event.messagesToKeep;
const cutPoint = event.cutPoint;
const tokensBefore = event.tokensBefore;
const model = event.model;
const resolveApiKey = event.resolveApiKey;
const firstKeptEntryId = event.firstKeptEntryId;
const { preparation, previousCompactions, sessionManager, modelRegistry, model } = event;
const { messagesToSummarize, messagesToKeep, tokensBefore, firstKeptEntryId, cutPoint } = preparation;
// Get previous summary from most recent compaction
const _previousSummary = previousCompactions[0]?.summary;
// Verify types
expect(Array.isArray(messages)).toBe(true);
expect(Array.isArray(messagesToSummarize)).toBe(true);
expect(Array.isArray(messagesToKeep)).toBe(true);
expect(typeof cutPoint.firstKeptEntryIndex).toBe("number"); // cutPoint still uses index
expect(typeof cutPoint.firstKeptEntryIndex).toBe("number");
expect(typeof tokensBefore).toBe("number");
expect(model).toBeDefined();
expect(typeof resolveApiKey).toBe("function");
expect(typeof sessionManager.getEntries).toBe("function");
expect(typeof modelRegistry.getApiKey).toBe("function");
expect(typeof firstKeptEntryId).toBe("string");
const summary = messages
const summary = messagesToSummarize
.filter((m) => m.role === "user")
.map((m) => `- ${typeof m.content === "string" ? m.content.slice(0, 100) : "[complex]"}`)
.join("\n");
@ -57,12 +56,11 @@ describe("Documentation example", () => {
// After narrowing, these should all be accessible
const entry = event.compactionEntry;
const tokensBefore = event.tokensBefore;
const fromHook = event.fromHook;
expect(entry.type).toBe("compaction");
expect(typeof entry.summary).toBe("string");
expect(typeof tokensBefore).toBe("number");
expect(typeof entry.tokensBefore).toBe("number");
expect(typeof fromHook).toBe("boolean");
});
};

View file

@ -40,7 +40,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
});
function createHook(
onBeforeCompact?: (event: SessionEvent) => { cancel?: boolean; compactionEntry?: any } | undefined,
onBeforeCompact?: (event: SessionEvent) => { cancel?: boolean; compaction?: any } | undefined,
onCompact?: (event: SessionEvent) => void,
): LoadedHook {
const handlers = new Map<string, ((event: any, ctx: any) => Promise<any>)[]>();
@ -98,7 +98,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
},
false,
);
hookRunner.setSessionFile(sessionManager.getSessionFile());
hookRunner.setSessionFile(sessionManager.getSessionFile() ?? null);
session = new AgentSession({
agent,
@ -131,20 +131,21 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
const beforeEvent = beforeCompactEvents[0];
if (beforeEvent.reason === "before_compact") {
expect(beforeEvent.cutPoint).toBeDefined();
expect(beforeEvent.cutPoint.firstKeptEntryIndex).toBeGreaterThanOrEqual(0);
expect(beforeEvent.messagesToSummarize).toBeDefined();
expect(beforeEvent.messagesToKeep).toBeDefined();
expect(beforeEvent.tokensBefore).toBeGreaterThanOrEqual(0);
expect(beforeEvent.preparation).toBeDefined();
expect(beforeEvent.preparation.cutPoint.firstKeptEntryIndex).toBeGreaterThanOrEqual(0);
expect(beforeEvent.preparation.messagesToSummarize).toBeDefined();
expect(beforeEvent.preparation.messagesToKeep).toBeDefined();
expect(beforeEvent.preparation.tokensBefore).toBeGreaterThanOrEqual(0);
expect(beforeEvent.model).toBeDefined();
expect(beforeEvent.resolveApiKey).toBeDefined();
expect(beforeEvent.sessionManager).toBeDefined();
expect(beforeEvent.modelRegistry).toBeDefined();
}
const afterEvent = compactEvents[0];
if (afterEvent.reason === "compact") {
expect(afterEvent.compactionEntry).toBeDefined();
expect(afterEvent.compactionEntry.summary.length).toBeGreaterThan(0);
expect(afterEvent.tokensBefore).toBeGreaterThanOrEqual(0);
expect(afterEvent.compactionEntry.tokensBefore).toBeGreaterThanOrEqual(0);
expect(afterEvent.fromHook).toBe(false);
}
}, 120000);
@ -162,18 +163,16 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
expect(compactEvents.length).toBe(0);
}, 120000);
it("should allow hooks to provide custom compactionEntry", async () => {
it("should allow hooks to provide custom compaction", async () => {
const customSummary = "Custom summary from hook";
const hook = createHook((event) => {
if (event.reason === "before_compact") {
return {
compactionEntry: {
type: "compaction" as const,
timestamp: new Date().toISOString(),
compaction: {
summary: customSummary,
firstKeptEntryIndex: event.cutPoint.firstKeptEntryIndex,
tokensBefore: event.tokensBefore,
firstKeptEntryId: event.preparation.firstKeptEntryId,
tokensBefore: event.preparation.tokensBefore,
},
};
}
@ -215,7 +214,8 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
const afterEvent = compactEvents[0];
if (afterEvent.reason === "compact") {
const hasCompactionEntry = afterEvent.entries.some((e) => e.type === "compaction");
const entries = afterEvent.sessionManager.getEntries();
const hasCompactionEntry = entries.some((e) => e.type === "compaction");
expect(hasCompactionEntry).toBe(true);
}
}, 120000);
@ -337,35 +337,34 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
expect(capturedBeforeEvent).not.toBeNull();
const event = capturedBeforeEvent!;
expect(event.cutPoint).toHaveProperty("firstKeptEntryIndex");
expect(event.cutPoint).toHaveProperty("isSplitTurn");
expect(event.cutPoint).toHaveProperty("turnStartIndex");
expect(event.preparation.cutPoint).toHaveProperty("firstKeptEntryIndex");
expect(event.preparation.cutPoint).toHaveProperty("isSplitTurn");
expect(event.preparation.cutPoint).toHaveProperty("turnStartIndex");
expect(Array.isArray(event.messagesToSummarize)).toBe(true);
expect(Array.isArray(event.messagesToKeep)).toBe(true);
expect(Array.isArray(event.preparation.messagesToSummarize)).toBe(true);
expect(Array.isArray(event.preparation.messagesToKeep)).toBe(true);
expect(typeof event.tokensBefore).toBe("number");
expect(typeof event.preparation.tokensBefore).toBe("number");
expect(event.model).toHaveProperty("provider");
expect(event.model).toHaveProperty("id");
expect(typeof event.resolveApiKey).toBe("function");
expect(typeof event.modelRegistry.getApiKey).toBe("function");
expect(Array.isArray(event.entries)).toBe(true);
expect(event.entries.length).toBeGreaterThan(0);
const entries = event.sessionManager.getEntries();
expect(Array.isArray(entries)).toBe(true);
expect(entries.length).toBeGreaterThan(0);
}, 120000);
it("should use hook compactionEntry even with different firstKeptEntryIndex", async () => {
const customSummary = "Custom summary with modified index";
it("should use hook compaction even with different values", async () => {
const customSummary = "Custom summary with modified values";
const hook = createHook((event) => {
if (event.reason === "before_compact") {
return {
compactionEntry: {
type: "compaction" as const,
timestamp: new Date().toISOString(),
compaction: {
summary: customSummary,
firstKeptEntryIndex: 0,
firstKeptEntryId: event.preparation.firstKeptEntryId,
tokensBefore: 999,
},
};