Make CompactionEntry and CompactionResult generic with details field

- CompactionEntry<T> and CompactionResult<T> now have optional details?: T
- appendCompaction() accepts optional details parameter
- Hooks can return compaction.details to store custom data
- Enables structured compaction with ArtifactIndex (see #314)
- Fix CompactionResult export location (now from compaction.ts)
- Update plan with remaining compaction refactor items
This commit is contained in:
Mario Zechner 2025-12-26 22:05:03 +01:00
parent efb1036d8e
commit d96375b5e5
9 changed files with 54 additions and 24 deletions

View file

@ -15,12 +15,15 @@
- New methods: `getTree()`, `getPath()`, `getLeafUuid()`, `getLeafEntry()`, `getEntry()`, `branchWithSummary()`
- New `appendCustomEntry(customType, data)` for hooks to store custom data
- **Compaction API**:
- `compact()` now returns `CompactionResult` (`{ summary, firstKeptEntryId, tokensBefore }`) instead of `CompactionEntry`
- `CompactionEntry<T>` and `CompactionResult<T>` are now generic with optional `details?: T` for hook-specific data
- `compact()` now returns `CompactionResult` (`{ summary, firstKeptEntryId, tokensBefore, details? }`) instead of `CompactionEntry`
- `appendCompaction()` now accepts optional `details` parameter
- `CompactionEntry.firstKeptEntryIndex` replaced with `firstKeptEntryId`
- `prepareCompaction()` now returns `firstKeptEntryId` in its result
- **Hook types**:
- `SessionEventResult.compactionEntry` replaced with `SessionEventResult.compaction` (content only, SessionManager adds id/parentId)
- `before_compact` event now includes `firstKeptEntryId` field for hooks that return custom compaction
- Hooks can return `compaction.details` to store custom data (e.g., ArtifactIndex for structured compaction)
### Added

View file

@ -58,8 +58,18 @@ Reference: [session-tree.md](./session-tree.md)
### Compaction Refactor
- [ ] Clean up types passed to hooks (currently messy mix of `CompactionEntry`, `CompactionResult`, hook's `compaction` content)
- [ ] Ensure consistent API between what hooks receive and what they return
- [x] Use `CompactionResult` type for hook return value
- [ ] Make `CompactionEntry<T>` generic with optional `details?: T` field for hook-specific data
- [ ] Make `CompactionResult<T>` generic to match
- [ ] Update `SessionEventBase` to pass `sessionManager` and `modelRegistry` instead of derived fields
- [ ] Update `before_compact` event:
- Pass `preparation: CompactionPreparation` instead of individual fields
- Pass `previousCompactions: CompactionEntry[]` (newest first) instead of `previousSummary?: string`
- Keep: `customInstructions`, `model`, `signal`
- Drop: `resolveApiKey` (use `modelRegistry.getApiKey()`), `cutPoint`, `entries`
- [ ] Update hook example `custom-compaction.ts` to use new API
Reference: [#314](https://github.com/badlogic/pi-mono/pull/314) - Structured compaction with anchored iterative summarization needs `details` field to store `ArtifactIndex` and version markers.
### Branch Summary Design

View file

@ -18,7 +18,13 @@ import type { AssistantMessage, Message, Model, TextContent } from "@mariozechne
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
import { getAuthPath } from "../config.js";
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
import { calculateContextTokens, compact, prepareCompaction, shouldCompact } from "./compaction.js";
import {
type CompactionResult,
calculateContextTokens,
compact,
prepareCompaction,
shouldCompact,
} from "./compaction.js";
import type { LoadedCustomTool, SessionEvent as ToolSessionEvent } from "./custom-tools/index.js";
import { exportSessionToHtml } from "./export-html.js";
import type { HookRunner, SessionEventResult, TurnEndEvent, TurnStartEvent } from "./hooks/index.js";
@ -76,12 +82,6 @@ export interface ModelCycleResult {
isScoped: boolean;
}
/** Result from compact() or checkAutoCompaction() */
export interface CompactionResult {
tokensBefore: number;
summary: string;
}
/** Session statistics for /session command */
export interface SessionStats {
sessionFile: string | null;
@ -771,7 +771,7 @@ export class AgentSession {
}
}
let hookCompaction: { summary: string; firstKeptEntryId: string; tokensBefore: number } | undefined;
let hookCompaction: CompactionResult | undefined;
let fromHook = false;
if (this._hookRunner?.hasHandlers("session")) {
@ -806,12 +806,14 @@ export class AgentSession {
let summary: string;
let firstKeptEntryId: string;
let tokensBefore: number;
let details: unknown;
if (hookCompaction) {
// Hook provided compaction content
summary = hookCompaction.summary;
firstKeptEntryId = hookCompaction.firstKeptEntryId;
tokensBefore = hookCompaction.tokensBefore;
details = hookCompaction.details;
} else {
// Generate compaction result
const result = await compact(
@ -825,13 +827,14 @@ export class AgentSession {
summary = result.summary;
firstKeptEntryId = result.firstKeptEntryId;
tokensBefore = result.tokensBefore;
details = result.details;
}
if (this._compactionAbortController.signal.aborted) {
throw new Error("Compaction cancelled");
}
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore);
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details);
const newEntries = this.sessionManager.getEntries();
const sessionContext = this.sessionManager.buildSessionContext();
this.agent.replaceMessages(sessionContext.messages);
@ -855,8 +858,10 @@ export class AgentSession {
}
return {
tokensBefore,
summary,
firstKeptEntryId,
tokensBefore,
details,
};
} finally {
this._compactionAbortController = null;
@ -952,7 +957,7 @@ export class AgentSession {
}
}
let hookCompaction: { summary: string; firstKeptEntryId: string; tokensBefore: number } | undefined;
let hookCompaction: CompactionResult | undefined;
let fromHook = false;
if (this._hookRunner?.hasHandlers("session")) {
@ -988,12 +993,14 @@ export class AgentSession {
let summary: string;
let firstKeptEntryId: string;
let tokensBefore: number;
let details: unknown;
if (hookCompaction) {
// Hook provided compaction content
summary = hookCompaction.summary;
firstKeptEntryId = hookCompaction.firstKeptEntryId;
tokensBefore = hookCompaction.tokensBefore;
details = hookCompaction.details;
} else {
// Generate compaction result
const compactResult = await compact(
@ -1006,6 +1013,7 @@ export class AgentSession {
summary = compactResult.summary;
firstKeptEntryId = compactResult.firstKeptEntryId;
tokensBefore = compactResult.tokensBefore;
details = compactResult.details;
}
if (this._autoCompactionAbortController.signal.aborted) {
@ -1013,7 +1021,7 @@ export class AgentSession {
return;
}
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore);
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details);
const newEntries = this.sessionManager.getEntries();
const sessionContext = this.sessionManager.buildSessionContext();
this.agent.replaceMessages(sessionContext.messages);
@ -1037,8 +1045,10 @@ export class AgentSession {
}
const result: CompactionResult = {
tokensBefore,
summary,
firstKeptEntryId,
tokensBefore,
details,
};
this._emit({ type: "auto_compaction_end", result, aborted: false, willRetry });

View file

@ -12,10 +12,12 @@ import { messageTransformer } from "./messages.js";
import type { CompactionEntry, SessionEntry } from "./session-manager.js";
/** Result from compact() - SessionManager adds uuid/parentUuid when saving */
export interface CompactionResult {
export interface CompactionResult<T = unknown> {
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
/** Hook-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
details?: T;
}
// ============================================================================

View file

@ -7,12 +7,12 @@ export {
type AgentSessionConfig,
type AgentSessionEvent,
type AgentSessionEventListener,
type CompactionResult,
type ModelCycleResult,
type PromptOptions,
type SessionStats,
} from "./agent-session.js";
export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor.js";
export type { CompactionResult } from "./compaction.js";
export {
type CustomAgentTool,
type CustomToolFactory,

View file

@ -49,11 +49,13 @@ export interface ModelChangeEntry extends SessionEntryBase {
modelId: string;
}
export interface CompactionEntry extends SessionEntryBase {
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
type: "compaction";
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
/** Hook-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
details?: T;
}
export interface BranchSummaryEntry extends SessionEntryBase {
@ -592,8 +594,8 @@ export class SessionManager {
}
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
appendCompaction(summary: string, firstKeptEntryId: string, tokensBefore: number): string {
const entry: CompactionEntry = {
appendCompaction<T = unknown>(summary: string, firstKeptEntryId: string, tokensBefore: number, details?: T): string {
const entry: CompactionEntry<T> = {
type: "compaction",
id: generateId(this.byId),
parentId: this.leafId || null,
@ -601,6 +603,7 @@ export class SessionManager {
summary,
firstKeptEntryId,
tokensBefore,
details,
};
this._appendEntry(entry);
return entry.id;

View file

@ -4,7 +4,6 @@ export {
type AgentSessionConfig,
type AgentSessionEvent,
type AgentSessionEventListener,
type CompactionResult,
type ModelCycleResult,
type PromptOptions,
type SessionStats,
@ -13,6 +12,7 @@ export {
export { type ApiKeyCredential, type AuthCredential, AuthStorage, type OAuthCredential } from "./core/auth-storage.js";
// Compaction
export {
type CompactionResult,
type CutPointResult,
calculateContextTokens,
compact,

View file

@ -7,8 +7,9 @@
import { type ChildProcess, spawn } from "node:child_process";
import * as readline from "node:readline";
import type { AgentEvent, AppMessage, Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core";
import type { CompactionResult, SessionStats } from "../../core/agent-session.js";
import type { SessionStats } from "../../core/agent-session.js";
import type { BashResult } from "../../core/bash-executor.js";
import type { CompactionResult } from "../../core/compaction.js";
import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types.js";
// ============================================================================

View file

@ -7,8 +7,9 @@
import type { AppMessage, Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core";
import type { Model } from "@mariozechner/pi-ai";
import type { CompactionResult, SessionStats } from "../../core/agent-session.js";
import type { SessionStats } from "../../core/agent-session.js";
import type { BashResult } from "../../core/bash-executor.js";
import type { CompactionResult } from "../../core/compaction.js";
// ============================================================================
// RPC Commands (stdin)