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

@ -3,6 +3,7 @@
*/
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { Model } from "@mariozechner/pi-ai";
import type { ModelRegistry } from "../model-registry.js";
import type { SessionManager } from "../session-manager.js";
import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js";
@ -11,9 +12,9 @@ import type {
BeforeAgentStartEventResult,
ContextEvent,
ContextEventResult,
HookContext,
HookError,
HookEvent,
HookEventContext,
HookMessageRenderer,
HookUIContext,
RegisteredCommand,
@ -72,6 +73,7 @@ export class HookRunner {
private modelRegistry: ModelRegistry;
private timeout: number;
private errorListeners: Set<HookErrorListener> = new Set();
private getModel: () => Model<any> | undefined = () => undefined;
constructor(
hooks: LoadedHook[],
@ -89,6 +91,30 @@ export class HookRunner {
this.timeout = timeout;
}
/**
* Initialize HookRunner with all required context.
* Modes call this once the agent session is fully set up.
*/
initialize(options: {
/** Function to get the current model */
getModel: () => Model<any> | undefined;
/** Handler for hooks to send messages */
sendMessageHandler: SendMessageHandler;
/** Handler for hooks to append entries */
appendEntryHandler: AppendEntryHandler;
/** UI context for interactive prompts */
uiContext?: HookUIContext;
/** Whether UI is available */
hasUI?: boolean;
}): void {
this.getModel = options.getModel;
this.setSendMessageHandler(options.sendMessageHandler);
this.setAppendEntryHandler(options.appendEntryHandler);
if (options.uiContext) {
this.setUIContext(options.uiContext, options.hasUI ?? false);
}
}
/**
* Set the UI context for hooks.
* Call this when the mode initializes and UI is available.
@ -217,13 +243,14 @@ export class HookRunner {
/**
* Create the event context for handlers.
*/
private createContext(): HookEventContext {
private createContext(): HookContext {
return {
ui: this.uiContext,
hasUI: this.hasUI,
cwd: this.cwd,
sessionManager: this.sessionManager,
modelRegistry: this.modelRegistry,
model: this.getModel(),
};
}

View file

@ -88,9 +88,9 @@ export interface HookUIContext {
}
/**
* Context passed to hook event handlers.
* Context passed to hook event and command handlers.
*/
export interface HookEventContext {
export interface HookContext {
/** UI methods for user interaction */
ui: HookUIContext;
/** Whether UI is available (false in print mode) */
@ -101,6 +101,8 @@ export interface HookEventContext {
sessionManager: ReadonlySessionManager;
/** Model registry - use for API key resolution and model retrieval */
modelRegistry: ModelRegistry;
/** Current model (may be undefined if no model is selected yet) */
model: Model<any> | undefined;
}
// ============================================================================
@ -152,14 +154,12 @@ export interface SessionBranchEvent {
/** Fired before context compaction (can be cancelled or customized) */
export interface SessionBeforeCompactEvent {
type: "session_before_compact";
/** Compaction preparation with cut point, messages to summarize/keep, etc. */
/** Compaction preparation with messages to summarize, file ops, previous summary, etc. */
preparation: CompactionPreparation;
/** Previous compaction entries, newest first. Use for iterative summarization. */
previousCompactions: CompactionEntry[];
/** Branch entries (root to current leaf). Use to inspect custom state or previous compactions. */
branchEntries: SessionEntry[];
/** Optional user-provided instructions for the summary */
customInstructions?: string;
/** Current model */
model: Model<any>;
/** Abort signal - hooks should pass this to LLM calls and check it periodically */
signal: AbortSignal;
}
@ -196,9 +196,7 @@ export interface SessionBeforeTreeEvent {
type: "session_before_tree";
/** Preparation data for the navigation */
preparation: TreePreparation;
/** Model to use for summarization (conversation model) */
model: Model<any>;
/** Abort signal - honors Escape during summarization */
/** Abort signal - honors Escape during summarization (model available via ctx.model) */
signal: AbortSignal;
}
@ -529,7 +527,7 @@ export interface SessionBeforeTreeResult {
* Handlers can return R, undefined, or void (bare return statements).
*/
// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements in handlers
export type HookHandler<E, R = undefined> = (event: E, ctx: HookEventContext) => Promise<R | void> | R | void;
export type HookHandler<E, R = undefined> = (event: E, ctx: HookContext) => Promise<R | void> | R | void;
export interface HookMessageRenderOptions {
/** Whether the view is expanded */
@ -546,24 +544,6 @@ export type HookMessageRenderer<T = unknown> = (
theme: Theme,
) => Component | undefined;
/**
* Context passed to hook command handlers.
*/
export interface HookCommandContext {
/** Arguments after the command name */
args: string;
/** UI methods for user interaction */
ui: HookUIContext;
/** Whether UI is available (false in print mode) */
hasUI: boolean;
/** Current working directory */
cwd: string;
/** Session manager (read-only) - use pi.sendMessage()/pi.appendEntry() for writes */
sessionManager: ReadonlySessionManager;
/** Model registry for API keys */
modelRegistry: ModelRegistry;
}
/**
* Command registration options.
*/
@ -571,7 +551,7 @@ export interface RegisteredCommand {
name: string;
description?: string;
handler: (ctx: HookCommandContext) => Promise<void>;
handler: (args: string, ctx: HookContext) => Promise<void>;
}
/**