- Added session-domain modules and exports for session-entries, context, listing, loader, and migrations. - Changed persistence to async append writes plus writeTextAtomic, removing sync line APIs. - Added compaction-aware session context rebuild with dangling tool-call cleanup. - Added resumable session resolution with status inference, id/stem/suffix matching, and backup recovery.
344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
/**
|
|
* Event payload and result shapes shared between the extensions and hooks
|
|
* subsystems.
|
|
*
|
|
* Both subsystems observe the same agent/session lifecycle, so the *event*
|
|
* payloads (what happened) and the simpler *result* shapes (handler return
|
|
* values that don't depend on subsystem-specific identifiers like
|
|
* `AgentMessage` vs `Message`) are intentionally identical.
|
|
*
|
|
* Anything that diverges between the two subsystems — UI context, runtime
|
|
* context, command context, tool-call discrimination, or return shapes that
|
|
* carry subsystem-specific message types — lives in the per-subsystem
|
|
* `types.ts` files and is documented there.
|
|
*/
|
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
import type { CompactionPreparation, CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
import type { ImageContent, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
import type { Rule } from "../capability/rule";
|
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
import type { BranchSummaryEntry, CompactionEntry, SessionEntry } from "../session/session-entries";
|
|
import type { TodoItem } from "../tools/todo";
|
|
|
|
// ============================================================================
|
|
// Session Events
|
|
// ============================================================================
|
|
|
|
/** Fired on initial session load */
|
|
export interface SessionStartEvent {
|
|
type: "session_start";
|
|
}
|
|
|
|
/** Fired before switching to another session (can be cancelled) */
|
|
export interface SessionBeforeSwitchEvent {
|
|
type: "session_before_switch";
|
|
/** Reason for the switch */
|
|
reason: "new" | "resume" | "fork";
|
|
/** Session file we're switching to (only for "resume") */
|
|
targetSessionFile?: string;
|
|
}
|
|
|
|
/** Fired after switching to another session */
|
|
export interface SessionSwitchEvent {
|
|
type: "session_switch";
|
|
/** Reason for the switch */
|
|
reason: "new" | "resume" | "fork";
|
|
/** Session file we came from */
|
|
previousSessionFile: string | undefined;
|
|
}
|
|
|
|
/** Fired before branching a session (can be cancelled) */
|
|
export interface SessionBeforeBranchEvent {
|
|
type: "session_before_branch";
|
|
/** ID of the entry to branch from */
|
|
entryId: string;
|
|
}
|
|
|
|
/** Fired after branching a session */
|
|
export interface SessionBranchEvent {
|
|
type: "session_branch";
|
|
previousSessionFile: string | undefined;
|
|
}
|
|
|
|
/** Fired before context compaction (can be cancelled or customized) */
|
|
export interface SessionBeforeCompactEvent {
|
|
type: "session_before_compact";
|
|
/** Compaction preparation with messages to summarize, file ops, previous summary, etc. */
|
|
preparation: CompactionPreparation;
|
|
/** 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;
|
|
/** Abort signal - handlers should pass this to LLM calls and check it periodically */
|
|
signal: AbortSignal;
|
|
}
|
|
|
|
/** Fired before compaction summarization to customize prompts/context */
|
|
export interface SessionCompactingEvent {
|
|
type: "session.compacting";
|
|
sessionId: string;
|
|
messages: AgentMessage[];
|
|
}
|
|
|
|
/** Fired after context compaction */
|
|
export interface SessionCompactEvent {
|
|
type: "session_compact";
|
|
compactionEntry: CompactionEntry;
|
|
/** Whether the compaction entry was provided by an extension/hook */
|
|
fromExtension: boolean;
|
|
}
|
|
|
|
/** Fired on process exit (SIGINT/SIGTERM) */
|
|
export interface SessionShutdownEvent {
|
|
type: "session_shutdown";
|
|
}
|
|
|
|
/** Preparation data for tree navigation (used by session_before_tree event) */
|
|
export interface TreePreparation {
|
|
/** Node being switched to */
|
|
targetId: string;
|
|
/** Current active leaf (being abandoned), null if no current position */
|
|
oldLeafId: string | null;
|
|
/** Common ancestor of target and old leaf, null if no common ancestor */
|
|
commonAncestorId: string | null;
|
|
/** Entries to summarize (old leaf back to common ancestor or compaction) */
|
|
entriesToSummarize: SessionEntry[];
|
|
/** Whether user chose to summarize */
|
|
userWantsSummary: boolean;
|
|
}
|
|
|
|
/** Fired before navigating to a different node in the session tree (can be cancelled) */
|
|
export interface SessionBeforeTreeEvent {
|
|
type: "session_before_tree";
|
|
/** Preparation data for the navigation */
|
|
preparation: TreePreparation;
|
|
/** Abort signal - honors Escape during summarization (model available via ctx.model) */
|
|
signal: AbortSignal;
|
|
}
|
|
|
|
/** Fired after navigating to a different node in the session tree */
|
|
export interface SessionTreeEvent {
|
|
type: "session_tree";
|
|
/** The new active leaf, null if navigated to before first entry */
|
|
newLeafId: string | null;
|
|
/** Previous active leaf, null if there was no position */
|
|
oldLeafId: string | null;
|
|
/** Branch summary entry if one was created */
|
|
summaryEntry?: BranchSummaryEntry;
|
|
/** Whether summary came from extension/hook */
|
|
fromExtension?: boolean;
|
|
}
|
|
|
|
/** Union of all session event types */
|
|
export interface GoalUpdatedEvent {
|
|
type: "goal_updated";
|
|
goal: Goal | null;
|
|
state?: GoalModeState;
|
|
}
|
|
|
|
export type SessionEvent =
|
|
| SessionStartEvent
|
|
| SessionBeforeSwitchEvent
|
|
| SessionSwitchEvent
|
|
| SessionBeforeBranchEvent
|
|
| SessionBranchEvent
|
|
| SessionBeforeCompactEvent
|
|
| SessionCompactingEvent
|
|
| SessionCompactEvent
|
|
| SessionShutdownEvent
|
|
| SessionBeforeTreeEvent
|
|
| SessionTreeEvent
|
|
| GoalUpdatedEvent;
|
|
|
|
// ============================================================================
|
|
// Agent / Turn Events
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Fired before each LLM call.
|
|
*
|
|
* Original session messages are NOT modified - only the messages sent to the
|
|
* LLM are affected when a handler returns a replacement (the return shape
|
|
* differs between extensions and hooks; see each subsystem's
|
|
* `ContextEventResult`).
|
|
*/
|
|
export interface ContextEvent {
|
|
type: "context";
|
|
/** Messages about to be sent to the LLM (deep copy, safe to modify) */
|
|
messages: AgentMessage[];
|
|
}
|
|
|
|
/**
|
|
* Fired when an agent loop starts (once per user prompt).
|
|
*/
|
|
export interface AgentStartEvent {
|
|
type: "agent_start";
|
|
}
|
|
|
|
/** Fired when an agent loop ends */
|
|
export interface AgentEndEvent {
|
|
type: "agent_end";
|
|
messages: AgentMessage[];
|
|
}
|
|
|
|
/** Fired at the start of each turn */
|
|
export interface TurnStartEvent {
|
|
type: "turn_start";
|
|
turnIndex: number;
|
|
timestamp: number;
|
|
}
|
|
|
|
/** Fired at the end of each turn */
|
|
export interface TurnEndEvent {
|
|
type: "turn_end";
|
|
turnIndex: number;
|
|
message: AgentMessage;
|
|
toolResults: ToolResultMessage[];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Auto-compaction / Auto-retry Events
|
|
// ============================================================================
|
|
|
|
/** Fired when auto-compaction starts */
|
|
export interface AutoCompactionStartEvent {
|
|
type: "auto_compaction_start";
|
|
reason: "threshold" | "overflow" | "idle" | "incomplete";
|
|
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
}
|
|
|
|
/** Fired when auto-compaction ends */
|
|
export interface AutoCompactionEndEvent {
|
|
type: "auto_compaction_end";
|
|
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
result: CompactionResult | undefined;
|
|
aborted: boolean;
|
|
willRetry: boolean;
|
|
errorMessage?: string;
|
|
/** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
|
|
skipped?: boolean;
|
|
}
|
|
|
|
/** Fired when auto-retry starts */
|
|
export interface AutoRetryStartEvent {
|
|
type: "auto_retry_start";
|
|
attempt: number;
|
|
maxAttempts: number;
|
|
delayMs: number;
|
|
errorMessage: string;
|
|
}
|
|
|
|
/** Fired when auto-retry ends */
|
|
export interface AutoRetryEndEvent {
|
|
type: "auto_retry_end";
|
|
success: boolean;
|
|
attempt: number;
|
|
finalError?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// TTSR / Todo Reminders
|
|
// ============================================================================
|
|
|
|
/** Fired when TTSR rule matching interrupts generation */
|
|
export interface TtsrTriggeredEvent {
|
|
type: "ttsr_triggered";
|
|
rules: Rule[];
|
|
}
|
|
|
|
/** Fired when todo reminder logic detects unfinished todos */
|
|
export interface TodoReminderEvent {
|
|
type: "todo_reminder";
|
|
todos: TodoItem[];
|
|
attempt: number;
|
|
maxAttempts: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Shared Event Result Shapes
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Return type for `tool_call` handlers.
|
|
* Allows handlers to block tool execution.
|
|
*/
|
|
export interface ToolCallEventResult {
|
|
/** If true, block the tool from executing */
|
|
block?: boolean;
|
|
/** Reason for blocking (returned to LLM as error) */
|
|
reason?: string;
|
|
}
|
|
|
|
/**
|
|
* Return type for `tool_result` handlers.
|
|
* Allows handlers to modify tool results.
|
|
*/
|
|
export interface ToolResultEventResult {
|
|
/** Replacement content array (text and images) */
|
|
content?: (TextContent | ImageContent)[];
|
|
/** Replacement details */
|
|
details?: unknown;
|
|
/** Override isError flag */
|
|
isError?: boolean;
|
|
}
|
|
|
|
/** Return type for `session_before_switch` handlers */
|
|
export interface SessionBeforeSwitchResult {
|
|
/** If true, cancel the switch */
|
|
cancel?: boolean;
|
|
}
|
|
|
|
/** Return type for `session_before_branch` handlers */
|
|
export interface SessionBeforeBranchResult {
|
|
/**
|
|
* If true, abort the branch entirely. No new session file is created,
|
|
* conversation stays unchanged.
|
|
*/
|
|
cancel?: boolean;
|
|
/**
|
|
* If true, the branch proceeds (new session file created, session state updated)
|
|
* but the in-memory conversation is NOT rewound to the branch point.
|
|
*
|
|
* Use case: git-checkpoint handler that restores code state separately.
|
|
* The handler handles state restoration itself, so it doesn't want the
|
|
* agent's conversation to be rewound (which would lose recent context).
|
|
*
|
|
* - `cancel: true` → nothing happens, user stays in current session
|
|
* - `skipConversationRestore: true` → branch happens, but messages stay as-is
|
|
* - neither → branch happens AND messages rewind to branch point (default)
|
|
*/
|
|
skipConversationRestore?: boolean;
|
|
}
|
|
|
|
/** Return type for `session_before_compact` handlers */
|
|
export interface SessionBeforeCompactResult {
|
|
/** If true, cancel the compaction */
|
|
cancel?: boolean;
|
|
/** Custom compaction result - SessionManager adds id/parentId */
|
|
compaction?: CompactionResult;
|
|
}
|
|
|
|
/** Return type for `session.compacting` handlers */
|
|
export interface SessionCompactingResult {
|
|
/** Additional context lines to include in summary */
|
|
context?: string[];
|
|
/** Override the default compaction prompt */
|
|
prompt?: string;
|
|
/** Custom data to store in compaction entry */
|
|
preserveData?: Record<string, unknown>;
|
|
}
|
|
|
|
/** Return type for `session_before_tree` handlers */
|
|
export interface SessionBeforeTreeResult {
|
|
/** If true, cancel the navigation entirely */
|
|
cancel?: boolean;
|
|
/**
|
|
* Custom summary (skips default summarizer).
|
|
* Only used if preparation.userWantsSummary is true.
|
|
*/
|
|
summary?: {
|
|
summary: string;
|
|
details?: unknown;
|
|
};
|
|
}
|