@caupulican/pi-agent-core 0.80.103 → 0.81.0
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.
- package/dist/compaction/branch-summarization.d.ts +88 -0
- package/dist/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/{harness/compaction → compaction}/branch-summarization.js +86 -18
- package/dist/compaction/branch-summarization.js.map +1 -0
- package/dist/compaction/compaction.d.ts +143 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/{harness/compaction → compaction}/compaction.js +208 -77
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/index.d.ts +7 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +7 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/utils.d.ts +38 -0
- package/dist/compaction/utils.d.ts.map +1 -0
- package/dist/{harness/compaction → compaction}/utils.js +36 -14
- package/dist/compaction/utils.js.map +1 -0
- package/dist/index.d.ts +5 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -16
- package/dist/index.js.map +1 -1
- package/dist/{harness/messages.d.ts → messages.d.ts} +28 -2
- package/dist/messages.d.ts.map +1 -0
- package/dist/{harness/messages.js → messages.js} +22 -1
- package/dist/messages.js.map +1 -0
- package/dist/node.d.ts +5 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -1
- package/dist/node.js.map +1 -1
- package/dist/reliability/classifier.d.ts +33 -0
- package/dist/reliability/classifier.d.ts.map +1 -0
- package/dist/reliability/classifier.js +69 -0
- package/dist/reliability/classifier.js.map +1 -0
- package/dist/reliability/index.d.ts +5 -0
- package/dist/reliability/index.d.ts.map +1 -0
- package/dist/reliability/index.js +5 -0
- package/dist/reliability/index.js.map +1 -0
- package/dist/reliability/node.d.ts +2 -0
- package/dist/reliability/node.d.ts.map +1 -0
- package/dist/reliability/node.js +2 -0
- package/dist/reliability/node.js.map +1 -0
- package/dist/reliability/process-tree.d.ts +10 -0
- package/dist/reliability/process-tree.d.ts.map +1 -0
- package/dist/reliability/process-tree.js +75 -0
- package/dist/reliability/process-tree.js.map +1 -0
- package/dist/reliability/retry-controller.d.ts +66 -0
- package/dist/reliability/retry-controller.d.ts.map +1 -0
- package/dist/reliability/retry-controller.js +104 -0
- package/dist/reliability/retry-controller.js.map +1 -0
- package/dist/reliability/retry.d.ts +19 -0
- package/dist/reliability/retry.d.ts.map +1 -0
- package/dist/reliability/retry.js +35 -0
- package/dist/reliability/retry.js.map +1 -0
- package/dist/reliability/watchdogs.d.ts +47 -0
- package/dist/reliability/watchdogs.d.ts.map +1 -0
- package/dist/reliability/watchdogs.js +159 -0
- package/dist/reliability/watchdogs.js.map +1 -0
- package/dist/session/message-retention.d.ts +26 -0
- package/dist/session/message-retention.d.ts.map +1 -0
- package/dist/session/message-retention.js +95 -0
- package/dist/session/message-retention.js.map +1 -0
- package/dist/session/session-manager.d.ts +350 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +1339 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/utils/paths.d.ts +15 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +31 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/shell-output.d.ts +10 -0
- package/dist/utils/shell-output.d.ts.map +1 -0
- package/dist/utils/shell-output.js +38 -0
- package/dist/utils/shell-output.js.map +1 -0
- package/dist/utils/truncate.d.ts.map +1 -0
- package/dist/{harness/utils → utils}/truncate.js +27 -102
- package/dist/utils/truncate.js.map +1 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +6 -2
- package/dist/harness/agent-harness.d.ts +0 -95
- package/dist/harness/agent-harness.d.ts.map +0 -1
- package/dist/harness/agent-harness.js +0 -963
- package/dist/harness/agent-harness.js.map +0 -1
- package/dist/harness/compaction/branch-summarization.d.ts +0 -53
- package/dist/harness/compaction/branch-summarization.d.ts.map +0 -1
- package/dist/harness/compaction/branch-summarization.js.map +0 -1
- package/dist/harness/compaction/compaction.d.ts +0 -95
- package/dist/harness/compaction/compaction.d.ts.map +0 -1
- package/dist/harness/compaction/compaction.js.map +0 -1
- package/dist/harness/compaction/utils.d.ts +0 -25
- package/dist/harness/compaction/utils.d.ts.map +0 -1
- package/dist/harness/compaction/utils.js.map +0 -1
- package/dist/harness/env/nodejs.d.ts +0 -51
- package/dist/harness/env/nodejs.d.ts.map +0 -1
- package/dist/harness/env/nodejs.js +0 -486
- package/dist/harness/env/nodejs.js.map +0 -1
- package/dist/harness/messages.d.ts.map +0 -1
- package/dist/harness/messages.js.map +0 -1
- package/dist/harness/prompt-templates.d.ts +0 -48
- package/dist/harness/prompt-templates.d.ts.map +0 -1
- package/dist/harness/prompt-templates.js +0 -233
- package/dist/harness/prompt-templates.js.map +0 -1
- package/dist/harness/session/jsonl-repo.d.ts +0 -28
- package/dist/harness/session/jsonl-repo.d.ts.map +0 -1
- package/dist/harness/session/jsonl-repo.js +0 -112
- package/dist/harness/session/jsonl-repo.js.map +0 -1
- package/dist/harness/session/jsonl-storage.d.ts +0 -33
- package/dist/harness/session/jsonl-storage.d.ts.map +0 -1
- package/dist/harness/session/jsonl-storage.js +0 -231
- package/dist/harness/session/jsonl-storage.js.map +0 -1
- package/dist/harness/session/memory-repo.d.ts +0 -18
- package/dist/harness/session/memory-repo.d.ts.map +0 -1
- package/dist/harness/session/memory-repo.js +0 -42
- package/dist/harness/session/memory-repo.js.map +0 -1
- package/dist/harness/session/memory-storage.d.ts +0 -25
- package/dist/harness/session/memory-storage.d.ts.map +0 -1
- package/dist/harness/session/memory-storage.js +0 -114
- package/dist/harness/session/memory-storage.js.map +0 -1
- package/dist/harness/session/repo-utils.d.ts +0 -11
- package/dist/harness/session/repo-utils.d.ts.map +0 -1
- package/dist/harness/session/repo-utils.js +0 -39
- package/dist/harness/session/repo-utils.js.map +0 -1
- package/dist/harness/session/session.d.ts +0 -33
- package/dist/harness/session/session.d.ts.map +0 -1
- package/dist/harness/session/session.js +0 -210
- package/dist/harness/session/session.js.map +0 -1
- package/dist/harness/session/uuid.d.ts.map +0 -1
- package/dist/harness/session/uuid.js.map +0 -1
- package/dist/harness/skills.d.ts +0 -44
- package/dist/harness/skills.d.ts.map +0 -1
- package/dist/harness/skills.js +0 -311
- package/dist/harness/skills.js.map +0 -1
- package/dist/harness/system-prompt.d.ts +0 -3
- package/dist/harness/system-prompt.d.ts.map +0 -1
- package/dist/harness/system-prompt.js +0 -30
- package/dist/harness/system-prompt.js.map +0 -1
- package/dist/harness/types.d.ts +0 -615
- package/dist/harness/types.d.ts.map +0 -1
- package/dist/harness/types.js +0 -94
- package/dist/harness/types.js.map +0 -1
- package/dist/harness/utils/shell-output.d.ts +0 -14
- package/dist/harness/utils/shell-output.d.ts.map +0 -1
- package/dist/harness/utils/shell-output.js +0 -126
- package/dist/harness/utils/shell-output.js.map +0 -1
- package/dist/harness/utils/truncate.d.ts.map +0 -1
- package/dist/harness/utils/truncate.js.map +0 -1
- /package/dist/{harness/utils → utils}/truncate.d.ts +0 -0
- /package/dist/{harness/session/uuid.d.ts → uuid.d.ts} +0 -0
- /package/dist/{harness/session/uuid.js → uuid.js} +0 -0
|
@@ -1,21 +1,23 @@
|
|
|
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
|
+
*/
|
|
1
7
|
import { completeSimple } from "@caupulican/pi-ai";
|
|
2
8
|
import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
3
|
-
import { buildSessionContext } from "../session/session.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return JSON.stringify(value) ?? "undefined";
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return "[unserializable]";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
9
|
+
import { buildSessionContext } from "../session/session-manager.js";
|
|
10
|
+
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
|
11
|
+
/**
|
|
12
|
+
* Extract file operations from messages and previous compaction entries.
|
|
13
|
+
*/
|
|
14
14
|
function extractFileOperations(messages, entries, prevCompactionIndex) {
|
|
15
15
|
const fileOps = createFileOps();
|
|
16
|
+
// Collect from previous compaction's details (if pi-generated)
|
|
16
17
|
if (prevCompactionIndex >= 0) {
|
|
17
18
|
const prevCompaction = entries[prevCompactionIndex];
|
|
18
19
|
if (!prevCompaction.fromHook && prevCompaction.details) {
|
|
20
|
+
// fromHook field kept for session file compatibility
|
|
19
21
|
const details = prevCompaction.details;
|
|
20
22
|
if (Array.isArray(details.readFiles)) {
|
|
21
23
|
for (const f of details.readFiles)
|
|
@@ -27,11 +29,19 @@ function extractFileOperations(messages, entries, prevCompactionIndex) {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
}
|
|
32
|
+
// Extract from tool calls in messages
|
|
30
33
|
for (const msg of messages) {
|
|
31
34
|
extractFileOpsFromMessage(msg, fileOps);
|
|
32
35
|
}
|
|
33
36
|
return fileOps;
|
|
34
37
|
}
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Message Extraction
|
|
40
|
+
// ============================================================================
|
|
41
|
+
/**
|
|
42
|
+
* Extract AgentMessage from an entry if it produces one.
|
|
43
|
+
* Returns undefined for entries that don't contribute to LLM context.
|
|
44
|
+
*/
|
|
35
45
|
function getMessageFromEntry(entry) {
|
|
36
46
|
if (entry.type === "message") {
|
|
37
47
|
return entry.message;
|
|
@@ -53,16 +63,26 @@ function getMessageFromEntryForCompaction(entry) {
|
|
|
53
63
|
}
|
|
54
64
|
return getMessageFromEntry(entry);
|
|
55
65
|
}
|
|
56
|
-
/** Default compaction settings used by the harness. */
|
|
57
66
|
export const DEFAULT_COMPACTION_SETTINGS = {
|
|
58
67
|
enabled: true,
|
|
59
68
|
reserveTokens: 16384,
|
|
60
69
|
keepRecentTokens: 20000,
|
|
70
|
+
triggerPercent: 0.7,
|
|
61
71
|
};
|
|
62
|
-
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Token calculation
|
|
74
|
+
// ============================================================================
|
|
75
|
+
/**
|
|
76
|
+
* Calculate total context tokens from usage.
|
|
77
|
+
* Uses the native totalTokens field when available, falls back to computing from components.
|
|
78
|
+
*/
|
|
63
79
|
export function calculateContextTokens(usage) {
|
|
64
80
|
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
65
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Get usage from an assistant message if available.
|
|
84
|
+
* Skips aborted and error messages as they don't have valid usage data.
|
|
85
|
+
*/
|
|
66
86
|
function getAssistantUsage(msg) {
|
|
67
87
|
if (msg.role === "assistant" && "usage" in msg) {
|
|
68
88
|
const assistantMsg = msg;
|
|
@@ -72,7 +92,9 @@ function getAssistantUsage(msg) {
|
|
|
72
92
|
}
|
|
73
93
|
return undefined;
|
|
74
94
|
}
|
|
75
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Find the last non-aborted assistant message usage from session entries.
|
|
97
|
+
*/
|
|
76
98
|
export function getLastAssistantUsage(entries) {
|
|
77
99
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
78
100
|
const entry = entries[i];
|
|
@@ -92,7 +114,10 @@ function getLastAssistantUsageInfo(messages) {
|
|
|
92
114
|
}
|
|
93
115
|
return undefined;
|
|
94
116
|
}
|
|
95
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Estimate context tokens from messages, using the last assistant usage when available.
|
|
119
|
+
* If there are messages after the last usage, estimate their tokens with estimateTokens.
|
|
120
|
+
*/
|
|
96
121
|
export function estimateContextTokens(messages) {
|
|
97
122
|
const usageInfo = getLastAssistantUsageInfo(messages);
|
|
98
123
|
if (!usageInfo) {
|
|
@@ -119,12 +144,46 @@ export function estimateContextTokens(messages) {
|
|
|
119
144
|
lastUsageIndex: usageInfo.index,
|
|
120
145
|
};
|
|
121
146
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Minimum projected space saving for the EARLY (fractional) compaction trigger to fire. Anti-thrashing
|
|
149
|
+
* (cost guard, #30): an early compaction whose summary would barely shrink the context (mostly recent,
|
|
150
|
+
* protected content) just burns a summarization call for little gain — skip it and let the context grow
|
|
151
|
+
* until either the saving is worthwhile or the hard (near-full) trigger forces it. Does NOT gate the
|
|
152
|
+
* hard trigger, so overflow is always avoided.
|
|
153
|
+
*/
|
|
154
|
+
export const MIN_COMPACTION_SAVINGS = 0.12;
|
|
155
|
+
/**
|
|
156
|
+
* Check if compaction should trigger based on context usage.
|
|
157
|
+
*
|
|
158
|
+
* Two triggers:
|
|
159
|
+
* - HARD: context exceeds `contextWindow - reserveTokens` (near-full) or an explicit `triggerTokens`
|
|
160
|
+
* override — always compact (prevents overflow).
|
|
161
|
+
* - EARLY (fractional, cost guard): context exceeds `contextWindow * triggerPercent` — compact only if
|
|
162
|
+
* the summary would actually save enough (`MIN_COMPACTION_SAVINGS`), so we don't thrash for tiny gains.
|
|
163
|
+
*/
|
|
164
|
+
export function shouldCompact(contextTokens, contextWindow, settings, triggerTokens) {
|
|
124
165
|
if (!settings.enabled)
|
|
125
166
|
return false;
|
|
126
|
-
|
|
167
|
+
// Hard trigger: near-full, or a caller-supplied lower override. Always compacts (avoid overflow).
|
|
168
|
+
const reserveTrigger = contextWindow - settings.reserveTokens;
|
|
169
|
+
const hardTrigger = triggerTokens === undefined ? reserveTrigger : Math.min(reserveTrigger, triggerTokens);
|
|
170
|
+
if (contextTokens > hardTrigger)
|
|
171
|
+
return true;
|
|
172
|
+
// Early fractional trigger: bounds per-turn input cost on large-window models, gated by anti-thrashing.
|
|
173
|
+
const pct = settings.triggerPercent ?? 0;
|
|
174
|
+
if (pct > 0 && pct < 1) {
|
|
175
|
+
const fractionalTrigger = Math.floor(contextWindow * pct);
|
|
176
|
+
if (contextTokens > fractionalTrigger) {
|
|
177
|
+
// Projected saving ≈ the non-protected fraction (everything but the recent tail we keep).
|
|
178
|
+
const projectedSavings = contextTokens > 0 ? 1 - settings.keepRecentTokens / contextTokens : 0;
|
|
179
|
+
return projectedSavings >= MIN_COMPACTION_SAVINGS;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
127
183
|
}
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// Cut point detection
|
|
186
|
+
// ============================================================================
|
|
128
187
|
const ESTIMATED_IMAGE_CHARS = 4800;
|
|
129
188
|
function estimateTextAndImageContentChars(content) {
|
|
130
189
|
if (typeof content === "string") {
|
|
@@ -141,7 +200,10 @@ function estimateTextAndImageContentChars(content) {
|
|
|
141
200
|
}
|
|
142
201
|
return chars;
|
|
143
202
|
}
|
|
144
|
-
/**
|
|
203
|
+
/**
|
|
204
|
+
* Estimate token count for a message using chars/4 heuristic.
|
|
205
|
+
* This is conservative (overestimates tokens).
|
|
206
|
+
*/
|
|
145
207
|
export function estimateTokens(message) {
|
|
146
208
|
let chars = 0;
|
|
147
209
|
switch (message.role) {
|
|
@@ -159,7 +221,7 @@ export function estimateTokens(message) {
|
|
|
159
221
|
chars += block.thinking.length;
|
|
160
222
|
}
|
|
161
223
|
else if (block.type === "toolCall") {
|
|
162
|
-
chars += block.name.length +
|
|
224
|
+
chars += block.name.length + JSON.stringify(block.arguments).length;
|
|
163
225
|
}
|
|
164
226
|
}
|
|
165
227
|
return Math.ceil(chars / 4);
|
|
@@ -181,6 +243,13 @@ export function estimateTokens(message) {
|
|
|
181
243
|
}
|
|
182
244
|
return 0;
|
|
183
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Find valid cut points: indices of user, assistant, custom, or bashExecution messages.
|
|
248
|
+
* Never cut at tool results (they must follow their tool call).
|
|
249
|
+
* When we cut at an assistant message with tool calls, its tool results follow it
|
|
250
|
+
* and will be kept.
|
|
251
|
+
* BashExecutionMessage is treated like a user message (user-initiated context).
|
|
252
|
+
*/
|
|
184
253
|
function findValidCutPoints(entries, startIndex, endIndex) {
|
|
185
254
|
const cutPoints = [];
|
|
186
255
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -204,26 +273,30 @@ function findValidCutPoints(entries, startIndex, endIndex) {
|
|
|
204
273
|
}
|
|
205
274
|
case "thinking_level_change":
|
|
206
275
|
case "model_change":
|
|
207
|
-
case "active_tools_change":
|
|
208
276
|
case "compaction":
|
|
209
277
|
case "branch_summary":
|
|
210
278
|
case "custom":
|
|
211
279
|
case "custom_message":
|
|
212
280
|
case "label":
|
|
213
281
|
case "session_info":
|
|
214
|
-
case "leaf":
|
|
215
282
|
break;
|
|
216
283
|
}
|
|
284
|
+
// branch_summary and custom_message are user-role messages, valid cut points
|
|
217
285
|
if (entry.type === "branch_summary" || entry.type === "custom_message") {
|
|
218
286
|
cutPoints.push(i);
|
|
219
287
|
}
|
|
220
288
|
}
|
|
221
289
|
return cutPoints;
|
|
222
290
|
}
|
|
223
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Find the user message (or bashExecution) that starts the turn containing the given entry index.
|
|
293
|
+
* Returns -1 if no turn start found before the index.
|
|
294
|
+
* BashExecutionMessage is treated like a user message for turn boundaries.
|
|
295
|
+
*/
|
|
224
296
|
export function findTurnStartIndex(entries, entryIndex, startIndex) {
|
|
225
297
|
for (let i = entryIndex; i >= startIndex; i--) {
|
|
226
298
|
const entry = entries[i];
|
|
299
|
+
// branch_summary and custom_message are user-role messages, can start a turn
|
|
227
300
|
if (entry.type === "branch_summary" || entry.type === "custom_message") {
|
|
228
301
|
return i;
|
|
229
302
|
}
|
|
@@ -236,21 +309,40 @@ export function findTurnStartIndex(entries, entryIndex, startIndex) {
|
|
|
236
309
|
}
|
|
237
310
|
return -1;
|
|
238
311
|
}
|
|
239
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
|
314
|
+
*
|
|
315
|
+
* Algorithm: Walk backwards from newest, accumulating estimated message sizes.
|
|
316
|
+
* Stop when we've accumulated >= keepRecentTokens. Cut at that point.
|
|
317
|
+
*
|
|
318
|
+
* Can cut at user OR assistant messages (never tool results). When cutting at an
|
|
319
|
+
* assistant message with tool calls, its tool results come after and will be kept.
|
|
320
|
+
*
|
|
321
|
+
* Returns CutPointResult with:
|
|
322
|
+
* - firstKeptEntryIndex: the entry index to start keeping from
|
|
323
|
+
* - turnStartIndex: if cutting mid-turn, the user message that started that turn
|
|
324
|
+
* - isSplitTurn: whether we're cutting in the middle of a turn
|
|
325
|
+
*
|
|
326
|
+
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
|
327
|
+
*/
|
|
240
328
|
export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
|
241
329
|
const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
|
|
242
330
|
if (cutPoints.length === 0) {
|
|
243
331
|
return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
|
|
244
332
|
}
|
|
333
|
+
// Walk backwards from newest, accumulating estimated message sizes
|
|
245
334
|
let accumulatedTokens = 0;
|
|
246
|
-
let cutIndex = cutPoints[0];
|
|
335
|
+
let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
|
|
247
336
|
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
248
337
|
const entry = entries[i];
|
|
249
338
|
if (entry.type !== "message")
|
|
250
339
|
continue;
|
|
340
|
+
// Estimate this message's size
|
|
251
341
|
const messageTokens = estimateTokens(entry.message);
|
|
252
342
|
accumulatedTokens += messageTokens;
|
|
343
|
+
// Check if we've exceeded the budget
|
|
253
344
|
if (accumulatedTokens >= keepRecentTokens) {
|
|
345
|
+
// Find the closest valid cut point at or after this entry
|
|
254
346
|
for (let c = 0; c < cutPoints.length; c++) {
|
|
255
347
|
if (cutPoints[c] >= i) {
|
|
256
348
|
cutIndex = cutPoints[c];
|
|
@@ -260,16 +352,21 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
|
|
260
352
|
break;
|
|
261
353
|
}
|
|
262
354
|
}
|
|
355
|
+
// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)
|
|
263
356
|
while (cutIndex > startIndex) {
|
|
264
357
|
const prevEntry = entries[cutIndex - 1];
|
|
358
|
+
// Stop at session header or compaction boundaries
|
|
265
359
|
if (prevEntry.type === "compaction") {
|
|
266
360
|
break;
|
|
267
361
|
}
|
|
268
362
|
if (prevEntry.type === "message") {
|
|
363
|
+
// Stop if we hit any message
|
|
269
364
|
break;
|
|
270
365
|
}
|
|
366
|
+
// Include this non-message entry (bash, settings change, etc.)
|
|
271
367
|
cutIndex--;
|
|
272
368
|
}
|
|
369
|
+
// Determine if this is a split turn
|
|
273
370
|
const cutEntry = entries[cutIndex];
|
|
274
371
|
const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
|
|
275
372
|
const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
|
|
@@ -279,9 +376,9 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
|
|
279
376
|
isSplitTurn: !isUserMessage && turnStartIndex !== -1,
|
|
280
377
|
};
|
|
281
378
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
379
|
+
// ============================================================================
|
|
380
|
+
// Summarization
|
|
381
|
+
// ============================================================================
|
|
285
382
|
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
|
|
286
383
|
|
|
287
384
|
Use this EXACT format:
|
|
@@ -352,15 +449,46 @@ Use this EXACT format:
|
|
|
352
449
|
- [Preserve important context, add new if needed]
|
|
353
450
|
|
|
354
451
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
355
|
-
|
|
356
|
-
|
|
452
|
+
function createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel) {
|
|
453
|
+
const options = { maxTokens, signal, apiKey, headers };
|
|
454
|
+
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
|
|
455
|
+
options.reasoning = thinkingLevel;
|
|
456
|
+
}
|
|
457
|
+
return options;
|
|
458
|
+
}
|
|
459
|
+
async function completeSummarization(model, context, options, streamFn) {
|
|
460
|
+
if (!streamFn) {
|
|
461
|
+
return completeSimple(model, context, options);
|
|
462
|
+
}
|
|
463
|
+
const stream = await streamFn(model, context, options);
|
|
464
|
+
return stream.result();
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Generate a summary of the conversation using the LLM.
|
|
468
|
+
* If previousSummary is provided, uses the update prompt to merge.
|
|
469
|
+
*/
|
|
470
|
+
export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest) {
|
|
357
471
|
const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
|
472
|
+
// Use update prompt if we have a previous summary, otherwise initial prompt
|
|
358
473
|
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
|
359
474
|
if (customInstructions) {
|
|
360
475
|
basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`;
|
|
361
476
|
}
|
|
477
|
+
// Serialize conversation to text so model doesn't try to continue it
|
|
478
|
+
// Convert to LLM messages first (handles custom types like bashExecution, custom, etc.)
|
|
362
479
|
const llmMessages = convertToLlm(currentMessages);
|
|
363
|
-
|
|
480
|
+
let conversationText = serializeConversation(llmMessages);
|
|
481
|
+
// Brain-curation surface 3 (opt-in, injected by the session): pre-digest old chunks locally
|
|
482
|
+
// before the frontier summarization call. Best-effort — failure keeps the verbatim text.
|
|
483
|
+
if (preDigest) {
|
|
484
|
+
try {
|
|
485
|
+
conversationText = await preDigest(conversationText, signal);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// verbatim fallback
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Build the prompt with conversation wrapped in tags
|
|
364
492
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
365
493
|
if (previousSummary) {
|
|
366
494
|
promptText += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
|
|
@@ -373,26 +501,20 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
|
|
|
373
501
|
timestamp: Date.now(),
|
|
374
502
|
},
|
|
375
503
|
];
|
|
376
|
-
const completionOptions = model
|
|
377
|
-
|
|
378
|
-
: { maxTokens, signal, apiKey, headers };
|
|
379
|
-
const response = await completeSimple(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions);
|
|
380
|
-
if (response.stopReason === "aborted") {
|
|
381
|
-
return err(new CompactionError("aborted", response.errorMessage || "Summarization aborted"));
|
|
382
|
-
}
|
|
504
|
+
const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel);
|
|
505
|
+
const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, streamFn);
|
|
383
506
|
if (response.stopReason === "error") {
|
|
384
|
-
|
|
507
|
+
throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
|
|
385
508
|
}
|
|
386
509
|
const textContent = response.content
|
|
387
510
|
.filter((c) => c.type === "text")
|
|
388
511
|
.map((c) => c.text)
|
|
389
512
|
.join("\n");
|
|
390
|
-
return
|
|
513
|
+
return textContent;
|
|
391
514
|
}
|
|
392
|
-
/** Prepare session entries for compaction, or return undefined when compaction is not applicable. */
|
|
393
515
|
export function prepareCompaction(pathEntries, settings) {
|
|
394
|
-
if (pathEntries.length
|
|
395
|
-
return
|
|
516
|
+
if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
|
|
517
|
+
return undefined;
|
|
396
518
|
}
|
|
397
519
|
let prevCompactionIndex = -1;
|
|
398
520
|
for (let i = pathEntries.length - 1; i >= 0; i--) {
|
|
@@ -412,18 +534,21 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
412
534
|
const boundaryEnd = pathEntries.length;
|
|
413
535
|
const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;
|
|
414
536
|
const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
|
|
537
|
+
// Get UUID of first kept entry
|
|
415
538
|
const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
|
|
416
539
|
if (!firstKeptEntry?.id) {
|
|
417
|
-
return
|
|
540
|
+
return undefined; // Session needs migration
|
|
418
541
|
}
|
|
419
542
|
const firstKeptEntryId = firstKeptEntry.id;
|
|
420
543
|
const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
|
|
544
|
+
// Messages to summarize (will be discarded after summary)
|
|
421
545
|
const messagesToSummarize = [];
|
|
422
546
|
for (let i = boundaryStart; i < historyEnd; i++) {
|
|
423
547
|
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
|
424
548
|
if (msg)
|
|
425
549
|
messagesToSummarize.push(msg);
|
|
426
550
|
}
|
|
551
|
+
// Messages for turn prefix summary (if splitting a turn)
|
|
427
552
|
const turnPrefixMessages = [];
|
|
428
553
|
if (cutPoint.isSplitTurn) {
|
|
429
554
|
for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
|
|
@@ -432,13 +557,15 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
432
557
|
turnPrefixMessages.push(msg);
|
|
433
558
|
}
|
|
434
559
|
}
|
|
560
|
+
// Extract file operations from messages and previous compaction
|
|
435
561
|
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
|
562
|
+
// Also extract file ops from turn prefix if splitting
|
|
436
563
|
if (cutPoint.isSplitTurn) {
|
|
437
564
|
for (const msg of turnPrefixMessages) {
|
|
438
565
|
extractFileOpsFromMessage(msg, fileOps);
|
|
439
566
|
}
|
|
440
567
|
}
|
|
441
|
-
return
|
|
568
|
+
return {
|
|
442
569
|
firstKeptEntryId,
|
|
443
570
|
messagesToSummarize,
|
|
444
571
|
turnPrefixMessages,
|
|
@@ -447,8 +574,11 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
447
574
|
previousSummary,
|
|
448
575
|
fileOps,
|
|
449
576
|
settings,
|
|
450
|
-
}
|
|
577
|
+
};
|
|
451
578
|
}
|
|
579
|
+
// ============================================================================
|
|
580
|
+
// Main compaction function
|
|
581
|
+
// ============================================================================
|
|
452
582
|
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
|
453
583
|
|
|
454
584
|
Summarize the prefix to provide context for the retained suffix:
|
|
@@ -463,44 +593,50 @@ Summarize the prefix to provide context for the retained suffix:
|
|
|
463
593
|
- [Information needed to understand the retained recent work]
|
|
464
594
|
|
|
465
595
|
Be concise. Focus on what's needed to understand the kept suffix.`;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
596
|
+
/**
|
|
597
|
+
* Generate summaries for compaction using prepared data.
|
|
598
|
+
* Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.
|
|
599
|
+
*
|
|
600
|
+
* @param preparation - Pre-calculated preparation from prepareCompaction()
|
|
601
|
+
* @param customInstructions - Optional custom focus for the summary
|
|
602
|
+
*/
|
|
603
|
+
export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, preDigest) {
|
|
469
604
|
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
|
|
470
|
-
if
|
|
471
|
-
return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
|
|
472
|
-
}
|
|
605
|
+
// Generate summaries (can be parallel if both needed) and merge into one
|
|
473
606
|
let summary;
|
|
474
607
|
if (isSplitTurn && turnPrefixMessages.length > 0) {
|
|
608
|
+
// Generate both summaries in parallel
|
|
475
609
|
const [historyResult, turnPrefixResult] = await Promise.all([
|
|
476
610
|
messagesToSummarize.length > 0
|
|
477
|
-
? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel)
|
|
478
|
-
: Promise.resolve(
|
|
479
|
-
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel),
|
|
611
|
+
? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest)
|
|
612
|
+
: Promise.resolve("No prior history."),
|
|
613
|
+
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn),
|
|
480
614
|
]);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (!turnPrefixResult.ok)
|
|
484
|
-
return err(turnPrefixResult.error);
|
|
485
|
-
summary = `${historyResult.value}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value}`;
|
|
615
|
+
// Merge into single summary
|
|
616
|
+
summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
|
|
486
617
|
}
|
|
487
618
|
else {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return err(summaryResult.error);
|
|
491
|
-
summary = summaryResult.value;
|
|
619
|
+
// Just generate history summary
|
|
620
|
+
summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, preDigest);
|
|
492
621
|
}
|
|
622
|
+
// Compute file lists and append to summary
|
|
493
623
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
494
624
|
summary += formatFileOperations(readFiles, modifiedFiles);
|
|
495
|
-
|
|
625
|
+
if (!firstKeptEntryId) {
|
|
626
|
+
throw new Error("First kept entry has no UUID - session may need migration");
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
496
629
|
summary,
|
|
497
630
|
firstKeptEntryId,
|
|
498
631
|
tokensBefore,
|
|
499
632
|
details: { readFiles, modifiedFiles },
|
|
500
|
-
}
|
|
633
|
+
};
|
|
501
634
|
}
|
|
502
|
-
|
|
503
|
-
|
|
635
|
+
/**
|
|
636
|
+
* Generate a summary for a turn prefix (when splitting a turn).
|
|
637
|
+
*/
|
|
638
|
+
async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn) {
|
|
639
|
+
const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); // Smaller budget for turn prefix
|
|
504
640
|
const llmMessages = convertToLlm(messages);
|
|
505
641
|
const conversationText = serializeConversation(llmMessages);
|
|
506
642
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
@@ -511,18 +647,13 @@ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey,
|
|
|
511
647
|
timestamp: Date.now(),
|
|
512
648
|
},
|
|
513
649
|
];
|
|
514
|
-
const response = await
|
|
515
|
-
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
|
|
516
|
-
: { maxTokens, signal, apiKey, headers });
|
|
517
|
-
if (response.stopReason === "aborted") {
|
|
518
|
-
return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted"));
|
|
519
|
-
}
|
|
650
|
+
const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel), streamFn);
|
|
520
651
|
if (response.stopReason === "error") {
|
|
521
|
-
|
|
652
|
+
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
|
|
522
653
|
}
|
|
523
|
-
return
|
|
654
|
+
return response.content
|
|
524
655
|
.filter((c) => c.type === "text")
|
|
525
656
|
.map((c) => c.text)
|
|
526
|
-
.join("\n")
|
|
657
|
+
.join("\n");
|
|
527
658
|
}
|
|
528
659
|
//# sourceMappingURL=compaction.js.map
|