pi-agent-flow 2.2.17 → 2.2.19
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/agents/audit.md +1 -0
- package/agents/build.md +1 -0
- package/agents/craft.md +1 -0
- package/agents/debug.md +1 -0
- package/agents/ideas.md +1 -0
- package/agents/scout.md +1 -0
- package/agents/trace.md +1 -0
- package/dist/batch/batch-bash.d.ts.map +1 -1
- package/dist/batch/batch-bash.js +1 -4
- package/dist/batch/batch-bash.js.map +1 -1
- package/dist/batch/execute.js +2 -2
- package/dist/batch/execute.js.map +1 -1
- package/dist/batch/index.js +5 -5
- package/dist/batch/index.js.map +1 -1
- package/dist/config/config.d.ts +2 -2
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +5 -3
- package/dist/config/config.js.map +1 -1
- package/dist/config/settings-resolver.d.ts +2 -1
- package/dist/config/settings-resolver.d.ts.map +1 -1
- package/dist/config/settings-resolver.js +6 -3
- package/dist/config/settings-resolver.js.map +1 -1
- package/dist/core2/snapshot.d.ts +25 -1
- package/dist/core2/snapshot.d.ts.map +1 -1
- package/dist/core2/snapshot.js +655 -23
- package/dist/core2/snapshot.js.map +1 -1
- package/dist/flow/agents.d.ts +7 -0
- package/dist/flow/agents.d.ts.map +1 -1
- package/dist/flow/agents.js +54 -1
- package/dist/flow/agents.js.map +1 -1
- package/dist/flow/execute-single.d.ts.map +1 -1
- package/dist/flow/execute-single.js +1 -0
- package/dist/flow/execute-single.js.map +1 -1
- package/dist/flow/executor.d.ts +1 -0
- package/dist/flow/executor.d.ts.map +1 -1
- package/dist/flow/executor.js.map +1 -1
- package/dist/flow/runner.d.ts +2 -0
- package/dist/flow/runner.d.ts.map +1 -1
- package/dist/flow/runner.js +1 -0
- package/dist/flow/runner.js.map +1 -1
- package/dist/flow/settings-command.d.ts +1 -1
- package/dist/flow/settings-command.d.ts.map +1 -1
- package/dist/flow/settings-command.js +7 -6
- package/dist/flow/settings-command.js.map +1 -1
- package/dist/flow/settings-handler.d.ts.map +1 -1
- package/dist/flow/settings-handler.js +21 -19
- package/dist/flow/settings-handler.js.map +1 -1
- package/dist/flow/settings-items.d.ts +2 -1
- package/dist/flow/settings-items.d.ts.map +1 -1
- package/dist/flow/settings-items.js +18 -8
- package/dist/flow/settings-items.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -38
- package/dist/index.js.map +1 -1
- package/dist/snapshot/cli-args.d.ts.map +1 -1
- package/dist/snapshot/cli-args.js +0 -1
- package/dist/snapshot/cli-args.js.map +1 -1
- package/dist/steering/tool-utils.d.ts +2 -20
- package/dist/steering/tool-utils.d.ts.map +1 -1
- package/dist/steering/tool-utils.js +2 -48
- package/dist/steering/tool-utils.js.map +1 -1
- package/dist/tools/ask-user.d.ts.map +1 -1
- package/dist/tools/ask-user.js +1 -4
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/loop-guard.d.ts +11 -0
- package/dist/tools/loop-guard.d.ts.map +1 -0
- package/dist/tools/loop-guard.js +40 -0
- package/dist/tools/loop-guard.js.map +1 -0
- package/dist/tools/timed-bash.d.ts.map +1 -1
- package/dist/tools/timed-bash.js +5 -3
- package/dist/tools/timed-bash.js.map +1 -1
- package/dist/tools/trace.d.ts +1 -0
- package/dist/tools/trace.d.ts.map +1 -1
- package/dist/tools/trace.js +12 -4
- package/dist/tools/trace.js.map +1 -1
- package/dist/tui/body-render.js +3 -3
- package/dist/tui/body-render.js.map +1 -1
- package/dist/tui/context-display.d.ts +2 -0
- package/dist/tui/context-display.d.ts.map +1 -1
- package/dist/tui/context-display.js +11 -0
- package/dist/tui/context-display.js.map +1 -1
- package/dist/tui/flow-colors.js +1 -1
- package/dist/tui/flow-colors.js.map +1 -1
- package/dist/tui/render-utils.d.ts +1 -0
- package/dist/tui/render-utils.d.ts.map +1 -1
- package/dist/tui/render-utils.js +13 -3
- package/dist/tui/render-utils.js.map +1 -1
- package/dist/tui/render.d.ts +1 -1
- package/dist/tui/render.d.ts.map +1 -1
- package/dist/tui/render.js +9 -64
- package/dist/tui/render.js.map +1 -1
- package/dist/types/flow.d.ts +3 -2
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/flow.js.map +1 -1
- package/package.json +1 -1
package/dist/core2/snapshot.js
CHANGED
|
@@ -7,6 +7,102 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { stripDirectives } from "../steering/tool-utils.js";
|
|
9
9
|
import { logWarn } from "../config/log.js";
|
|
10
|
+
/**
|
|
11
|
+
* Build a snapshot with optional context compression. If the estimated token
|
|
12
|
+
* count exceeds the threshold (or an override is set), a second pass applies
|
|
13
|
+
* the resolved compression level and profile.
|
|
14
|
+
*/
|
|
15
|
+
export function buildSnapshotWithCompression(sessionManager, options, maxContextTokens) {
|
|
16
|
+
const rawSnapshot = buildCore2Snapshot(sessionManager, options);
|
|
17
|
+
if (!rawSnapshot)
|
|
18
|
+
return { snapshot: null };
|
|
19
|
+
const estimatedTokens = estimateTotalContextTokens(rawSnapshot);
|
|
20
|
+
const thresholdEnv = process.env.PI_FLOW_CONTEXT_THRESHOLD;
|
|
21
|
+
const threshold = thresholdEnv ? Number(thresholdEnv) : 70_000;
|
|
22
|
+
const overrideRaw = process.env.PI_FLOW_CONTEXT_COMPRESSION?.trim().toLowerCase();
|
|
23
|
+
const envOverride = overrideRaw === "none" || overrideRaw === "light" || overrideRaw === "medium" || overrideRaw === "aggressive"
|
|
24
|
+
? overrideRaw
|
|
25
|
+
: undefined;
|
|
26
|
+
const override = options?.compressionLevel ?? envOverride;
|
|
27
|
+
const effectiveThreshold = maxContextTokens && maxContextTokens > 0 && Number.isFinite(maxContextTokens)
|
|
28
|
+
? Math.min(threshold, Math.floor(maxContextTokens * 0.6))
|
|
29
|
+
: threshold;
|
|
30
|
+
const lightThreshold = effectiveThreshold;
|
|
31
|
+
const mediumThreshold = Math.floor(effectiveThreshold * 1.21);
|
|
32
|
+
const aggressiveThreshold = Math.floor(effectiveThreshold * 1.43);
|
|
33
|
+
let level;
|
|
34
|
+
if (override) {
|
|
35
|
+
level = override;
|
|
36
|
+
}
|
|
37
|
+
else if (estimatedTokens < lightThreshold) {
|
|
38
|
+
return { snapshot: rawSnapshot };
|
|
39
|
+
}
|
|
40
|
+
else if (estimatedTokens < mediumThreshold) {
|
|
41
|
+
level = "light";
|
|
42
|
+
}
|
|
43
|
+
else if (estimatedTokens < aggressiveThreshold) {
|
|
44
|
+
level = "medium";
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
level = "aggressive";
|
|
48
|
+
}
|
|
49
|
+
if (level === "none")
|
|
50
|
+
return { snapshot: rawSnapshot };
|
|
51
|
+
const stats = {
|
|
52
|
+
preBytes: rawSnapshot.length,
|
|
53
|
+
postBytes: 0,
|
|
54
|
+
level,
|
|
55
|
+
messagesDropped: 0,
|
|
56
|
+
};
|
|
57
|
+
const compressedSnapshot = buildCore2Snapshot(sessionManager, {
|
|
58
|
+
...options,
|
|
59
|
+
compressionLevel: level,
|
|
60
|
+
compressionProfile: options?.compressionProfile,
|
|
61
|
+
compressionStats: stats,
|
|
62
|
+
});
|
|
63
|
+
if (stats && compressedSnapshot) {
|
|
64
|
+
stats.preBytes = rawSnapshot.length;
|
|
65
|
+
stats.postBytes = compressedSnapshot.length;
|
|
66
|
+
}
|
|
67
|
+
// Emergency warp fallback: if aggressive compression is still too large,
|
|
68
|
+
// and PI_FLOW_EMERGENCY_WARP is enabled, distill to a minimal snapshot.
|
|
69
|
+
if (level === "aggressive" &&
|
|
70
|
+
compressedSnapshot &&
|
|
71
|
+
estimateTotalContextTokens(compressedSnapshot) > (maxContextTokens ?? 100_000) * 0.6 &&
|
|
72
|
+
(process.env.PI_FLOW_EMERGENCY_WARP === "1" || process.env.PI_FLOW_EMERGENCY_WARP === "true")) {
|
|
73
|
+
const allEntries = compressedSnapshot
|
|
74
|
+
.split("\n")
|
|
75
|
+
.filter((l) => l.length > 0)
|
|
76
|
+
.map((l) => {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(l);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
logWarn(`[pi-agent-flow] Failed to parse JSONL line in emergency warp: ${e}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.filter((e) => e !== null);
|
|
86
|
+
const summary = generateSyntheticSummary(allEntries);
|
|
87
|
+
const lastMessages = allEntries.slice(-2);
|
|
88
|
+
const distilled = [
|
|
89
|
+
allEntries[0],
|
|
90
|
+
CONTEXT_MAP_ENTRY,
|
|
91
|
+
{
|
|
92
|
+
type: "message",
|
|
93
|
+
message: {
|
|
94
|
+
role: "system",
|
|
95
|
+
content: `[Emergency Warp] Context exceeds safe limits after aggressive compression. Distilled summary:\n${summary ?? "(no summary generated)"}`,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
...lastMessages,
|
|
99
|
+
];
|
|
100
|
+
const emergencySnapshot = distilled.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
101
|
+
stats.postBytes = emergencySnapshot.length;
|
|
102
|
+
return { snapshot: emergencySnapshot, stats };
|
|
103
|
+
}
|
|
104
|
+
return { snapshot: compressedSnapshot, stats };
|
|
105
|
+
}
|
|
10
106
|
/** Normalize toolCalls field from various input shapes. */
|
|
11
107
|
function normalizeToolCalls(msg) {
|
|
12
108
|
if (!msg || typeof msg !== "object")
|
|
@@ -26,6 +122,89 @@ function normalizeToolCallId(tc) {
|
|
|
26
122
|
const t = tc;
|
|
27
123
|
return t.id || t.toolCallId || t[SNAKE_TOOL_CALL_ID];
|
|
28
124
|
}
|
|
125
|
+
/** Rough token estimate for a snapshot string (1 token ≈ 4 chars). */
|
|
126
|
+
export function estimateTotalContextTokens(snapshotJsonl) {
|
|
127
|
+
return Math.ceil(snapshotJsonl.length / 4);
|
|
128
|
+
}
|
|
129
|
+
/** Extract plain text from a message entry for scoring / classification. */
|
|
130
|
+
function extractMessageText(msg) {
|
|
131
|
+
if (typeof msg.content === "string") {
|
|
132
|
+
return msg.content;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(msg.content)) {
|
|
135
|
+
return msg.content
|
|
136
|
+
.filter((b) => b && b.type === "text" && typeof b.text === "string")
|
|
137
|
+
.map((b) => b.text)
|
|
138
|
+
.join("\n");
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
/** Classify a tool/toolResult message into a category for profile-aware compression. */
|
|
143
|
+
function classifyToolResult(entry) {
|
|
144
|
+
if (!entry || typeof entry !== "object")
|
|
145
|
+
return "other";
|
|
146
|
+
const e = entry;
|
|
147
|
+
if (e.type !== "message" || !e.message || typeof e.message !== "object")
|
|
148
|
+
return "other";
|
|
149
|
+
const msg = e.message;
|
|
150
|
+
if (msg.role !== "tool" && msg.role !== "toolResult")
|
|
151
|
+
return "other";
|
|
152
|
+
const text = extractMessageText(msg);
|
|
153
|
+
const toolName = typeof msg.toolName === "string" ? msg.toolName : typeof msg.name === "string" ? msg.name : "";
|
|
154
|
+
// Error / stack-trace heuristics
|
|
155
|
+
if (text.includes("Error:") ||
|
|
156
|
+
text.includes("error:") ||
|
|
157
|
+
text.includes("FAIL") ||
|
|
158
|
+
text.includes("failed") ||
|
|
159
|
+
text.includes("Exception") ||
|
|
160
|
+
text.includes("exception") ||
|
|
161
|
+
text.includes("AssertionError") ||
|
|
162
|
+
text.includes("TypeError") ||
|
|
163
|
+
text.includes("ReferenceError")) {
|
|
164
|
+
// Stack trace detection: lines starting with " at " or " at "
|
|
165
|
+
const stackPattern = /^\s+at\s+\S+/gm;
|
|
166
|
+
if (stackPattern.test(text))
|
|
167
|
+
return "stackTrace";
|
|
168
|
+
return "error";
|
|
169
|
+
}
|
|
170
|
+
// Test failure
|
|
171
|
+
if (text.includes("Test failed") ||
|
|
172
|
+
text.includes("test failure") ||
|
|
173
|
+
text.includes("Assertion failed") ||
|
|
174
|
+
text.includes("expected") && text.includes("received") ||
|
|
175
|
+
text.includes("✕") ||
|
|
176
|
+
text.includes("FAIL") && text.includes("test")) {
|
|
177
|
+
return "testFailure";
|
|
178
|
+
}
|
|
179
|
+
// Git diff
|
|
180
|
+
if (text.includes("diff --git") ||
|
|
181
|
+
text.includes("--- a/") ||
|
|
182
|
+
text.includes("+++ b/") ||
|
|
183
|
+
text.includes("@@ -")) {
|
|
184
|
+
return "gitDiff";
|
|
185
|
+
}
|
|
186
|
+
// Batch file operations (read / write / edit)
|
|
187
|
+
if (text.includes("--- read:") ||
|
|
188
|
+
text.includes("--- write:") ||
|
|
189
|
+
text.includes("--- edit:") ||
|
|
190
|
+
text.includes("--- delete:") ||
|
|
191
|
+
(text.includes("✔") && (text.includes("read") || text.includes("write") || text.includes("edit")))) {
|
|
192
|
+
return "fileContent";
|
|
193
|
+
}
|
|
194
|
+
// Grep / find / ls results
|
|
195
|
+
if (text.includes("--- rg:") ||
|
|
196
|
+
text.includes("--- find:") ||
|
|
197
|
+
text.includes("--- ls:") ||
|
|
198
|
+
text.includes("grep results") ||
|
|
199
|
+
/^[^\n]+:\d+:.+/m.test(text)) {
|
|
200
|
+
return "grepResult";
|
|
201
|
+
}
|
|
202
|
+
// Bash success
|
|
203
|
+
if (toolName === "bash" || text.includes("--- bash [") || text.includes("--- [") && text.includes("exit")) {
|
|
204
|
+
return "bashSuccess";
|
|
205
|
+
}
|
|
206
|
+
return "other";
|
|
207
|
+
}
|
|
29
208
|
/** Synthetic system message providing the context architecture map. */
|
|
30
209
|
const CONTEXT_MAP_ENTRY = {
|
|
31
210
|
type: "message",
|
|
@@ -102,8 +281,8 @@ export function buildCore2Snapshot(sessionManager, options) {
|
|
|
102
281
|
processedEntry = maybeStripCompaction(processedEntry);
|
|
103
282
|
if (!processedEntry)
|
|
104
283
|
continue;
|
|
105
|
-
processedEntry = compressSnapshotEntry(processedEntry, options?.tier);
|
|
106
|
-
processedEntry = truncateStandaloneBashResult(processedEntry);
|
|
284
|
+
processedEntry = compressSnapshotEntry(processedEntry, options?.tier, options?.compressionLevel, options?.compressionProfile);
|
|
285
|
+
processedEntry = truncateStandaloneBashResult(processedEntry, options?.compressionLevel, options?.compressionProfile);
|
|
107
286
|
if (options?.activeToolCallId) {
|
|
108
287
|
processedEntry = stripActiveToolCall(processedEntry, options.activeToolCallId);
|
|
109
288
|
if (!processedEntry)
|
|
@@ -112,14 +291,26 @@ export function buildCore2Snapshot(sessionManager, options) {
|
|
|
112
291
|
processedEntries.push(processedEntry);
|
|
113
292
|
}
|
|
114
293
|
const finalEntries = applyMessageLimit(processedEntries, options?.tier);
|
|
115
|
-
const
|
|
294
|
+
const compressedResult = applyContextCompression(finalEntries, options?.compressionLevel, options?.compressionProfile);
|
|
295
|
+
const entriesWithMap = insertContextMap(compressedResult.entries);
|
|
116
296
|
for (const entry of entriesWithMap) {
|
|
117
297
|
const line = JSON.stringify(entry);
|
|
118
298
|
// Strip batch read/write/edit bodies from tool result messages
|
|
119
|
-
const processed = maybeStripBatchBodies(line);
|
|
299
|
+
const processed = maybeStripBatchBodies(line, options?.compressionLevel);
|
|
120
300
|
lines.push(processed);
|
|
121
301
|
}
|
|
122
|
-
|
|
302
|
+
const snapshot = `${lines.join("\n")}\n`;
|
|
303
|
+
if (options?.compressionStats) {
|
|
304
|
+
const preBytes = processedEntries.reduce((sum, e) => sum + JSON.stringify(e).length, 0);
|
|
305
|
+
const postBytes = snapshot.length;
|
|
306
|
+
options.compressionStats.preBytes = preBytes;
|
|
307
|
+
options.compressionStats.postBytes = postBytes;
|
|
308
|
+
options.compressionStats.level = options.compressionLevel ?? "none";
|
|
309
|
+
options.compressionStats.profileName = options.compressionProfile?.name;
|
|
310
|
+
options.compressionStats.messagesDropped = compressedResult.droppedCount;
|
|
311
|
+
options.compressionStats.syntheticSummary = compressedResult.syntheticSummary;
|
|
312
|
+
}
|
|
313
|
+
return snapshot;
|
|
123
314
|
}
|
|
124
315
|
/**
|
|
125
316
|
* Keep only the usage fields pi-coding-agent needs for context accounting.
|
|
@@ -355,7 +546,7 @@ function optimizeSharedContext(branchEntries) {
|
|
|
355
546
|
// ---------------------------------------------------------------------------
|
|
356
547
|
// Standalone bash result truncation
|
|
357
548
|
// ---------------------------------------------------------------------------
|
|
358
|
-
function truncateStandaloneBashResult(entry) {
|
|
549
|
+
function truncateStandaloneBashResult(entry, level, profile) {
|
|
359
550
|
if (!entry || typeof entry !== "object")
|
|
360
551
|
return entry;
|
|
361
552
|
const e = entry;
|
|
@@ -367,6 +558,9 @@ function truncateStandaloneBashResult(entry) {
|
|
|
367
558
|
const toolName = typeof msg.toolName === "string" ? msg.toolName : typeof msg.name === "string" ? msg.name : undefined;
|
|
368
559
|
if (toolName !== "bash")
|
|
369
560
|
return entry;
|
|
561
|
+
// Respect profile: if bashSuccess is in keepCategories, skip truncation
|
|
562
|
+
if (profile?.keepCategories?.includes("bashSuccess"))
|
|
563
|
+
return entry;
|
|
370
564
|
let text;
|
|
371
565
|
let textIndex;
|
|
372
566
|
if (typeof msg.content === "string") {
|
|
@@ -385,8 +579,24 @@ function truncateStandaloneBashResult(entry) {
|
|
|
385
579
|
if (!text)
|
|
386
580
|
return entry;
|
|
387
581
|
const lines = text.split("\n");
|
|
388
|
-
|
|
389
|
-
|
|
582
|
+
let head;
|
|
583
|
+
let tail;
|
|
584
|
+
if (level === "light") {
|
|
585
|
+
head = 15;
|
|
586
|
+
tail = 10;
|
|
587
|
+
}
|
|
588
|
+
else if (level === "medium") {
|
|
589
|
+
head = 10;
|
|
590
|
+
tail = 5;
|
|
591
|
+
}
|
|
592
|
+
else if (level === "aggressive") {
|
|
593
|
+
head = 5;
|
|
594
|
+
tail = 3;
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
head = 30;
|
|
598
|
+
tail = 20;
|
|
599
|
+
}
|
|
390
600
|
if (lines.length <= head + tail)
|
|
391
601
|
return entry;
|
|
392
602
|
const kept = [...lines.slice(0, head), `[...${lines.length - head - tail} lines of bash output truncated...]`, ...lines.slice(-tail)];
|
|
@@ -433,13 +643,16 @@ function getTierMaxMessages(tier) {
|
|
|
433
643
|
return Number.isFinite(n) && n > 0 ? n : defaultVal;
|
|
434
644
|
}
|
|
435
645
|
/**
|
|
436
|
-
* Compress a single snapshot entry based on tier.
|
|
646
|
+
* Compress a single snapshot entry based on tier and optional compression level.
|
|
647
|
+
*
|
|
648
|
+
* When no compression level is set, all tiers strip tool/toolResult content to
|
|
649
|
+
* placeholders (e.g. [toolResult: bash]) — the legacy behavior.
|
|
437
650
|
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
651
|
+
* When a compression level is active, the profile determines which categories
|
|
652
|
+
* are essential (kept verbatim) vs non-essential (compressed to placeholder).
|
|
653
|
+
* Aggressive always compresses everything regardless of profile.
|
|
441
654
|
*/
|
|
442
|
-
function compressSnapshotEntry(entry, tier) {
|
|
655
|
+
function compressSnapshotEntry(entry, tier, level, profile) {
|
|
443
656
|
if (!tier)
|
|
444
657
|
return entry;
|
|
445
658
|
if (!entry || typeof entry !== "object")
|
|
@@ -453,6 +666,29 @@ function compressSnapshotEntry(entry, tier) {
|
|
|
453
666
|
if (role !== "tool" && role !== "toolResult") {
|
|
454
667
|
return entry;
|
|
455
668
|
}
|
|
669
|
+
// Legacy behavior when no compression level is active
|
|
670
|
+
if (!level || level === "none") {
|
|
671
|
+
const toolName = typeof msg.toolName === "string" ? msg.toolName : typeof msg.name === "string" ? msg.name : undefined;
|
|
672
|
+
const placeholder = toolName ? `[${role}: ${toolName}]` : `[${role} result omitted]`;
|
|
673
|
+
msg.content = [{ type: "text", text: placeholder }];
|
|
674
|
+
return { ...e, message: msg };
|
|
675
|
+
}
|
|
676
|
+
// Aggressive compresses all tool results regardless of profile
|
|
677
|
+
if (level === "aggressive") {
|
|
678
|
+
const toolName = typeof msg.toolName === "string" ? msg.toolName : typeof msg.name === "string" ? msg.name : undefined;
|
|
679
|
+
const placeholder = toolName ? `[${role}: ${toolName}]` : `[${role} result omitted]`;
|
|
680
|
+
msg.content = [{ type: "text", text: placeholder }];
|
|
681
|
+
return { ...e, message: msg };
|
|
682
|
+
}
|
|
683
|
+
// Profile-aware selective compression for light / medium
|
|
684
|
+
const category = classifyToolResult(entry);
|
|
685
|
+
const keep = profile?.keepCategories ?? [];
|
|
686
|
+
const compress = profile?.compressCategories ?? [];
|
|
687
|
+
if (keep.includes(category)) {
|
|
688
|
+
// Keep this tool result verbatim
|
|
689
|
+
return entry;
|
|
690
|
+
}
|
|
691
|
+
// Compress to placeholder (default for unknown categories too)
|
|
456
692
|
const toolName = typeof msg.toolName === "string" ? msg.toolName : typeof msg.name === "string" ? msg.name : undefined;
|
|
457
693
|
const placeholder = toolName ? `[${role}: ${toolName}]` : `[${role} result omitted]`;
|
|
458
694
|
msg.content = [{ type: "text", text: placeholder }];
|
|
@@ -510,6 +746,368 @@ function applyMessageLimit(entries, tier) {
|
|
|
510
746
|
return entries;
|
|
511
747
|
}
|
|
512
748
|
// ---------------------------------------------------------------------------
|
|
749
|
+
// Context compression (token-gate progressive reduction)
|
|
750
|
+
// ---------------------------------------------------------------------------
|
|
751
|
+
function scoreMessageForProfile(entry, profile) {
|
|
752
|
+
if (!entry || typeof entry !== "object")
|
|
753
|
+
return 1;
|
|
754
|
+
const e = entry;
|
|
755
|
+
if (e.type !== "message" || !e.message || typeof e.message !== "object")
|
|
756
|
+
return 1;
|
|
757
|
+
const msg = e.message;
|
|
758
|
+
// Base score: newer messages win ties via caller sorting
|
|
759
|
+
let score = 10;
|
|
760
|
+
if (profile?.name === "intent-first") {
|
|
761
|
+
if (msg.role === "user")
|
|
762
|
+
return 100;
|
|
763
|
+
if (msg.role === "assistant") {
|
|
764
|
+
const text = extractMessageText(msg);
|
|
765
|
+
if (/\b(decision|design|plan|architecture|goal|objective)\b/i.test(text))
|
|
766
|
+
return 90;
|
|
767
|
+
return 50;
|
|
768
|
+
}
|
|
769
|
+
return 5;
|
|
770
|
+
}
|
|
771
|
+
if (profile?.name === "files-first") {
|
|
772
|
+
if (msg.role === "toolResult" || msg.role === "tool") {
|
|
773
|
+
const cat = classifyToolResult(entry);
|
|
774
|
+
if (cat === "fileContent")
|
|
775
|
+
return 90;
|
|
776
|
+
if (cat === "error")
|
|
777
|
+
return 80;
|
|
778
|
+
return 20;
|
|
779
|
+
}
|
|
780
|
+
if (msg.role === "assistant") {
|
|
781
|
+
const content = msg.content;
|
|
782
|
+
if (Array.isArray(content)) {
|
|
783
|
+
const hasFileOps = content.some((block) => {
|
|
784
|
+
if (block?.type === "toolCall") {
|
|
785
|
+
const name = block.name || block.toolCall?.name;
|
|
786
|
+
return name === "batch" || name === "batch_read";
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
});
|
|
790
|
+
if (hasFileOps)
|
|
791
|
+
return 85;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return 40;
|
|
795
|
+
}
|
|
796
|
+
if (profile?.name === "errors-first") {
|
|
797
|
+
if (msg.role === "toolResult" || msg.role === "tool") {
|
|
798
|
+
const cat = classifyToolResult(entry);
|
|
799
|
+
if (cat === "error" || cat === "stackTrace" || cat === "testFailure")
|
|
800
|
+
return 100;
|
|
801
|
+
return 15;
|
|
802
|
+
}
|
|
803
|
+
return 40;
|
|
804
|
+
}
|
|
805
|
+
if (profile?.name === "edits-first") {
|
|
806
|
+
if (msg.role === "toolResult" || msg.role === "tool") {
|
|
807
|
+
const cat = classifyToolResult(entry);
|
|
808
|
+
if (cat === "fileContent" || cat === "gitDiff")
|
|
809
|
+
return 95;
|
|
810
|
+
if (cat === "error")
|
|
811
|
+
return 80;
|
|
812
|
+
return 20;
|
|
813
|
+
}
|
|
814
|
+
return 40;
|
|
815
|
+
}
|
|
816
|
+
if (profile?.name === "discovery-first") {
|
|
817
|
+
if (msg.role === "toolResult" || msg.role === "tool") {
|
|
818
|
+
const cat = classifyToolResult(entry);
|
|
819
|
+
if (cat === "grepResult")
|
|
820
|
+
return 95;
|
|
821
|
+
if (cat === "fileContent")
|
|
822
|
+
return 80;
|
|
823
|
+
if (cat === "error")
|
|
824
|
+
return 70;
|
|
825
|
+
return 20;
|
|
826
|
+
}
|
|
827
|
+
return 40;
|
|
828
|
+
}
|
|
829
|
+
if (profile?.name === "code-first") {
|
|
830
|
+
if (msg.role === "toolResult" || msg.role === "tool") {
|
|
831
|
+
const cat = classifyToolResult(entry);
|
|
832
|
+
if (cat === "fileContent")
|
|
833
|
+
return 100;
|
|
834
|
+
if (cat === "error")
|
|
835
|
+
return 80;
|
|
836
|
+
return 15;
|
|
837
|
+
}
|
|
838
|
+
return 40;
|
|
839
|
+
}
|
|
840
|
+
// Default scoring
|
|
841
|
+
if (msg.role === "user")
|
|
842
|
+
return 60;
|
|
843
|
+
if (msg.role === "assistant")
|
|
844
|
+
return 50;
|
|
845
|
+
if (msg.role === "toolResult" || msg.role === "tool") {
|
|
846
|
+
const cat = classifyToolResult(entry);
|
|
847
|
+
if (cat === "error")
|
|
848
|
+
return 45;
|
|
849
|
+
return 30;
|
|
850
|
+
}
|
|
851
|
+
return 20;
|
|
852
|
+
}
|
|
853
|
+
function stripOldSystemMessages(entries, _profile) {
|
|
854
|
+
return entries.filter((entry) => {
|
|
855
|
+
if (!entry || typeof entry !== "object")
|
|
856
|
+
return true;
|
|
857
|
+
const e = entry;
|
|
858
|
+
if (e.type !== "message" || !e.message || typeof e.message !== "object")
|
|
859
|
+
return true;
|
|
860
|
+
const msg = e.message;
|
|
861
|
+
if (msg.role !== "system")
|
|
862
|
+
return true;
|
|
863
|
+
// Keep the context map entry (it hasn't been inserted yet here, but be defensive)
|
|
864
|
+
const text = extractMessageText(msg);
|
|
865
|
+
if (text.includes("[SHARED CONTEXT]") || text.includes("<context-seal>"))
|
|
866
|
+
return true;
|
|
867
|
+
// Drop old system messages
|
|
868
|
+
return false;
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function generateSyntheticSummary(droppedEntries) {
|
|
872
|
+
if (droppedEntries.length === 0)
|
|
873
|
+
return undefined;
|
|
874
|
+
const files = new Set();
|
|
875
|
+
const commands = new Set();
|
|
876
|
+
const errors = new Set();
|
|
877
|
+
const decisions = new Set();
|
|
878
|
+
for (const entry of droppedEntries) {
|
|
879
|
+
if (!entry || typeof entry !== "object")
|
|
880
|
+
continue;
|
|
881
|
+
const e = entry;
|
|
882
|
+
if (e.type !== "message" || !e.message || typeof e.message !== "object")
|
|
883
|
+
continue;
|
|
884
|
+
const msg = e.message;
|
|
885
|
+
const text = extractMessageText(msg);
|
|
886
|
+
// Extract file paths from read/write/edit headers
|
|
887
|
+
const fileMatches = text.match(/--- (?:read|write|edit|delete): ([^\s()]+)/g);
|
|
888
|
+
if (fileMatches) {
|
|
889
|
+
for (const m of fileMatches) {
|
|
890
|
+
const path = m.replace(/^--- (?:read|write|edit|delete): /, "").trim();
|
|
891
|
+
if (path)
|
|
892
|
+
files.add(path);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Extract bash commands — permissive header matching + fallback scan
|
|
896
|
+
let foundBash = false;
|
|
897
|
+
const bashHeaderPattern = /--- bash(?:\s*\[.*?\])?\s*(?:exit\s*\d+|pending|error)?\s*---\s*([\s\S]*?)(?=\n---|\n###\s|$)/g;
|
|
898
|
+
let bashMatch;
|
|
899
|
+
while ((bashMatch = bashHeaderPattern.exec(text)) !== null) {
|
|
900
|
+
const block = bashMatch[1].trim();
|
|
901
|
+
const firstLine = block.split("\n").find((l) => l.trim().length > 0);
|
|
902
|
+
if (firstLine) {
|
|
903
|
+
commands.add(firstLine.trim().slice(0, 120));
|
|
904
|
+
foundBash = true;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (!foundBash) {
|
|
908
|
+
// Fallback: scan for bash-like lines or JSONL tool call blocks
|
|
909
|
+
const fallbackBash = [];
|
|
910
|
+
const bashLikeRe = /\bbash\b.*?:\s*(.+)/gim;
|
|
911
|
+
let m;
|
|
912
|
+
while ((m = bashLikeRe.exec(text)) !== null) {
|
|
913
|
+
fallbackBash.push(m[1].trim());
|
|
914
|
+
}
|
|
915
|
+
const jsonCmdRe = /"command"\s*:\s*"([^"]+)"/g;
|
|
916
|
+
while ((m = jsonCmdRe.exec(text)) !== null) {
|
|
917
|
+
fallbackBash.push(m[1].trim());
|
|
918
|
+
}
|
|
919
|
+
if (fallbackBash.length > 0) {
|
|
920
|
+
for (const cmd of fallbackBash) {
|
|
921
|
+
if (cmd)
|
|
922
|
+
commands.add(cmd.slice(0, 120));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Also extract from tool call arguments if present
|
|
927
|
+
const tcs = (msg.toolCalls ?? msg.tool_calls ?? []);
|
|
928
|
+
for (const tc of tcs) {
|
|
929
|
+
if (!tc || typeof tc !== "object")
|
|
930
|
+
continue;
|
|
931
|
+
const t = tc;
|
|
932
|
+
const args = t.arguments ?? t.function?.arguments;
|
|
933
|
+
if (args && typeof args === "object") {
|
|
934
|
+
const a = args;
|
|
935
|
+
if (typeof a.command === "string")
|
|
936
|
+
commands.add(a.command);
|
|
937
|
+
if (typeof a.c === "string")
|
|
938
|
+
commands.add(a.c);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (Array.isArray(msg.content)) {
|
|
942
|
+
for (const block of msg.content) {
|
|
943
|
+
if (block && typeof block === "object" && block.type === "toolCall" && block.arguments) {
|
|
944
|
+
const args = block.arguments;
|
|
945
|
+
if (typeof args.command === "string")
|
|
946
|
+
commands.add(args.command);
|
|
947
|
+
if (typeof args.c === "string")
|
|
948
|
+
commands.add(args.c);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// Extract errors (first line only, capped)
|
|
953
|
+
const errorPattern = /\b(Error|FAIL|failed|Exception|AssertionError)\b/i;
|
|
954
|
+
if (errorPattern.test(text)) {
|
|
955
|
+
const firstErrorLine = text.split("\n").find((l) => errorPattern.test(l));
|
|
956
|
+
if (firstErrorLine) {
|
|
957
|
+
const truncated = firstErrorLine.trim().slice(0, 120);
|
|
958
|
+
if (truncated)
|
|
959
|
+
errors.add(truncated);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
// Extract decisions with word boundaries
|
|
963
|
+
const decisionPattern = /\b(decision|design|plan|agreed|chose|selected|will use)\b/i;
|
|
964
|
+
if (decisionPattern.test(text)) {
|
|
965
|
+
const decisionLine = text.split("\n").find((l) => decisionPattern.test(l));
|
|
966
|
+
if (decisionLine) {
|
|
967
|
+
const truncated = decisionLine.trim().slice(0, 120);
|
|
968
|
+
if (truncated)
|
|
969
|
+
decisions.add(truncated);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// Fallback: if regex-based extraction found nothing, do a simple keyword scan
|
|
974
|
+
if (files.size === 0 && commands.size === 0 && errors.size === 0 && decisions.size === 0) {
|
|
975
|
+
for (const entry of droppedEntries) {
|
|
976
|
+
if (!entry || typeof entry !== "object")
|
|
977
|
+
continue;
|
|
978
|
+
const e = entry;
|
|
979
|
+
if (e.type !== "message" || !e.message || typeof e.message !== "object")
|
|
980
|
+
continue;
|
|
981
|
+
const msg = e.message;
|
|
982
|
+
const text = extractMessageText(msg);
|
|
983
|
+
const lines = text.split("\n");
|
|
984
|
+
for (const line of lines) {
|
|
985
|
+
const trimmed = line.trim();
|
|
986
|
+
if (!trimmed)
|
|
987
|
+
continue;
|
|
988
|
+
if (/\b(?:src\/|lib\/|tests\/|\.ts|\.js|\.json|\.md)\b/.test(trimmed)) {
|
|
989
|
+
const path = trimmed.split(/\s+/)[0];
|
|
990
|
+
if (path && path.includes("/"))
|
|
991
|
+
files.add(path.slice(0, 120));
|
|
992
|
+
}
|
|
993
|
+
if (/^(?:npm|yarn|pnpm|node|git|cargo|go|python|pytest|eslint|tsc|vitest|docker|kubectl|make|curl|wget)\b/.test(trimmed)) {
|
|
994
|
+
commands.add(trimmed.slice(0, 120));
|
|
995
|
+
}
|
|
996
|
+
if (/\b(error|fail|exception|panic|timeout)\b/i.test(trimmed)) {
|
|
997
|
+
errors.add(trimmed.slice(0, 120));
|
|
998
|
+
}
|
|
999
|
+
if (/\b(decided|decision|plan|design|agreed|selected|will|should|recommend)\b/i.test(trimmed)) {
|
|
1000
|
+
decisions.add(trimmed.slice(0, 120));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (files.size === 0 && commands.size === 0 && errors.size === 0 && decisions.size === 0) {
|
|
1006
|
+
return undefined;
|
|
1007
|
+
}
|
|
1008
|
+
const parts = ["[Context summary — earlier messages omitted]"];
|
|
1009
|
+
if (files.size > 0) {
|
|
1010
|
+
parts.push(`Files: ${Array.from(files).slice(0, 10).join(", ")}${files.size > 10 ? "…" : ""}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (commands.size > 0) {
|
|
1013
|
+
parts.push(`Commands: ${Array.from(commands).slice(0, 5).join(", ")}${commands.size > 5 ? "…" : ""}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (errors.size > 0) {
|
|
1016
|
+
parts.push(`Errors: ${Array.from(errors).slice(0, 3).join("; ")}${errors.size > 3 ? "…" : ""}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (decisions.size > 0) {
|
|
1019
|
+
parts.push(`Decisions: ${Array.from(decisions).slice(0, 3).join("; ")}${decisions.size > 3 ? "…" : ""}`);
|
|
1020
|
+
}
|
|
1021
|
+
return parts.join("\n");
|
|
1022
|
+
}
|
|
1023
|
+
function applyContextCompression(entries, level, profile) {
|
|
1024
|
+
if (!level || level === "none") {
|
|
1025
|
+
return { entries, droppedCount: 0 };
|
|
1026
|
+
}
|
|
1027
|
+
// Count current messages
|
|
1028
|
+
const messageIndices = [];
|
|
1029
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1030
|
+
const e = entries[i];
|
|
1031
|
+
if (e && typeof e === "object" && e.type === "message") {
|
|
1032
|
+
messageIndices.push(i);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const currentMessageCount = messageIndices.length;
|
|
1036
|
+
let targetMessageCount;
|
|
1037
|
+
if (level === "light") {
|
|
1038
|
+
targetMessageCount = Math.max(5, Math.floor(currentMessageCount * 0.6));
|
|
1039
|
+
}
|
|
1040
|
+
else if (level === "medium") {
|
|
1041
|
+
targetMessageCount = Math.max(5, Math.floor(currentMessageCount * 0.4));
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
targetMessageCount = Math.max(3, 15);
|
|
1045
|
+
}
|
|
1046
|
+
if (currentMessageCount <= targetMessageCount) {
|
|
1047
|
+
let result = entries;
|
|
1048
|
+
if (level === "medium" || level === "aggressive") {
|
|
1049
|
+
result = stripOldSystemMessages(result, profile);
|
|
1050
|
+
}
|
|
1051
|
+
return { entries: result, droppedCount: 0 };
|
|
1052
|
+
}
|
|
1053
|
+
// Need to drop messages — prioritize based on profile score
|
|
1054
|
+
let headerEnd = 0;
|
|
1055
|
+
for (; headerEnd < entries.length; headerEnd++) {
|
|
1056
|
+
const e = entries[headerEnd];
|
|
1057
|
+
if (e && typeof e === "object") {
|
|
1058
|
+
const type = e.type;
|
|
1059
|
+
if (type === "session" || type === "header")
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
const headers = entries.slice(0, headerEnd);
|
|
1065
|
+
const rest = entries.slice(headerEnd);
|
|
1066
|
+
const scored = rest.map((entry, idx) => ({
|
|
1067
|
+
entry,
|
|
1068
|
+
originalIndex: headerEnd + idx,
|
|
1069
|
+
score: scoreMessageForProfile(entry, profile),
|
|
1070
|
+
isMessage: entry && typeof entry === "object" && entry.type === "message",
|
|
1071
|
+
}));
|
|
1072
|
+
// Separate messages from non-messages (e.g. compaction events, etc.)
|
|
1073
|
+
const messageScored = scored.filter((s) => s.isMessage);
|
|
1074
|
+
const nonMessageScored = scored.filter((s) => !s.isMessage);
|
|
1075
|
+
// Sort messages by score desc, then originalIndex desc (prefer newer)
|
|
1076
|
+
messageScored.sort((a, b) => {
|
|
1077
|
+
if (b.score !== a.score)
|
|
1078
|
+
return b.score - a.score;
|
|
1079
|
+
return b.originalIndex - a.originalIndex;
|
|
1080
|
+
});
|
|
1081
|
+
// Keep top N messages
|
|
1082
|
+
const keptMessages = messageScored.slice(0, targetMessageCount);
|
|
1083
|
+
const droppedMessages = messageScored.slice(targetMessageCount);
|
|
1084
|
+
// Combine kept messages with all non-messages, sort by original index
|
|
1085
|
+
const combined = [...nonMessageScored, ...keptMessages];
|
|
1086
|
+
combined.sort((a, b) => a.originalIndex - b.originalIndex);
|
|
1087
|
+
const droppedEntries = droppedMessages.map((d) => d.entry);
|
|
1088
|
+
const syntheticSummaryText = generateSyntheticSummary(droppedEntries);
|
|
1089
|
+
let result = [...headers, ...combined.map((c) => c.entry)];
|
|
1090
|
+
if (level === "medium" || level === "aggressive") {
|
|
1091
|
+
result = stripOldSystemMessages(result, profile);
|
|
1092
|
+
}
|
|
1093
|
+
if (syntheticSummaryText) {
|
|
1094
|
+
// Insert synthetic summary after headers but before other entries
|
|
1095
|
+
const syntheticEntry = {
|
|
1096
|
+
type: "message",
|
|
1097
|
+
message: {
|
|
1098
|
+
role: "system",
|
|
1099
|
+
content: [{ type: "text", text: syntheticSummaryText }],
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
result.splice(headerEnd, 0, syntheticEntry);
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
entries: result,
|
|
1106
|
+
droppedCount: droppedMessages.length,
|
|
1107
|
+
syntheticSummary: syntheticSummaryText,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
513
1111
|
// Batch body stripping
|
|
514
1112
|
// ---------------------------------------------------------------------------
|
|
515
1113
|
/** Headers that delimit the end of any batch tool section. */
|
|
@@ -542,8 +1140,8 @@ function isBatchSectionHeader(line) {
|
|
|
542
1140
|
/^--- \[.+\] (exit (\d+)|interrupted) ---$/.test(line) ||
|
|
543
1141
|
/^--- \[.+\] still running ---$/.test(line));
|
|
544
1142
|
}
|
|
545
|
-
/** Replace batch section bodies with first
|
|
546
|
-
function stripBatchBodies(text) {
|
|
1143
|
+
/** Replace batch section bodies with first N + last N lines as orientation. */
|
|
1144
|
+
function stripBatchBodies(text, level) {
|
|
547
1145
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
548
1146
|
const out = [];
|
|
549
1147
|
let i = 0;
|
|
@@ -559,8 +1157,24 @@ function stripBatchBodies(text) {
|
|
|
559
1157
|
}
|
|
560
1158
|
const isBash = line.includes("--- bash [") || line.includes("--- [");
|
|
561
1159
|
if (isBash) {
|
|
562
|
-
|
|
563
|
-
|
|
1160
|
+
let head;
|
|
1161
|
+
let tail;
|
|
1162
|
+
if (level === "light") {
|
|
1163
|
+
head = 15;
|
|
1164
|
+
tail = 10;
|
|
1165
|
+
}
|
|
1166
|
+
else if (level === "medium") {
|
|
1167
|
+
head = 10;
|
|
1168
|
+
tail = 5;
|
|
1169
|
+
}
|
|
1170
|
+
else if (level === "aggressive") {
|
|
1171
|
+
head = 5;
|
|
1172
|
+
tail = 3;
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
head = 30;
|
|
1176
|
+
tail = 20;
|
|
1177
|
+
}
|
|
564
1178
|
if (body.length > head + tail) {
|
|
565
1179
|
out.push(...body.slice(0, head));
|
|
566
1180
|
out.push(`[...${body.length - head - tail} lines of bash output truncated...]`);
|
|
@@ -571,10 +1185,28 @@ function stripBatchBodies(text) {
|
|
|
571
1185
|
}
|
|
572
1186
|
}
|
|
573
1187
|
else {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1188
|
+
let head;
|
|
1189
|
+
let tail;
|
|
1190
|
+
if (level === "light") {
|
|
1191
|
+
head = 2;
|
|
1192
|
+
tail = 2;
|
|
1193
|
+
}
|
|
1194
|
+
else if (level === "medium") {
|
|
1195
|
+
head = 1;
|
|
1196
|
+
tail = 1;
|
|
1197
|
+
}
|
|
1198
|
+
else if (level === "aggressive") {
|
|
1199
|
+
head = 0;
|
|
1200
|
+
tail = 0;
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
head = 3;
|
|
1204
|
+
tail = 3;
|
|
1205
|
+
}
|
|
1206
|
+
if (body.length > head + tail) {
|
|
1207
|
+
out.push(...body.slice(0, head));
|
|
1208
|
+
out.push(`[...${body.length - head - tail} lines truncated...]`);
|
|
1209
|
+
out.push(...body.slice(-tail));
|
|
578
1210
|
}
|
|
579
1211
|
else {
|
|
580
1212
|
out.push(...body);
|
|
@@ -589,7 +1221,7 @@ function stripBatchBodies(text) {
|
|
|
589
1221
|
return out.join("\n");
|
|
590
1222
|
}
|
|
591
1223
|
/** If the JSONL line is a tool/toolResult message, strip batch bodies from its text. */
|
|
592
|
-
function maybeStripBatchBodies(line) {
|
|
1224
|
+
function maybeStripBatchBodies(line, level) {
|
|
593
1225
|
// Fast path: skip non-tool messages without parsing JSON.
|
|
594
1226
|
if (!line.includes('"role":"tool"') && !line.includes('"role":"toolResult"')) {
|
|
595
1227
|
return line;
|
|
@@ -629,7 +1261,7 @@ function maybeStripBatchBodies(line) {
|
|
|
629
1261
|
if (!text || (!text.includes("\n--- ") && !text.includes("[Directive:") && !text.includes("[Hint:"))) {
|
|
630
1262
|
return line;
|
|
631
1263
|
}
|
|
632
|
-
const stripped = stripBatchBodies(text);
|
|
1264
|
+
const stripped = stripBatchBodies(text, level);
|
|
633
1265
|
const cleaned = stripDirectives(stripped);
|
|
634
1266
|
if (cleaned === text) {
|
|
635
1267
|
return line;
|