diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d31056bd..84e5735d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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` and `CompactionResult` 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 diff --git a/packages/coding-agent/docs/session-tree-plan.md b/packages/coding-agent/docs/session-tree-plan.md index 1f058d5d..3c9bea18 100644 --- a/packages/coding-agent/docs/session-tree-plan.md +++ b/packages/coding-agent/docs/session-tree-plan.md @@ -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` generic with optional `details?: T` field for hook-specific data +- [ ] Make `CompactionResult` 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 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 1547ba61..a1bac423 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -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 }); diff --git a/packages/coding-agent/src/core/compaction.ts b/packages/coding-agent/src/core/compaction.ts index ea1e44a3..b0156ec5 100644 --- a/packages/coding-agent/src/core/compaction.ts +++ b/packages/coding-agent/src/core/compaction.ts @@ -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 { summary: string; firstKeptEntryId: string; tokensBefore: number; + /** Hook-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ + details?: T; } // ============================================================================ diff --git a/packages/coding-agent/src/core/index.ts b/packages/coding-agent/src/core/index.ts index 117d96b6..4b75e1cd 100644 --- a/packages/coding-agent/src/core/index.ts +++ b/packages/coding-agent/src/core/index.ts @@ -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, diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index e59a2934..0b5934b1 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -49,11 +49,13 @@ export interface ModelChangeEntry extends SessionEntryBase { modelId: string; } -export interface CompactionEntry extends SessionEntryBase { +export interface CompactionEntry 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(summary: string, firstKeptEntryId: string, tokensBefore: number, details?: T): string { + const entry: CompactionEntry = { 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; diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 6ddaedb1..acfc766a 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -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, diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 4b79b946..53a242e5 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -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"; // ============================================================================ diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index ab4f0b61..79f9e12a 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -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)