refactor(coding-agent): fix compaction for branched sessions, consolidate hook context types

Compaction API:
- prepareCompaction() now takes (pathEntries, settings) only
- CompactionPreparation restructured: removed cutPoint/messagesToKeep/boundaryStart, added turnPrefixMessages/isSplitTurn/previousSummary/fileOps/settings
- compact() now takes (preparation, model, apiKey, customInstructions?, signal?)
- Fixed token overflow by using getPath() instead of getEntries()

Hook types:
- HookEventContext renamed to HookContext
- HookCommandContext removed, RegisteredCommand.handler takes (args, ctx)
- HookContext now includes model field
- SessionBeforeCompactEvent: removed previousCompactions/model, added branchEntries
- SessionBeforeTreeEvent: removed model (use ctx.model)
- HookRunner.initialize() added for modes to set up callbacks
This commit is contained in:
Mario Zechner 2025-12-31 02:24:24 +01:00
parent b4ce93c577
commit ddda8b124c
12 changed files with 177 additions and 201 deletions

View file

@ -11,23 +11,27 @@ describe("Documentation example", () => {
const exampleHook = (pi: HookAPI) => {
pi.on("session_before_compact", async (event: SessionBeforeCompactEvent, ctx) => {
// All these should be accessible on the event
const { preparation, previousCompactions, model } = event;
// sessionManager and modelRegistry come from ctx, not event
const { sessionManager, modelRegistry } = ctx;
const { messagesToSummarize, messagesToKeep, tokensBefore, firstKeptEntryId, cutPoint } = preparation;
// Get previous summary from most recent compaction
const _previousSummary = previousCompactions[0]?.summary;
const { preparation, branchEntries, signal } = event;
// sessionManager, modelRegistry, and model come from ctx
const { sessionManager, modelRegistry, model } = ctx;
const {
messagesToSummarize,
turnPrefixMessages,
tokensBefore,
firstKeptEntryId,
isSplitTurn,
previousSummary,
} = preparation;
// Verify types
expect(Array.isArray(messagesToSummarize)).toBe(true);
expect(Array.isArray(messagesToKeep)).toBe(true);
expect(typeof cutPoint.firstKeptEntryIndex).toBe("number");
expect(Array.isArray(turnPrefixMessages)).toBe(true);
expect(typeof isSplitTurn).toBe("boolean");
expect(typeof tokensBefore).toBe("number");
expect(model).toBeDefined();
expect(typeof sessionManager.getEntries).toBe("function");
expect(typeof modelRegistry.getApiKey).toBe("function");
expect(typeof firstKeptEntryId).toBe("string");
expect(Array.isArray(branchEntries)).toBe(true);
const summary = messagesToSummarize
.filter((m) => m.role === "user")

View file

@ -143,12 +143,12 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
const beforeEvent = beforeCompactEvents[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.turnPrefixMessages).toBeDefined();
expect(beforeEvent.preparation.tokensBefore).toBeGreaterThanOrEqual(0);
expect(beforeEvent.model).toBeDefined();
// sessionManager and modelRegistry are now on ctx, not event
expect(typeof beforeEvent.preparation.isSplitTurn).toBe("boolean");
expect(beforeEvent.branchEntries).toBeDefined();
// sessionManager, modelRegistry, and model are now on ctx, not event
const afterEvent = compactEvents[0];
expect(afterEvent.compactionEntry).toBeDefined();
@ -363,19 +363,17 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
expect(capturedBeforeEvent).not.toBeNull();
const event = capturedBeforeEvent!;
expect(event.preparation.cutPoint).toHaveProperty("firstKeptEntryIndex");
expect(event.preparation.cutPoint).toHaveProperty("isSplitTurn");
expect(event.preparation.cutPoint).toHaveProperty("turnStartIndex");
expect(typeof event.preparation.isSplitTurn).toBe("boolean");
expect(event.preparation.firstKeptEntryId).toBeDefined();
expect(Array.isArray(event.preparation.messagesToSummarize)).toBe(true);
expect(Array.isArray(event.preparation.messagesToKeep)).toBe(true);
expect(Array.isArray(event.preparation.turnPrefixMessages)).toBe(true);
expect(typeof event.preparation.tokensBefore).toBe("number");
expect(event.model).toHaveProperty("provider");
expect(event.model).toHaveProperty("id");
expect(Array.isArray(event.branchEntries)).toBe(true);
// sessionManager and modelRegistry are now on ctx, not event
// sessionManager, modelRegistry, and model are now on ctx, not event
// Verify they're accessible via session
expect(typeof session.sessionManager.getEntries).toBe("function");
expect(typeof session.modelRegistry.getApiKey).toBe("function");

View file

@ -11,6 +11,7 @@ import {
DEFAULT_COMPACTION_SETTINGS,
findCutPoint,
getLastAssistantUsage,
prepareCompaction,
shouldCompact,
} from "../src/core/compaction/index.js";
import {
@ -398,12 +399,10 @@ describe.skipIf(!process.env.ANTHROPIC_OAUTH_TOKEN)("LLM summarization", () => {
const entries = loadLargeSessionEntries();
const model = getModel("anthropic", "claude-sonnet-4-5")!;
const compactionResult = await compact(
entries,
model,
DEFAULT_COMPACTION_SETTINGS,
process.env.ANTHROPIC_OAUTH_TOKEN!,
);
const preparation = prepareCompaction(entries, DEFAULT_COMPACTION_SETTINGS);
expect(preparation).toBeDefined();
const compactionResult = await compact(preparation!, model, process.env.ANTHROPIC_OAUTH_TOKEN!);
expect(compactionResult.summary.length).toBeGreaterThan(100);
expect(compactionResult.firstKeptEntryId).toBeTruthy();
@ -421,12 +420,10 @@ describe.skipIf(!process.env.ANTHROPIC_OAUTH_TOKEN)("LLM summarization", () => {
const loaded = buildSessionContext(entries);
const model = getModel("anthropic", "claude-sonnet-4-5")!;
const compactionResult = await compact(
entries,
model,
DEFAULT_COMPACTION_SETTINGS,
process.env.ANTHROPIC_OAUTH_TOKEN!,
);
const preparation = prepareCompaction(entries, DEFAULT_COMPACTION_SETTINGS);
expect(preparation).toBeDefined();
const compactionResult = await compact(preparation!, model, process.env.ANTHROPIC_OAUTH_TOKEN!);
// Simulate appending compaction to entries by creating a proper entry
const lastEntry = entries[entries.length - 1];