@sayknow-cli/agent-core 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Append-only context mode — stabilizes the byte prefix sent to the LLM
3
+ * across turns so provider prefix caches (DeepSeek, Anthropic, etc.)
4
+ * hit at the maximum possible rate.
5
+ *
6
+ * Two mechanisms:
7
+ *
8
+ * 1. **StablePrefix** — system prompt + tool specs are computed once
9
+ * and frozen. Subsequent turns reuse the exact same byte sequence
10
+ * unless `invalidate()` is called (e.g. after MCP reconnect).
11
+ *
12
+ * 2. **AppendOnlyLog** — messages only grow; prior turns are never
13
+ * re-serialized. Combined with a stable prefix, only the user's new
14
+ * message delta is a cache miss each turn.
15
+ */
16
+ import type { Context, Message, Tool } from "@sayknow-cli/ai";
17
+ import type { AgentContext } from "./types";
18
+ /** Frozen system prompt + tool spec snapshot. */
19
+ export interface StablePrefixSnapshot {
20
+ systemPrompt: string[];
21
+ tools: Tool[];
22
+ fingerprint: string;
23
+ }
24
+ /** Options threaded through `build()` so the snapshot reflects loop-time settings. */
25
+ export interface BuildOptions {
26
+ /** Inject the `_i` intent field into tool schemas (must match agent-loop's normalizeTools). */
27
+ intentTracing: boolean;
28
+ }
29
+ /**
30
+ * A frozen prefix (system prompt + tools) that produces stable byte
31
+ * sequences across `build()` calls.
32
+ *
33
+ * The first `build()` snapshots the live state. Subsequent calls reuse
34
+ * the cached copy until `invalidate()` is called or the live state's
35
+ * fingerprint changes.
36
+ */
37
+ export declare class StablePrefix {
38
+ #private;
39
+ get fingerprint(): string;
40
+ get version(): number;
41
+ get built(): boolean;
42
+ exportSnapshot(): StablePrefixSnapshot | null;
43
+ importSnapshot(snapshot: StablePrefixSnapshot, options: BuildOptions): void;
44
+ /**
45
+ * Build or rebuild from live context.
46
+ * Returns `true` if the prefix actually changed (cache miss imminent).
47
+ */
48
+ build(context: AgentContext, options: BuildOptions): boolean;
49
+ /** Force rebuild on the next `build()` call. */
50
+ invalidate(): void;
51
+ /**
52
+ * Returns the cached prefix.
53
+ * @throws if `build()` was never called.
54
+ */
55
+ toContext(): {
56
+ systemPrompt: string[];
57
+ tools: Tool[];
58
+ };
59
+ }
60
+ /**
61
+ * Append-only message log at the `Message[]` (provider-level) layer.
62
+ *
63
+ * The only mutation path is `replaceTail()`, reserved for compaction.
64
+ * Every other operation is append-only.
65
+ */
66
+ export declare class AppendOnlyLog {
67
+ #private;
68
+ get length(): number;
69
+ append(message: any): void;
70
+ extend(messages: any[]): void;
71
+ /** Replace the last entry — only legal for compaction. */
72
+ replaceTail(replacement: any): void;
73
+ /** Returns a shallow copy of all entries. */
74
+ toMessages(): Message[];
75
+ /** Direct readonly access for in-place inspection. */
76
+ entries(): readonly Message[];
77
+ clear(): void;
78
+ }
79
+ /**
80
+ * Manages a stable prefix + append-only log for the agent loop.
81
+ *
82
+ * Call `build(context)` each turn to get a `Context` with stable
83
+ * `systemPrompt` and `tools` and append-only messages. Call
84
+ * `syncMessages(normalizedMessages)` after `convertToLlm` each
85
+ * turn to keep the log in sync.
86
+ *
87
+ * Example:
88
+ * ```
89
+ * const mgr = new AppendOnlyContextManager();
90
+ * const ctx = mgr.build(context); // first call snapshots prefix
91
+ * mgr.syncMessages(normalized); // grow the log
92
+ * ctx = mgr.build(context); // subsequent calls use cache
93
+ * ```
94
+ */
95
+ export interface AppendOnlyContextManagerOptions {
96
+ /**
97
+ * Invoked whenever the stable prefix fingerprint changes on `build()` (a
98
+ * provider prompt-cache prefix reset). Used for per-session diagnostics; must
99
+ * not throw. `from` is `<unbuilt>` on the first build.
100
+ */
101
+ readonly onPrefixChange?: (info: {
102
+ from: string;
103
+ to: string;
104
+ version: number;
105
+ }) => void;
106
+ }
107
+ export declare class AppendOnlyContextManager {
108
+ #private;
109
+ readonly prefix: StablePrefix;
110
+ readonly log: AppendOnlyLog;
111
+ constructor(options?: AppendOnlyContextManagerOptions);
112
+ static forkFromSeed(args: {
113
+ prefixSnapshot?: StablePrefixSnapshot;
114
+ messages?: readonly Message[];
115
+ options: BuildOptions;
116
+ }): AppendOnlyContextManager;
117
+ build(context: AgentContext, options: BuildOptions): Context;
118
+ /**
119
+ * Sync normalized (provider-level) messages into the append-only log.
120
+ *
121
+ * Detects both compaction (shorter array) and in-place rewrites
122
+ * (same length, changed content via a rolling digest).
123
+ */
124
+ syncMessages(normalizedMessages: any[]): void;
125
+ seedNormalizedMessages(messages: readonly Message[], options?: {
126
+ reset?: boolean;
127
+ }): void;
128
+ /** Reset prefix + log for a model/provider switch while mode stays active. */
129
+ invalidateForModelChange(): void;
130
+ /** Reset the sync cursor AND clear the log. */
131
+ resetSyncCursor(): void;
132
+ appendMessage(message: any): void;
133
+ replaceTailMessage(message: any): void;
134
+ invalidate(): void;
135
+ reset(context: AgentContext, options: BuildOptions): void;
136
+ }
137
+ export declare function cloneJson<T>(value: T): T;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Branch summarization for tree navigation.
3
+ *
4
+ * When navigating to a different point in the session tree, this generates
5
+ * a summary of the branch being left so context isn't lost.
6
+ */
7
+ import type { Model, ProviderSessionState } from "@sayknow-cli/ai";
8
+ import { type AgentTelemetry } from "../telemetry";
9
+ import type { AgentMessage } from "../types";
10
+ import type { ReadonlySessionManager, SessionEntry } from "./entries";
11
+ import { type ConvertToLlm } from "./messages";
12
+ import { type FileOperations } from "./utils";
13
+ export interface BranchSummaryResult {
14
+ summary?: string;
15
+ readFiles?: string[];
16
+ modifiedFiles?: string[];
17
+ aborted?: boolean;
18
+ error?: string;
19
+ }
20
+ /** Details stored in BranchSummaryEntry.details for file tracking */
21
+ export interface BranchSummaryDetails {
22
+ readFiles: string[];
23
+ modifiedFiles: string[];
24
+ }
25
+ export type { FileOperations } from "./utils";
26
+ export interface BranchPreparation {
27
+ /** Messages extracted for summarization, in chronological order */
28
+ messages: AgentMessage[];
29
+ /** File operations extracted from tool calls */
30
+ fileOps: FileOperations;
31
+ /** Total estimated tokens in messages */
32
+ totalTokens: number;
33
+ }
34
+ export interface CollectEntriesResult {
35
+ /** Entries to summarize, in chronological order */
36
+ entries: SessionEntry[];
37
+ /** Common ancestor between old and new position, if any */
38
+ commonAncestorId: string | null;
39
+ }
40
+ export interface GenerateBranchSummaryOptions {
41
+ /** Model to use for summarization */
42
+ model: Model;
43
+ /** API key for the model */
44
+ apiKey: string;
45
+ /** Abort signal for cancellation */
46
+ signal: AbortSignal;
47
+ /** Optional custom instructions for summarization */
48
+ customInstructions?: string;
49
+ /** Tokens reserved for prompt + LLM response (default 16384) */
50
+ reserveTokens?: number;
51
+ /** Optional metadata forwarded to the underlying API request (e.g. user_id for session attribution). */
52
+ metadata?: Record<string, unknown>;
53
+ /** Convert app-specific messages before serializing the branch summary prompt. */
54
+ convertToLlm?: ConvertToLlm;
55
+ /**
56
+ * Optional telemetry handle. When provided, the branch summary LLM call is
57
+ * wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "branch_summary"`.
58
+ */
59
+ telemetry?: AgentTelemetry;
60
+ /**
61
+ * Provider session affinity id forwarded to the branch summary LLM call so it
62
+ * reuses the live turn's provider/WebSocket session.
63
+ */
64
+ sessionId?: string;
65
+ /** Shared provider state map so the branch summary call reuses session-scoped transport/session caches. */
66
+ providerSessionState?: Map<string, ProviderSessionState>;
67
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
68
+ preferWebsockets?: boolean;
69
+ }
70
+ /**
71
+ * Collect entries that should be summarized when navigating from one position to another.
72
+ *
73
+ * Walks from oldLeafId back to the common ancestor with targetId, collecting entries
74
+ * along the way. Does NOT stop at compaction boundaries - those are included and their
75
+ * summaries become context.
76
+ *
77
+ * @param session - Session manager (read-only access)
78
+ * @param oldLeafId - Current position (where we're navigating from)
79
+ * @param targetId - Target position (where we're navigating to)
80
+ * @returns Entries to summarize and the common ancestor
81
+ */
82
+ export declare function collectEntriesForBranchSummary(session: ReadonlySessionManager, oldLeafId: string | null, targetId: string): CollectEntriesResult;
83
+ /**
84
+ * Prepare entries for summarization with token budget.
85
+ *
86
+ * Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget.
87
+ * This ensures we keep the most recent context when the branch is too long.
88
+ *
89
+ * Also collects file operations from:
90
+ * - Tool calls in assistant messages
91
+ * - Existing branch_summary entries' details (for cumulative tracking)
92
+ *
93
+ * @param entries - Entries in chronological order
94
+ * @param tokenBudget - Maximum tokens to include (0 = no limit)
95
+ */
96
+ export declare function prepareBranchEntries(entries: SessionEntry[], tokenBudget?: number): BranchPreparation;
97
+ /**
98
+ * Generate a summary of abandoned branch entries.
99
+ *
100
+ * @param entries - Session entries to summarize (chronological order)
101
+ * @param options - Generation options
102
+ */
103
+ export declare function generateBranchSummary(entries: SessionEntry[], options: GenerateBranchSummaryOptions): Promise<BranchSummaryResult>;
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Context compaction for long sessions.
3
+ *
4
+ * Pure functions for compaction logic. The session manager handles I/O,
5
+ * and after compaction the session is reloaded.
6
+ */
7
+ import { type MessageAttribution, type Model, type ProviderSessionState, type Usage } from "@sayknow-cli/ai";
8
+ import { type AgentTelemetry } from "../telemetry";
9
+ import type { AgentMessage, AgentTool } from "../types";
10
+ import type { SessionEntry } from "./entries";
11
+ import { type ConvertToLlm } from "./messages";
12
+ import { type FileOperations } from "./utils";
13
+ /** Details stored in CompactionEntry.details for file tracking */
14
+ export interface CompactionDetails {
15
+ readFiles: string[];
16
+ modifiedFiles: string[];
17
+ }
18
+ /** Result from compact() - SessionManager adds uuid/parentUuid when saving */
19
+ export interface CompactionResult<T = unknown> {
20
+ summary: string;
21
+ /** Short PR-style summary for display purposes. */
22
+ shortSummary?: string;
23
+ firstKeptEntryId: string;
24
+ tokensBefore: number;
25
+ /** Hook-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
26
+ details?: T;
27
+ /** Hook-provided data to persist alongside compaction entry. */
28
+ preserveData?: Record<string, unknown>;
29
+ }
30
+ export interface CompactionSettings {
31
+ enabled: boolean;
32
+ strategy?: "context-full" | "handoff" | "off";
33
+ thresholdPercent?: number;
34
+ thresholdTokens?: number;
35
+ reserveTokens: number;
36
+ keepRecentTokens: number;
37
+ autoContinue?: boolean;
38
+ remoteEnabled?: boolean;
39
+ remoteEndpoint?: string;
40
+ }
41
+ export type RemoteCompactionFallbackHealthEvent = {
42
+ kind: "success";
43
+ model: string;
44
+ provider: string;
45
+ } | {
46
+ kind: "fallback";
47
+ model: string;
48
+ provider: string;
49
+ error: string;
50
+ };
51
+ export interface RemoteCompactionFallbackHealthHooks {
52
+ recordRemoteCompactionFallback(event: RemoteCompactionFallbackHealthEvent): void;
53
+ }
54
+ export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
55
+ /**
56
+ * Calculate total context tokens from usage.
57
+ * Uses the native totalTokens field when available, falls back to computing from components.
58
+ */
59
+ export declare function calculateContextTokens(usage: Usage): number;
60
+ export declare function calculatePromptTokens(usage: Usage): number;
61
+ /**
62
+ * Find the last non-aborted assistant message usage from session entries.
63
+ */
64
+ export declare function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined;
65
+ /**
66
+ * Effective reserve: the largest of 15% of the context window, the configured floor,
67
+ * and the model's reserved completion budget (`maxOutputTokens`).
68
+ *
69
+ * Reserving `maxOutputTokens` keeps the safe input/prompt-packing budget below the
70
+ * *total* context window for models whose completion reservation exceeds the 15%
71
+ * floor (e.g. a 400K-context model with 128K max output reserves 128K, not 60K, so
72
+ * input is capped near 272K instead of 340K).
73
+ */
74
+ export declare function effectiveReserveTokens(contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number): number;
75
+ /**
76
+ * Check if compaction should trigger based on context usage.
77
+ *
78
+ * `maxOutputTokens` is the model's reserved completion budget; it is excluded from
79
+ * the safe input budget so prompt + reserved output cannot exceed the total window.
80
+ */
81
+ export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number): boolean;
82
+ /** Reason a compaction was triggered. `token` is the normal user-configurable path; the rest are emergency floors. */
83
+ export type CompactionTriggerReason = "token" | "heap" | "retainedMemory" | "providerBytes" | "messageCount" | "imageBytes";
84
+ /** A point-in-time resource sample. Supplied by an injectable sampler so tests never read real RSS. */
85
+ export interface EmergencyCompactionSample {
86
+ /** Resident heap bytes (e.g. process.memoryUsage().heapUsed). */
87
+ heapUsedBytes: number;
88
+ /** Approximate serialized provider-context bytes. */
89
+ providerBytes: number;
90
+ /** Provider-visible message count. */
91
+ messageCount: number;
92
+ /** Approximate inline image bytes in the provider context. */
93
+ imageBytes: number;
94
+ /** Bytes retained by session resident image sentinels; separate from provider-visible bytes. */
95
+ sessionResidentImageBytes?: number;
96
+ /** Bytes retained by non-provider materialized/session-local caches. */
97
+ materializedResidentBytes?: number;
98
+ /** Number of live TUI chat-container children. */
99
+ tuiChatChildren?: number;
100
+ /** Bytes retained by TUI render caches. */
101
+ tuiCachedRenderBytes?: number;
102
+ }
103
+ export interface EmergencyCompactionLimits {
104
+ heapUsedBytes: number;
105
+ providerBytes: number;
106
+ messageCount: number;
107
+ imageBytes: number;
108
+ retainedMemoryBytes?: number;
109
+ retainedMemoryDiagnosticBytes?: number;
110
+ tuiChatChildren?: number;
111
+ tuiChatChildrenDiagnostic?: number;
112
+ }
113
+ export declare function resetEmergencyRetainedMemoryDiagnosticsForTests(): void;
114
+ export declare function resolveEmergencyCompactionLimits(totalMemoryBytes?: number): EmergencyCompactionLimits;
115
+ /**
116
+ * Non-disableable emergency floors. These sit well above normal usage and exist so a
117
+ * long session on weak hardware compacts before OOM even when token-based compaction is
118
+ * disabled or its threshold is set too high. They are NOT user-tunable down to zero.
119
+ */
120
+ export declare const DEFAULT_EMERGENCY_COMPACTION_LIMITS: EmergencyCompactionLimits;
121
+ /**
122
+ * Returns the first emergency limit exceeded (heap > retainedMemory > providerBytes > imageBytes > messageCount),
123
+ * or null when none is. Pure apart from retained-memory diagnostics; the caller routes the result through the
124
+ * normal pair-safe `compact()` cut logic so a tool_use/tool_result pair is never split.
125
+ */
126
+ export declare function emergencyCompactionReason(sample: EmergencyCompactionSample, limits?: EmergencyCompactionLimits): CompactionTriggerReason | null;
127
+ export declare function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number): number;
128
+ /**
129
+ * Image content has no tokenizer representation; charge a fixed estimate
130
+ * matching what providers typically bill for inline images.
131
+ */
132
+ export declare const IMAGE_TOKEN_ESTIMATE = 1200;
133
+ /**
134
+ * Native-free token estimate for a message. This is the only message
135
+ * token estimator: provider usage (see {@link calculatePromptTokens}) anchors
136
+ * the already-sent context, and this covers unsent/trailing deltas, per-entry
137
+ * budgeting, and display surfaces. Callers add a conservative inflation factor
138
+ * where compaction-threshold safety requires it.
139
+ */
140
+ export declare function estimateMessageTokensHeuristic(message: AgentMessage): number;
141
+ /**
142
+ * Script-aware native-free token estimate for plain string fragments.
143
+ * Fragment-level counterpart of {@link estimateMessageTokensHeuristic}.
144
+ */
145
+ export declare function estimateTextTokensHeuristic(fragments: string | readonly string[]): number;
146
+ export declare function estimateEntryTokens(entry: SessionEntry): number;
147
+ export declare function estimateEntriesTokens(entries: SessionEntry[], startIndex: number, endIndex: number): number;
148
+ /**
149
+ * Find the user message (or bashExecution) that starts the turn containing the given entry index.
150
+ * Returns -1 if no turn start found before the index.
151
+ * BashExecutionMessage is treated like a user message for turn boundaries.
152
+ */
153
+ export declare function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number;
154
+ export interface CutPointResult {
155
+ /** Index of first entry to keep */
156
+ firstKeptEntryIndex: number;
157
+ /** Index of user message that starts the turn being split, or -1 if not splitting */
158
+ turnStartIndex: number;
159
+ /** Whether this cut splits a turn (cut point is not a user message) */
160
+ isSplitTurn: boolean;
161
+ }
162
+ /**
163
+ * Find the cut point in session entries that keeps approximately `keepRecentTokens`.
164
+ *
165
+ * Algorithm: Walk backwards from newest, accumulating estimated message sizes.
166
+ * Stop when we've accumulated >= keepRecentTokens. Cut at that point.
167
+ *
168
+ * Can cut at user OR assistant messages (never tool results). When cutting at an
169
+ * assistant message with tool calls, its tool results come after and will be kept.
170
+ *
171
+ * Returns CutPointResult with:
172
+ * - firstKeptEntryIndex: the entry index to start keeping from
173
+ * - turnStartIndex: if cutting mid-turn, the user message that started that turn
174
+ * - isSplitTurn: whether we're cutting in the middle of a turn
175
+ *
176
+ * Only considers entries between `startIndex` and `endIndex` (exclusive).
177
+ */
178
+ export declare function findCutPoint(entries: SessionEntry[], startIndex: number, endIndex: number, keepRecentTokens: number): CutPointResult;
179
+ export declare const AUTO_HANDOFF_THRESHOLD_FOCUS: string;
180
+ /**
181
+ * Generate a summary of the conversation using the LLM.
182
+ * If previousSummary is provided, uses the update prompt to merge.
183
+ */
184
+ export interface SummaryOptions {
185
+ promptOverride?: string;
186
+ extraContext?: string[];
187
+ remoteEndpoint?: string;
188
+ remoteInstructions?: string;
189
+ initiatorOverride?: MessageAttribution;
190
+ metadata?: Record<string, unknown>;
191
+ convertToLlm?: ConvertToLlm;
192
+ /**
193
+ * Optional telemetry handle. When provided, every LLM call emitted during
194
+ * compaction is wrapped in an OTEL chat span tagged with
195
+ * `pi.gen_ai.oneshot.kind` (`compaction_summary` or `compaction_turn_prefix`).
196
+ */
197
+ telemetry?: AgentTelemetry;
198
+ authCredentialType?: "api_key" | "oauth";
199
+ /**
200
+ * Provider session affinity id forwarded to the maintenance LLM call so it
201
+ * reuses the live turn's provider/WebSocket session (matches the
202
+ * `providerSessionId ?? sessionId` the agent loop sends for normal turns).
203
+ */
204
+ sessionId?: string;
205
+ /** Shared provider state map so maintenance calls reuse session-scoped transport/session caches. */
206
+ providerSessionState?: Map<string, ProviderSessionState>;
207
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
208
+ preferWebsockets?: boolean;
209
+ /** Session-owned health sink for remote-compaction fallback transition logging. */
210
+ remoteCompactionFallbackHealth?: RemoteCompactionFallbackHealthHooks;
211
+ }
212
+ /**
213
+ * Cap the serialized conversation fed to a summarization request so the request
214
+ * itself fits inside the model's context window.
215
+ *
216
+ * Without this, summarizing a near-full context serializes (nearly) the entire
217
+ * history back into a single summary request; on strict backends (e.g.
218
+ * OpenAI-code/Codex `context_length_exceeded`) that request itself overflows and
219
+ * throws, so context-overflow recovery cannot produce a summary and the agent
220
+ * fails to compact-and-continue — a non-interactive `skc -p` run then terminates
221
+ * on the very overflow the recovery was meant to absorb.
222
+ *
223
+ * The budget reserves the summary's own output tokens plus prompt/system/template
224
+ * overhead, and applies a conservative safety factor for estimator error on
225
+ * dense text (the reason the original overflow was missed).
226
+ * Truncation keeps the head (origin/goals) and the tail (most recent state) and
227
+ * elides the middle; it is a last resort that only triggers when the input would
228
+ * otherwise not fit.
229
+ */
230
+ export declare function boundConversationTextForSummary(conversationText: string, model: Model, outputMaxTokens: number): string;
231
+ export declare function generateSummary(currentMessages: AgentMessage[], model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, options?: SummaryOptions): Promise<string>;
232
+ export interface HandoffOptions {
233
+ /** Live agent system prompt — passed verbatim so providers hit the cached prefix. */
234
+ systemPrompt: string[];
235
+ /** Live agent tool list — same purpose. Forced to `toolChoice: "none"`. */
236
+ tools?: AgentTool<any>[];
237
+ customInstructions?: string;
238
+ /**
239
+ * Optional user-configured extension appended to the base handoff prompt.
240
+ * It SUPPLEMENTS the immutable base (safety/continuity structure); it never
241
+ * replaces `HANDOFF_DOCUMENT_PROMPT`.
242
+ */
243
+ promptExtension?: string;
244
+ convertToLlm?: ConvertToLlm;
245
+ initiatorOverride?: MessageAttribution;
246
+ metadata?: Record<string, unknown>;
247
+ /**
248
+ * Optional telemetry handle. When provided, the handoff LLM call is
249
+ * wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "handoff"`.
250
+ */
251
+ telemetry?: AgentTelemetry;
252
+ authCredentialType?: "api_key" | "oauth";
253
+ /**
254
+ * Provider session affinity id forwarded to the handoff LLM call so it
255
+ * reuses the live turn's provider/WebSocket session.
256
+ */
257
+ sessionId?: string;
258
+ /** Shared provider state map so the handoff call reuses session-scoped transport/session caches. */
259
+ providerSessionState?: Map<string, ProviderSessionState>;
260
+ /** Hint that websocket transport should be preferred when supported by the provider implementation. */
261
+ preferWebsockets?: boolean;
262
+ }
263
+ export declare function renderHandoffPrompt(customInstructions?: string, promptExtension?: string): string;
264
+ export declare function generateHandoff(messages: AgentMessage[], model: Model, apiKey: string, options: HandoffOptions, signal?: AbortSignal): Promise<string>;
265
+ export interface CompactionPreparation {
266
+ /** UUID of first entry to keep */
267
+ firstKeptEntryId: string;
268
+ /** Messages that will be summarized and discarded */
269
+ messagesToSummarize: AgentMessage[];
270
+ /** Messages that will be turned into turn prefix summary (if splitting) */
271
+ turnPrefixMessages: AgentMessage[];
272
+ /** Messages kept in full after compaction (recent history) */
273
+ recentMessages: AgentMessage[];
274
+ /** Whether this is a split turn (cut point in middle of turn) */
275
+ isSplitTurn: boolean;
276
+ tokensBefore: number;
277
+ /** Summary from previous compaction, for iterative update */
278
+ previousSummary?: string;
279
+ /** Preserved opaque compaction payload from the previous compaction, if any. */
280
+ previousPreserveData?: Record<string, unknown>;
281
+ /** File operations extracted from messagesToSummarize */
282
+ fileOps: FileOperations;
283
+ /** Compaction settions from settings.jsonl */
284
+ settings: CompactionSettings;
285
+ /**
286
+ * Diagnostics for the keep-window token correction (Finding 7). `ratio` is the
287
+ * clamped heuristic→actual correction that was applied (1 when none supplied);
288
+ * `keepRecentTokensCorrected` is the heuristic budget findCutPoint actually used.
289
+ */
290
+ tokenCorrection: {
291
+ ratio: number;
292
+ keepRecentTokensCorrected: number;
293
+ };
294
+ }
295
+ /** Bounds for the keep-window token correction (Finding 7): never trust a ratio
296
+ * beyond 2x in either direction so a bad estimate cannot balloon or collapse the
297
+ * kept window. */
298
+ export declare const TOKEN_CORRECTION_MIN_RATIO = 0.5;
299
+ export declare const TOKEN_CORRECTION_MAX_RATIO = 2;
300
+ export interface PrepareCompactionOptions {
301
+ /**
302
+ * Observed heuristic→actual token correction for the post-boundary keep window
303
+ * (actualTokens / chars-4-heuristicTokens), supplied by the caller from per-turn
304
+ * Usage deltas or a stable-prefix-subtracted comparison. Clamped to
305
+ * [0.5, 2] and applied bidirectionally. When omitted, no correction is applied
306
+ * (the confounded raw promptTokens/estimatedTokens quotient is never used).
307
+ */
308
+ tokenCorrectionRatio?: number;
309
+ /**
310
+ * Model context-window size. Windows below 66k retain the legacy fixed
311
+ * keepRecentTokens behavior; larger windows scale the keep window to 30%.
312
+ */
313
+ contextWindow?: number;
314
+ }
315
+ export declare function prepareCompaction(pathEntries: SessionEntry[], settings: CompactionSettings, options?: PrepareCompactionOptions): CompactionPreparation | undefined;
316
+ /**
317
+ * Generate summaries for compaction using prepared data.
318
+ * Returns CompactionResult - SessionManager adds id/parentId when saving.
319
+ *
320
+ * @param preparation - Pre-calculated preparation from prepareCompaction()
321
+ * @param customInstructions - Optional custom focus for the summary
322
+ */
323
+ export declare function compact(preparation: CompactionPreparation, model: Model, apiKey: string, customInstructions?: string, signal?: AbortSignal, options?: SummaryOptions): Promise<CompactionResult>;
@@ -0,0 +1,124 @@
1
+ import type { ImageContent, MessageAttribution, ServiceTier, TextContent } from "@sayknow-cli/ai";
2
+ import type { AgentMessage } from "../types";
3
+ export interface SessionEntryBase {
4
+ type: string;
5
+ id: string;
6
+ parentId: string | null;
7
+ timestamp: string;
8
+ }
9
+ export interface SessionMessageEntry extends SessionEntryBase {
10
+ type: "message";
11
+ message: AgentMessage;
12
+ }
13
+ export interface ThinkingLevelChangeEntry extends SessionEntryBase {
14
+ type: "thinking_level_change";
15
+ thinkingLevel?: string | null;
16
+ }
17
+ export interface ModelChangeEntry extends SessionEntryBase {
18
+ type: "model_change";
19
+ /** Model in "provider/modelId" format */
20
+ model: string;
21
+ /** Role: "default", "smol", "slow", etc. Undefined treated as "default" */
22
+ role?: string;
23
+ /** Requested model before a runtime substitution/fallback, in "provider/modelId" format. */
24
+ previousModel?: string;
25
+ /** Machine-readable reason for runtime model substitution/fallback. */
26
+ reason?: string;
27
+ /** Effective thinking level when the change was recorded. */
28
+ thinkingLevel?: string | null;
29
+ }
30
+ export interface ServiceTierChangeEntry extends SessionEntryBase {
31
+ type: "service_tier_change";
32
+ serviceTier: ServiceTier | null;
33
+ }
34
+ export interface CompactionEntry<T = unknown> extends SessionEntryBase {
35
+ type: "compaction";
36
+ summary: string;
37
+ shortSummary?: string;
38
+ firstKeptEntryId: string;
39
+ tokensBefore: number;
40
+ /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
41
+ details?: T;
42
+ /** Hook-provided data to persist across compaction */
43
+ preserveData?: Record<string, unknown>;
44
+ /** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
45
+ fromExtension?: boolean;
46
+ }
47
+ export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
48
+ type: "branch_summary";
49
+ fromId: string;
50
+ summary: string;
51
+ /** Extension-specific data (not sent to LLM) */
52
+ details?: T;
53
+ /** True if generated by an extension, false if pi-generated */
54
+ fromExtension?: boolean;
55
+ }
56
+ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
57
+ type: "custom_message";
58
+ customType: string;
59
+ content: string | (TextContent | ImageContent)[];
60
+ details?: T;
61
+ display: boolean;
62
+ /** Who initiated this message for billing/attribution semantics. */
63
+ attribution?: MessageAttribution;
64
+ }
65
+ export interface CustomEntry<T = unknown> extends SessionEntryBase {
66
+ type: "custom";
67
+ customType: string;
68
+ data?: T;
69
+ }
70
+ export interface LabelEntry extends SessionEntryBase {
71
+ type: "label";
72
+ targetId: string;
73
+ label: string | undefined;
74
+ }
75
+ export interface TtsrInjectionEntry extends SessionEntryBase {
76
+ type: "ttsr_injection";
77
+ /** Names of rules that were injected */
78
+ injectedRules: string[];
79
+ }
80
+ export interface MCPToolSelectionEntry extends SessionEntryBase {
81
+ type: "mcp_tool_selection";
82
+ /** MCP tool names selected for visibility in discovery mode. */
83
+ selectedToolNames: string[];
84
+ }
85
+ export interface DiscoveredBuiltinToolSelectionEntry extends SessionEntryBase {
86
+ type: "discovered_builtin_tool_selection";
87
+ /** Discoverable built-in tool names selected for visibility in discovery mode. */
88
+ selectedToolNames: string[];
89
+ }
90
+ export interface SessionInitEntry extends SessionEntryBase {
91
+ type: "session_init";
92
+ /** Full system prompt sent to the model */
93
+ systemPrompt: string;
94
+ /** Initial task/user message */
95
+ task: string;
96
+ /** Tools available to the agent */
97
+ tools: string[];
98
+ /** Output schema if structured output was requested */
99
+ outputSchema?: unknown;
100
+ }
101
+ export interface ModeChangeEntry extends SessionEntryBase {
102
+ type: "mode_change";
103
+ /** Current mode name, or "none" when exiting a mode */
104
+ mode: string;
105
+ /** Optional mode-specific data (e.g. plan file path) */
106
+ data?: Record<string, unknown>;
107
+ }
108
+ export interface ConfiguredModelChainEntry extends SessionEntryBase {
109
+ type: "configured_model_chain";
110
+ role: string;
111
+ entries: readonly string[];
112
+ origin: string;
113
+ identity?: string;
114
+ explicitHead: boolean;
115
+ /** Whether this entry removes the configured chain for its role. */
116
+ cleared?: boolean;
117
+ }
118
+ export interface CustomCompactionSessionEntries {
119
+ }
120
+ export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | ServiceTierChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | TtsrInjectionEntry | MCPToolSelectionEntry | DiscoveredBuiltinToolSelectionEntry | SessionInitEntry | ModeChangeEntry | ConfiguredModelChainEntry | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
121
+ export interface ReadonlySessionManager {
122
+ getBranch(leafId?: string | null): SessionEntry[];
123
+ getEntry(id: string): SessionEntry | undefined;
124
+ }