pi-smart-compact 7.5.2 → 7.7.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/README.md +492 -182
- package/dist/index.js +2979 -0
- package/package.json +5 -3
- package/src/constants.ts +0 -140
- package/src/core.ts +0 -360
- package/src/index.ts +0 -175
- package/src/phases/explore.ts +0 -371
- package/src/phases/synthesize.ts +0 -184
- package/src/phases/verify.ts +0 -191
- package/src/types.ts +0 -176
- package/src/ui/overlays.ts +0 -329
- package/src/utils/cache.ts +0 -145
- package/src/utils/damage.ts +0 -153
- package/src/utils/extraction.ts +0 -259
- package/src/utils/fingerprint.ts +0 -190
- package/src/utils/helpers.ts +0 -161
- package/src/utils/message-blocks.ts +0 -21
- package/src/utils/pruning.ts +0 -147
- package/src/utils/tokens.ts +0 -63
package/dist/index.js
ADDED
|
@@ -0,0 +1,2979 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/constants.ts
|
|
3
|
+
var VERSION = "7.7.0";
|
|
4
|
+
var CHARS_PER_TOKEN = 3.8;
|
|
5
|
+
var COMPACT_SYSTEM_PREFIX = "You are an expert conversation summarizer for a coding agent. " + "Produce structured markdown summaries. " + "Follow output format exactly. " + "Use EXACT names \u2014 never paraphrase code identifiers. " + "Trust deterministic extraction data over intuition.";
|
|
6
|
+
var PROFILES = {
|
|
7
|
+
light: {
|
|
8
|
+
summaryBudgetTokens: 1e4,
|
|
9
|
+
keepRecentTokens: 30000,
|
|
10
|
+
minChunkTokens: 800,
|
|
11
|
+
maxChunkTokens: 12000,
|
|
12
|
+
singlePassMaxTokens: 40000,
|
|
13
|
+
batchMaxTokens: 30000
|
|
14
|
+
},
|
|
15
|
+
balanced: {
|
|
16
|
+
summaryBudgetTokens: 6000,
|
|
17
|
+
keepRecentTokens: 20000,
|
|
18
|
+
minChunkTokens: 500,
|
|
19
|
+
maxChunkTokens: 8000,
|
|
20
|
+
singlePassMaxTokens: 30000,
|
|
21
|
+
batchMaxTokens: 24000
|
|
22
|
+
},
|
|
23
|
+
aggressive: {
|
|
24
|
+
summaryBudgetTokens: 3000,
|
|
25
|
+
keepRecentTokens: 1e4,
|
|
26
|
+
minChunkTokens: 300,
|
|
27
|
+
maxChunkTokens: 6000,
|
|
28
|
+
singlePassMaxTokens: 20000,
|
|
29
|
+
batchMaxTokens: 18000
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var DEFAULT_CONFIG = {
|
|
33
|
+
profile: "balanced",
|
|
34
|
+
profiles: PROFILES,
|
|
35
|
+
summaryModel: null,
|
|
36
|
+
segmentationModel: null,
|
|
37
|
+
autoTrigger: true,
|
|
38
|
+
backupEnabled: true,
|
|
39
|
+
backupDir: ""
|
|
40
|
+
};
|
|
41
|
+
var NO_OP_RE = /applied:\s*0|no changes applied|nothing to (?:do|change)|0 edits? applied/i;
|
|
42
|
+
var SHIFT_RE = /simdi|peki|bide|bi de|gecelim|bakalim|yapalim|baska|sonra|tamam simdi|now let|also|next|let's|moving on|switch to/i;
|
|
43
|
+
var CHOICE_RE = /use\s+\S+\s+(?:instead|not|rather)|don't\s+use|avoid\s+|switch\s+to\s+|go\s+with\s+|prefer\s+/i;
|
|
44
|
+
var SINGLE_PASS_PREFIX = `Summarize this coding agent conversation. Produce ONE structured summary.
|
|
45
|
+
` + `
|
|
46
|
+
Rules for Accuracy:
|
|
47
|
+
` + `1. Session Type: read-only tool calls = REVIEW, not implementation
|
|
48
|
+
` + `2. Status: Check for user complaints before marking "Done"
|
|
49
|
+
` + `3. Exact Names: Quote specific variable/function/parameter names, don't paraphrase
|
|
50
|
+
` + `4. Files: Use the VERIFIED file lists above (deterministically extracted, zero hallucination risk)
|
|
51
|
+
` + `
|
|
52
|
+
Output EXACTLY this format:
|
|
53
|
+
|
|
54
|
+
` + `## Goal
|
|
55
|
+
[What the user is trying to accomplish]
|
|
56
|
+
` + `## Constraints & Preferences
|
|
57
|
+
- [CRITICAL: user requirements, preferences, constraints]
|
|
58
|
+
` + `## Progress
|
|
59
|
+
### Done
|
|
60
|
+
- [x] [Completed tasks with file references]
|
|
61
|
+
### In Progress
|
|
62
|
+
- [ ] [Current work state]
|
|
63
|
+
### Blocked
|
|
64
|
+
- [Issues]
|
|
65
|
+
` + `## Key Decisions
|
|
66
|
+
- **[Decision]**: [Rationale]
|
|
67
|
+
` + `## Files Modified
|
|
68
|
+
- [Verified list from deterministic extraction]
|
|
69
|
+
` + `## Files Read
|
|
70
|
+
- [Verified list from deterministic extraction]
|
|
71
|
+
` + `## Next Steps
|
|
72
|
+
1. [What should happen next]
|
|
73
|
+
` + `## Critical Context
|
|
74
|
+
- [Specific data, patterns, info needed to continue]
|
|
75
|
+
- [Error patterns or gotchas]
|
|
76
|
+
` + `## Topics Covered
|
|
77
|
+
[Chronological bullet list with priority in brackets]
|
|
78
|
+
`;
|
|
79
|
+
var SINGLE_PASS_SUFFIX = `
|
|
80
|
+
{PREV_CONTEXT}
|
|
81
|
+
|
|
82
|
+
{EXTRACTION_CONTEXT}
|
|
83
|
+
|
|
84
|
+
{EXPLORATION_CONTEXT}
|
|
85
|
+
|
|
86
|
+
<conversation>
|
|
87
|
+
{CONVERSATION}
|
|
88
|
+
</conversation>`;
|
|
89
|
+
var BATCH_PROMPT_PREFIX = `Summarize these conversation segments.
|
|
90
|
+
|
|
91
|
+
Rules for Accuracy:
|
|
92
|
+
` + `1. Use EXACT file paths from extraction data
|
|
93
|
+
` + `2. Status: only mark "done" if there's clear evidence (successful test run, user confirmation)
|
|
94
|
+
` + `3. Quote specific values, don't paraphrase code
|
|
95
|
+
|
|
96
|
+
` + `For EACH segment produce:
|
|
97
|
+
` + `### {TOPIC_NAME}
|
|
98
|
+
` + `**Priority**: [critical|high|normal|low]
|
|
99
|
+
` + `**Summary**: [2-4 sentences: what happened, errors, code changes with paths]
|
|
100
|
+
` + `**Decisions**: [comma-separated, or "None"]
|
|
101
|
+
` + `**Modified**: [comma-separated paths, or "None"]
|
|
102
|
+
` + `**Read**: [comma-separated paths, or "None"]
|
|
103
|
+
`;
|
|
104
|
+
var BATCH_PROMPT_SUFFIX = `
|
|
105
|
+
{EXTRACTION_CONTEXT}
|
|
106
|
+
|
|
107
|
+
<segments>
|
|
108
|
+
{TEXT}
|
|
109
|
+
</segments>`;
|
|
110
|
+
var ASSEMBLY_PROMPT_PREFIX = `Merge these topic summaries into ONE coherent summary.
|
|
111
|
+
|
|
112
|
+
` + `## IMMUTABLE CONTEXT (do not modify or contradict these facts)
|
|
113
|
+
` + `These are deterministically verified from the original conversation. They take priority over ANY summary content below.
|
|
114
|
+
|
|
115
|
+
` + `Rules:
|
|
116
|
+
` + `1. Preserve ALL critical/high info. Condense normal, minimize low.
|
|
117
|
+
` + `2. Chronological order.
|
|
118
|
+
` + `3. The pre-processed data below is GROUND TRUTH \u2014 trust it over individual summaries.
|
|
119
|
+
` + `4. Files Modified list is deterministically verified \u2014 if a summary says a file was modified but it's NOT in the list above, omit it.
|
|
120
|
+
` + `5. Key Decisions below are verified \u2014 preserve them exactly, do not paraphrase the decision text.
|
|
121
|
+
` + `6. Do NOT fabricate file paths, function names, or error messages not present in the verified data.
|
|
122
|
+
|
|
123
|
+
` + `Format:
|
|
124
|
+
` + `## Goal
|
|
125
|
+
[Overall objective]
|
|
126
|
+
` + `## Constraints & Preferences
|
|
127
|
+
- [CRITICAL requirements, preferences, constraints]
|
|
128
|
+
` + `## Progress
|
|
129
|
+
### Done
|
|
130
|
+
- [x] [Completed tasks with file refs]
|
|
131
|
+
### In Progress
|
|
132
|
+
- [ ] [Current work state]
|
|
133
|
+
### Blocked
|
|
134
|
+
- [Issues]
|
|
135
|
+
` + `## Key Decisions
|
|
136
|
+
- **[Decision]**: [Rationale]
|
|
137
|
+
` + `## Files Modified
|
|
138
|
+
- [Verified deterministic list]
|
|
139
|
+
` + `## Files Read
|
|
140
|
+
- [Verified deterministic list]
|
|
141
|
+
` + `## Next Steps
|
|
142
|
+
1. [What should happen next]
|
|
143
|
+
` + `## Critical Context
|
|
144
|
+
- [Data, patterns, info needed]
|
|
145
|
+
` + `## Topics Covered
|
|
146
|
+
[Chronological bullets with priority]
|
|
147
|
+
`;
|
|
148
|
+
var ASSEMBLY_PROMPT_SUFFIX = `
|
|
149
|
+
IMMUTABLE CONTEXT (verified deterministic data):
|
|
150
|
+
- Key Decisions: {DECISIONS}
|
|
151
|
+
- Files Modified (VERIFIED): {MODIFIED}
|
|
152
|
+
- Files Read (VERIFIED): {READ}
|
|
153
|
+
|
|
154
|
+
{EXPLORATION_CONTEXT}
|
|
155
|
+
{PREV_CONTEXT}
|
|
156
|
+
|
|
157
|
+
<summaries>{SUMMARIES}</summaries>`;
|
|
158
|
+
var SESSION_TYPE_INSTRUCTIONS = {
|
|
159
|
+
debugging: "Focus on: error chains, root cause analysis, attempted fixes, resolution status. Prioritize error messages and stack traces. Mark files as Done only if all errors resolved.",
|
|
160
|
+
implementation: "Focus on: files created/modified, architectural decisions, feature completeness, test coverage. Prioritize code changes with exact paths.",
|
|
161
|
+
review: "Focus on: files read, issues found, recommendations, approval status. Prioritize findings over changes. Read-only tool calls = REVIEW, not implementation.",
|
|
162
|
+
discussion: "Focus on: decisions made, trade-offs discussed, consensus reached. Prioritize rationale over implementation details."
|
|
163
|
+
};
|
|
164
|
+
var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have deterministic extraction data and can query the raw conversation using tools.
|
|
165
|
+
|
|
166
|
+
` + `Your job:
|
|
167
|
+
` + `1. Verify/enrich the extracted boundaries (merge, split, or add as needed)
|
|
168
|
+
` + `2. Identify cross-topic relationships
|
|
169
|
+
` + `3. Find implicit constraints (user tone, frustration, urgency)
|
|
170
|
+
` + `4. Assess completion status accurately
|
|
171
|
+
` + `5. Extract the narrative arc
|
|
172
|
+
|
|
173
|
+
` + `Use tools BEFORE forming conclusions. You may make up to 8 tool calls.
|
|
174
|
+
|
|
175
|
+
` + `After exploration, output ONLY a JSON object (no markdown):
|
|
176
|
+
` + '{"boundaries":[{"afterIndex":N,"topic":"...","priority":"critical|high|normal|low","confidence":0.0-1.0}],"mainGoal":"...","sessionType":"implementation|review|debugging|discussion","enrichedConstraints":[...],"crossReferences":[...],"statusAssessment":{"done":[...],"inProgress":[...],"blocked":[...]},"criticalContext":[...],"keyDecisions":[...]}';
|
|
177
|
+
|
|
178
|
+
// src/utils/helpers.ts
|
|
179
|
+
import fs from "fs";
|
|
180
|
+
import path from "path";
|
|
181
|
+
import crypto from "crypto";
|
|
182
|
+
var _cfg = null;
|
|
183
|
+
var _cfgMtime = 0;
|
|
184
|
+
function loadConfig() {
|
|
185
|
+
try {
|
|
186
|
+
const p = path.join(process.env.HOME ?? "/tmp", ".pi/agent/settings.json");
|
|
187
|
+
const stat = fs.statSync(p);
|
|
188
|
+
if (_cfg && stat.mtimeMs === _cfgMtime)
|
|
189
|
+
return _cfg;
|
|
190
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
191
|
+
const sc = raw.smartCompact ?? raw.semanticCompact ?? {};
|
|
192
|
+
const merged = { ...DEFAULT_CONFIG, ...sc };
|
|
193
|
+
if (sc.profiles)
|
|
194
|
+
merged.profiles = { ...PROFILES, ...sc.profiles };
|
|
195
|
+
if (!merged.backupDir)
|
|
196
|
+
merged.backupDir = path.join(process.env.HOME ?? "/tmp", ".pi/agent/compact-backups");
|
|
197
|
+
_cfg = merged;
|
|
198
|
+
_cfgMtime = stat.mtimeMs;
|
|
199
|
+
return _cfg;
|
|
200
|
+
} catch {
|
|
201
|
+
return { ...DEFAULT_CONFIG, backupDir: path.join(process.env.HOME ?? "/tmp", ".pi/agent/compact-backups") };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function backupConversation(convText, sessionId) {
|
|
205
|
+
try {
|
|
206
|
+
const cfg = loadConfig();
|
|
207
|
+
if (!cfg.backupEnabled)
|
|
208
|
+
return null;
|
|
209
|
+
const dir = cfg.backupDir;
|
|
210
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
211
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
212
|
+
const hash = crypto.createHash("sha256").update(convText).digest("hex").slice(0, 8);
|
|
213
|
+
const fp = path.join(dir, sessionId + "-" + ts + "-" + hash + ".md");
|
|
214
|
+
fs.writeFileSync(fp, `# Smart Compact Backup
|
|
215
|
+
# Date: ` + new Date().toISOString() + `
|
|
216
|
+
# Session: ` + sessionId + `
|
|
217
|
+
|
|
218
|
+
` + convText);
|
|
219
|
+
return fp;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function getPreviousCompactionContext(branch) {
|
|
225
|
+
const compactions = branch.filter((e) => e.type === "compaction");
|
|
226
|
+
if (!compactions.length)
|
|
227
|
+
return "";
|
|
228
|
+
const last = compactions[compactions.length - 1];
|
|
229
|
+
const topics = last.details?.topics ?? [];
|
|
230
|
+
if (!topics.length)
|
|
231
|
+
return "";
|
|
232
|
+
return `
|
|
233
|
+
[IMPORTANT: Previous compaction exists (` + (last.details?.method ?? "unknown") + "). Already summarized topics: " + topics.join(", ") + ". Build upon this, don't re-summarize the same content.]";
|
|
234
|
+
}
|
|
235
|
+
function smartKeepBoundary(msgs, keepFromIndex) {
|
|
236
|
+
if (keepFromIndex <= 0 || keepFromIndex >= msgs.length)
|
|
237
|
+
return keepFromIndex;
|
|
238
|
+
const last = msgs[keepFromIndex - 1];
|
|
239
|
+
const first = msgs[keepFromIndex];
|
|
240
|
+
if (last && first) {
|
|
241
|
+
const lastText = JSON.stringify(last.message).toLowerCase();
|
|
242
|
+
const keptText = JSON.stringify(first.message).toLowerCase();
|
|
243
|
+
const fileRe = /(?:path|file)=["']([^"']+)["']/g;
|
|
244
|
+
const lastFiles = new Set([...lastText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
|
|
245
|
+
fileRe.lastIndex = 0;
|
|
246
|
+
const keptFiles = new Set([...keptText.matchAll(fileRe)].map((m) => m[1].split("/").pop()));
|
|
247
|
+
if ([...lastFiles].filter((f) => keptFiles.has(f)).length > 0)
|
|
248
|
+
return keepFromIndex - 1;
|
|
249
|
+
}
|
|
250
|
+
return keepFromIndex;
|
|
251
|
+
}
|
|
252
|
+
function createBatches(chunks, maxTokens) {
|
|
253
|
+
const batches = [];
|
|
254
|
+
let batch = [], bt = 0;
|
|
255
|
+
for (const ch of chunks) {
|
|
256
|
+
if (batch.length && bt + ch.tokenEstimate > maxTokens) {
|
|
257
|
+
batches.push(batch);
|
|
258
|
+
batch = [];
|
|
259
|
+
bt = 0;
|
|
260
|
+
}
|
|
261
|
+
batch.push(ch);
|
|
262
|
+
bt += ch.tokenEstimate;
|
|
263
|
+
}
|
|
264
|
+
if (batch.length)
|
|
265
|
+
batches.push(batch);
|
|
266
|
+
return batches;
|
|
267
|
+
}
|
|
268
|
+
function allocateTopicBudgets(summaries, totalBudget) {
|
|
269
|
+
const n = summaries.length;
|
|
270
|
+
if (n === 0)
|
|
271
|
+
return new Map;
|
|
272
|
+
const weights = summaries.map((s, i) => {
|
|
273
|
+
let w = 1;
|
|
274
|
+
if (s.priority === "critical")
|
|
275
|
+
w *= 2;
|
|
276
|
+
else if (s.priority === "high")
|
|
277
|
+
w *= 1.5;
|
|
278
|
+
else if (s.priority === "low")
|
|
279
|
+
w *= 0.6;
|
|
280
|
+
const errorKeywords = (s.summary.match(/error|fail|bug|fix|crash|exception/gi) ?? []).length;
|
|
281
|
+
w *= 1 + errorKeywords * 0.2;
|
|
282
|
+
const recency = (i + 1) / n;
|
|
283
|
+
w *= 0.6 + recency * 0.4;
|
|
284
|
+
if (s.keyDecisions.length > 0)
|
|
285
|
+
w *= 1.3;
|
|
286
|
+
return w;
|
|
287
|
+
});
|
|
288
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
289
|
+
const baseTokensPerTopic = Math.floor(totalBudget / n);
|
|
290
|
+
const budgetMap = new Map;
|
|
291
|
+
for (let i = 0;i < summaries.length; i++) {
|
|
292
|
+
const allocated = Math.round(baseTokensPerTopic * (weights[i] / (totalWeight / n)));
|
|
293
|
+
budgetMap.set(summaries[i].topic, Math.max(200, allocated));
|
|
294
|
+
}
|
|
295
|
+
return budgetMap;
|
|
296
|
+
}
|
|
297
|
+
function preProcessSummaries(summaries, budgetTokens) {
|
|
298
|
+
const topicBudgets = budgetTokens ? allocateTopicBudgets(summaries, budgetTokens) : null;
|
|
299
|
+
return {
|
|
300
|
+
decisions: [...new Set(summaries.flatMap((s) => s.keyDecisions))],
|
|
301
|
+
modified: [...new Set(summaries.flatMap((s) => s.filesModified))].sort(),
|
|
302
|
+
read: [...new Set(summaries.flatMap((s) => s.filesRead))].sort(),
|
|
303
|
+
text: summaries.map((cs, i) => {
|
|
304
|
+
const budgetHint = topicBudgets?.get(cs.topic);
|
|
305
|
+
const budgetLine = budgetHint ? `
|
|
306
|
+
Budget: ~` + budgetHint + " tokens" : "";
|
|
307
|
+
return "### Segment " + (i + 1) + ": " + cs.topic + `
|
|
308
|
+
Priority: ` + cs.priority + " | msgs " + cs.startIndex + "-" + cs.endIndex + budgetLine + `
|
|
309
|
+
|
|
310
|
+
` + cs.summary + `
|
|
311
|
+
|
|
312
|
+
Decisions: ` + (cs.keyDecisions.join("; ") || "None") + `
|
|
313
|
+
Modified: ` + (cs.filesModified.join(", ") || "None") + `
|
|
314
|
+
Read: ` + (cs.filesRead.join(", ") || "None");
|
|
315
|
+
}).join(`
|
|
316
|
+
---
|
|
317
|
+
`)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function buildExtractionContext(extraction, forRange) {
|
|
321
|
+
const files = forRange ? extraction.modifiedFiles.filter((f) => f.lastModifiedIndex >= forRange.start && f.lastModifiedIndex <= forRange.end) : extraction.modifiedFiles;
|
|
322
|
+
const errors = forRange ? extraction.errors.filter((e) => e.index >= forRange.start && e.index <= forRange.end) : extraction.errors;
|
|
323
|
+
return [
|
|
324
|
+
"## Deterministic Extraction (verified facts)",
|
|
325
|
+
"Files modified: " + (files.map((f) => f.path).join(", ") || "none"),
|
|
326
|
+
"Errors: " + (errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " \u2713" : "")).join("; ") || "none"),
|
|
327
|
+
"Decisions: " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 60)).join("; ") || "none"),
|
|
328
|
+
"Constraints: " + (extraction.constraints.map((c) => "[" + c.category + "] " + c.text.slice(0, 60)).join("; ") || "none")
|
|
329
|
+
].join(`
|
|
330
|
+
`);
|
|
331
|
+
}
|
|
332
|
+
function buildExplorationContext(report) {
|
|
333
|
+
if (!report.mainGoal && !report.crossReferences.length && !report.enrichedConstraints.length)
|
|
334
|
+
return "";
|
|
335
|
+
return [
|
|
336
|
+
"## Exploration Report",
|
|
337
|
+
"Main goal: " + report.mainGoal,
|
|
338
|
+
"Session type: " + report.sessionType,
|
|
339
|
+
report.crossReferences.length ? "Cross-references: " + report.crossReferences.join("; ") : "",
|
|
340
|
+
report.enrichedConstraints.length ? "Enriched constraints: " + report.enrichedConstraints.join("; ") : "",
|
|
341
|
+
report.statusAssessment.done.length ? "Assessed done: " + report.statusAssessment.done.join("; ") : "",
|
|
342
|
+
report.statusAssessment.inProgress.length ? "Assessed in-progress: " + report.statusAssessment.inProgress.join("; ") : "",
|
|
343
|
+
report.criticalContext.length ? "Critical context: " + report.criticalContext.join("; ") : ""
|
|
344
|
+
].filter(Boolean).join(`
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/core.ts
|
|
349
|
+
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
350
|
+
|
|
351
|
+
// src/utils/tokens.ts
|
|
352
|
+
var PROVIDER_MAP = {
|
|
353
|
+
"zai-anthropic": {
|
|
354
|
+
maxOutputTokens: 8192,
|
|
355
|
+
supportsTools: "probe",
|
|
356
|
+
jsonReliability: "high",
|
|
357
|
+
instructionFollowing: "high",
|
|
358
|
+
tokenRatioEstimate: 3.5,
|
|
359
|
+
concurrencyLimit: 3,
|
|
360
|
+
cacheStrategy: "anthropic"
|
|
361
|
+
},
|
|
362
|
+
minimax: {
|
|
363
|
+
maxOutputTokens: 4096,
|
|
364
|
+
supportsTools: "probe",
|
|
365
|
+
jsonReliability: "medium",
|
|
366
|
+
instructionFollowing: "medium",
|
|
367
|
+
tokenRatioEstimate: 3.8,
|
|
368
|
+
concurrencyLimit: 2,
|
|
369
|
+
cacheStrategy: "anthropic"
|
|
370
|
+
},
|
|
371
|
+
"xiaomi-token-plan": {
|
|
372
|
+
maxOutputTokens: 8192,
|
|
373
|
+
supportsTools: "probe",
|
|
374
|
+
jsonReliability: "medium",
|
|
375
|
+
instructionFollowing: "medium",
|
|
376
|
+
tokenRatioEstimate: 3.3,
|
|
377
|
+
concurrencyLimit: 2,
|
|
378
|
+
cacheStrategy: "openai"
|
|
379
|
+
},
|
|
380
|
+
openai: {
|
|
381
|
+
maxOutputTokens: 16384,
|
|
382
|
+
supportsTools: true,
|
|
383
|
+
jsonReliability: "high",
|
|
384
|
+
instructionFollowing: "high",
|
|
385
|
+
tokenRatioEstimate: 4,
|
|
386
|
+
concurrencyLimit: 5,
|
|
387
|
+
cacheStrategy: "openai"
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function getProviderCaps(provider) {
|
|
391
|
+
return PROVIDER_MAP[provider] ?? {
|
|
392
|
+
maxOutputTokens: 8192,
|
|
393
|
+
supportsTools: "probe",
|
|
394
|
+
jsonReliability: "medium",
|
|
395
|
+
instructionFollowing: "medium",
|
|
396
|
+
tokenRatioEstimate: 3.8,
|
|
397
|
+
concurrencyLimit: 2,
|
|
398
|
+
cacheStrategy: "none"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
var _calibrationFactors = new Map;
|
|
402
|
+
function getCalibrationFactor(provider) {
|
|
403
|
+
if (!provider)
|
|
404
|
+
return 1;
|
|
405
|
+
return _calibrationFactors.get(provider) ?? 1;
|
|
406
|
+
}
|
|
407
|
+
function estimateTokens(text, provider) {
|
|
408
|
+
const baseRatio = provider ? getProviderCaps(provider).tokenRatioEstimate : CHARS_PER_TOKEN;
|
|
409
|
+
const jsonPenalty = text.startsWith("[") || text.startsWith("{") ? 0.85 : 1;
|
|
410
|
+
const langPenalty = /[\u00E7\u011F\u0131\u00F6\u015F\u00FC\u00C7\u011E\u0130\u00D6\u015E\u00DC]/.test(text) ? 0.9 : 1;
|
|
411
|
+
const calibration = getCalibrationFactor(provider);
|
|
412
|
+
return Math.ceil(text.length / baseRatio * jsonPenalty * langPenalty * calibration);
|
|
413
|
+
}
|
|
414
|
+
function calibrateFromResponse(estimated, actual, provider) {
|
|
415
|
+
if (actual > 0 && estimated > 0 && provider) {
|
|
416
|
+
const prev = _calibrationFactors.get(provider) ?? 1;
|
|
417
|
+
const sample = actual / estimated;
|
|
418
|
+
_calibrationFactors.set(provider, prev * 0.7 + sample * 0.3);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/utils/cache.ts
|
|
423
|
+
import fs2 from "fs";
|
|
424
|
+
import path2 from "path";
|
|
425
|
+
import crypto2 from "crypto";
|
|
426
|
+
import { complete } from "@earendil-works/pi-ai";
|
|
427
|
+
var CACHE_DIR = path2.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache");
|
|
428
|
+
var _compactSessionId = null;
|
|
429
|
+
function getCompactSessionId() {
|
|
430
|
+
if (!_compactSessionId) {
|
|
431
|
+
_compactSessionId = "sc-" + Date.now().toString(36) + "-" + crypto2.randomBytes(4).toString("hex");
|
|
432
|
+
}
|
|
433
|
+
return _compactSessionId;
|
|
434
|
+
}
|
|
435
|
+
function resetCompactSessionId() {
|
|
436
|
+
_compactSessionId = null;
|
|
437
|
+
}
|
|
438
|
+
function cacheOpts(opts) {
|
|
439
|
+
const retention = opts.cacheRetention ?? "short";
|
|
440
|
+
if (retention === "none") {
|
|
441
|
+
return { ...opts, cacheRetention: "none" };
|
|
442
|
+
}
|
|
443
|
+
return { ...opts, sessionId: getCompactSessionId(), cacheRetention: "short" };
|
|
444
|
+
}
|
|
445
|
+
var _metrics = [];
|
|
446
|
+
function resetMetrics() {
|
|
447
|
+
_metrics.length = 0;
|
|
448
|
+
}
|
|
449
|
+
function recordMetric(m) {
|
|
450
|
+
_metrics.push(m);
|
|
451
|
+
if (_metrics.length > 200)
|
|
452
|
+
_metrics.splice(0, _metrics.length - 100);
|
|
453
|
+
}
|
|
454
|
+
function getMetricsSummary() {
|
|
455
|
+
const n = _metrics.length;
|
|
456
|
+
if (!n)
|
|
457
|
+
return { totalCalls: 0, totalInput: 0, totalOutput: 0, totalCacheHit: 0, avgLatency: 0, cacheHitRate: 0 };
|
|
458
|
+
const totalInput = _metrics.reduce((s, m) => s + m.inputTokens, 0);
|
|
459
|
+
const totalOutput = _metrics.reduce((s, m) => s + m.outputTokens, 0);
|
|
460
|
+
const totalCacheHit = _metrics.reduce((s, m) => s + m.cacheHitTokens, 0);
|
|
461
|
+
const avgLatency = _metrics.reduce((s, m) => s + m.latencyMs, 0) / n;
|
|
462
|
+
return {
|
|
463
|
+
totalCalls: n,
|
|
464
|
+
totalInput,
|
|
465
|
+
totalOutput,
|
|
466
|
+
totalCacheHit,
|
|
467
|
+
avgLatency: Math.round(avgLatency),
|
|
468
|
+
cacheHitRate: totalInput > 0 ? totalCacheHit / totalInput : 0
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
async function trackedComplete(phase, model, reqBody, opts) {
|
|
472
|
+
const start = Date.now();
|
|
473
|
+
try {
|
|
474
|
+
const resp = await complete(model, reqBody, opts);
|
|
475
|
+
const latency = Date.now() - start;
|
|
476
|
+
const usage = resp.usage;
|
|
477
|
+
const inputT = usage?.input ?? 0;
|
|
478
|
+
const outputT = usage?.output ?? 0;
|
|
479
|
+
const cacheT = usage?.cacheRead ?? 0;
|
|
480
|
+
recordMetric({
|
|
481
|
+
phase,
|
|
482
|
+
model: model.id,
|
|
483
|
+
inputTokens: inputT,
|
|
484
|
+
outputTokens: outputT,
|
|
485
|
+
cacheHitTokens: cacheT,
|
|
486
|
+
latencyMs: latency,
|
|
487
|
+
success: true
|
|
488
|
+
});
|
|
489
|
+
if (inputT > 0 && "messages" in reqBody) {
|
|
490
|
+
const rawText = JSON.stringify(reqBody.messages);
|
|
491
|
+
calibrateFromResponse(estimateTokens(rawText), inputT, model.provider);
|
|
492
|
+
}
|
|
493
|
+
return resp;
|
|
494
|
+
} catch (err) {
|
|
495
|
+
recordMetric({
|
|
496
|
+
phase,
|
|
497
|
+
model: model.id,
|
|
498
|
+
inputTokens: 0,
|
|
499
|
+
outputTokens: 0,
|
|
500
|
+
cacheHitTokens: 0,
|
|
501
|
+
latencyMs: Date.now() - start,
|
|
502
|
+
success: false
|
|
503
|
+
});
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function getCachePath(sessionId) {
|
|
508
|
+
return path2.join(CACHE_DIR, "compact-extraction-" + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
|
|
509
|
+
}
|
|
510
|
+
function saveCachedExtraction(sessionId, extraction, msgCount) {
|
|
511
|
+
try {
|
|
512
|
+
if (!fs2.existsSync(CACHE_DIR))
|
|
513
|
+
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
514
|
+
const cached = {
|
|
515
|
+
lastMessageIndex: msgCount - 1,
|
|
516
|
+
extraction,
|
|
517
|
+
messageCount: msgCount,
|
|
518
|
+
timestamp: Date.now()
|
|
519
|
+
};
|
|
520
|
+
fs2.writeFileSync(getCachePath(sessionId), JSON.stringify(cached));
|
|
521
|
+
} catch {}
|
|
522
|
+
}
|
|
523
|
+
function loadCachedExtraction(sessionId) {
|
|
524
|
+
try {
|
|
525
|
+
const fp = getCachePath(sessionId);
|
|
526
|
+
if (!fs2.existsSync(fp))
|
|
527
|
+
return null;
|
|
528
|
+
const cached = JSON.parse(fs2.readFileSync(fp, "utf8"));
|
|
529
|
+
if (Date.now() - cached.timestamp > 3600000)
|
|
530
|
+
return null;
|
|
531
|
+
return cached;
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function mergeExtractions(base, delta, baseMsgCount) {
|
|
537
|
+
return {
|
|
538
|
+
modifiedFiles: [...base.modifiedFiles, ...delta.modifiedFiles],
|
|
539
|
+
readFiles: [...new Set([...base.readFiles, ...delta.readFiles])],
|
|
540
|
+
deletedFiles: [...new Set([...base.deletedFiles, ...delta.deletedFiles])],
|
|
541
|
+
errors: [...base.errors, ...delta.errors],
|
|
542
|
+
decisions: [...base.decisions, ...delta.decisions],
|
|
543
|
+
constraints: [...base.constraints, ...delta.constraints],
|
|
544
|
+
topics: [...base.topics, ...delta.topics],
|
|
545
|
+
timeline: [...base.timeline, ...delta.timeline],
|
|
546
|
+
mainGoal: delta.mainGoal ?? base.mainGoal,
|
|
547
|
+
lastUserMessages: delta.lastUserMessages.length > 0 ? delta.lastUserMessages : base.lastUserMessages,
|
|
548
|
+
lastErrors: delta.lastErrors.length > 0 ? delta.lastErrors : base.lastErrors,
|
|
549
|
+
messageCount: baseMsgCount + delta.messageCount
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function appendMetricsLog(sessionId) {
|
|
553
|
+
try {
|
|
554
|
+
if (!fs2.existsSync(CACHE_DIR))
|
|
555
|
+
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
556
|
+
const logPath = path2.join(CACHE_DIR, "compact-metrics.jsonl");
|
|
557
|
+
const entry = { ts: new Date().toISOString(), sessionId, ...getMetricsSummary() };
|
|
558
|
+
fs2.appendFileSync(logPath, JSON.stringify(entry) + `
|
|
559
|
+
`);
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/utils/extraction.ts
|
|
564
|
+
import path3 from "path";
|
|
565
|
+
|
|
566
|
+
// src/types.ts
|
|
567
|
+
function isToolCallBlock(c) {
|
|
568
|
+
return typeof c === "object" && c !== null && c.type === "toolCall" && typeof c.name === "string";
|
|
569
|
+
}
|
|
570
|
+
function getToolCallNames(content) {
|
|
571
|
+
if (!Array.isArray(content))
|
|
572
|
+
return [];
|
|
573
|
+
return content.filter(isToolCallBlock).map((b) => b.name);
|
|
574
|
+
}
|
|
575
|
+
function filterToolCalls(content) {
|
|
576
|
+
if (!Array.isArray(content))
|
|
577
|
+
return [];
|
|
578
|
+
return content.filter(isToolCallBlock);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/utils/extraction.ts
|
|
582
|
+
function extractText(content) {
|
|
583
|
+
if (typeof content === "string")
|
|
584
|
+
return content;
|
|
585
|
+
if (Array.isArray(content))
|
|
586
|
+
return content.map((b) => {
|
|
587
|
+
if (typeof b === "string")
|
|
588
|
+
return b;
|
|
589
|
+
if (b?.type === "text")
|
|
590
|
+
return b.text ?? "";
|
|
591
|
+
return "";
|
|
592
|
+
}).join("");
|
|
593
|
+
return "";
|
|
594
|
+
}
|
|
595
|
+
function buildToolCallIndex(msgs) {
|
|
596
|
+
const idx = new Map;
|
|
597
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
598
|
+
const m = msgs[i];
|
|
599
|
+
if (m.role !== "assistant")
|
|
600
|
+
continue;
|
|
601
|
+
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
602
|
+
for (const b of blocks) {
|
|
603
|
+
if (isToolCallBlock(b) && b.id) {
|
|
604
|
+
idx.set(b.id, { name: b.name, arguments: b.arguments, msgIndex: i });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return idx;
|
|
609
|
+
}
|
|
610
|
+
function trackFileOps(msgs) {
|
|
611
|
+
const tcIdx = buildToolCallIndex(msgs);
|
|
612
|
+
const modMap = new Map;
|
|
613
|
+
const readSet = new Set;
|
|
614
|
+
const delSet = new Set;
|
|
615
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
616
|
+
const m = msgs[i];
|
|
617
|
+
if (m.role !== "toolResult" || m.isError)
|
|
618
|
+
continue;
|
|
619
|
+
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
620
|
+
if (!tc)
|
|
621
|
+
continue;
|
|
622
|
+
const args = tc.arguments;
|
|
623
|
+
const filePath = args?.path ?? args?.file_path ?? args?.filePath;
|
|
624
|
+
if (!filePath)
|
|
625
|
+
continue;
|
|
626
|
+
const tool = tc.name.toLowerCase();
|
|
627
|
+
if (tool.includes("write") || tool.includes("edit")) {
|
|
628
|
+
const resultText = extractText(m.content);
|
|
629
|
+
if (!NO_OP_RE.test(resultText)) {
|
|
630
|
+
const existing = modMap.get(filePath);
|
|
631
|
+
modMap.set(filePath, { toolCalls: (existing?.toolCalls ?? 0) + 1, lastIdx: i });
|
|
632
|
+
}
|
|
633
|
+
} else if (tool.includes("delete") || tool.includes("remove")) {
|
|
634
|
+
delSet.add(filePath);
|
|
635
|
+
} else if (tool.includes("read")) {
|
|
636
|
+
readSet.add(filePath);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
modified: [...modMap.entries()].map(([p, d]) => ({ path: p, toolCalls: d.toolCalls, lastModifiedIndex: d.lastIdx })),
|
|
641
|
+
read: [...readSet],
|
|
642
|
+
deleted: [...delSet]
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function catalogErrors(msgs) {
|
|
646
|
+
const tcIdx = buildToolCallIndex(msgs);
|
|
647
|
+
const errors = [];
|
|
648
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
649
|
+
const m = msgs[i];
|
|
650
|
+
if (m.role !== "toolResult")
|
|
651
|
+
continue;
|
|
652
|
+
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
653
|
+
if (m.isError) {
|
|
654
|
+
errors.push({ index: i, tool: tc?.name ?? "unknown", message: extractText(m.content).slice(0, 500), retryAttempted: false, resolved: false });
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (tc?.name === "bash") {
|
|
658
|
+
const txt = extractText(m.content);
|
|
659
|
+
const isLikelyError = /(?:command not found|no such file|permission denied|syntax error|cannot find|module not found|compilation error|build failed|test failed)/i.test(txt);
|
|
660
|
+
if (isLikelyError && txt.length < 2000) {
|
|
661
|
+
errors.push({ index: i, tool: "bash", message: txt.slice(0, 300), retryAttempted: false, resolved: false });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const err of errors) {
|
|
666
|
+
for (let j = err.index + 1;j < Math.min(msgs.length, err.index + 6); j++) {
|
|
667
|
+
if (msgs[j]?.role === "assistant") {
|
|
668
|
+
const blocks = Array.isArray(msgs[j]?.content) ? msgs[j].content : [];
|
|
669
|
+
for (const b of blocks) {
|
|
670
|
+
if (isToolCallBlock(b) && b.name === err.tool) {
|
|
671
|
+
err.retryAttempted = true;
|
|
672
|
+
for (let k = j + 1;k < Math.min(msgs.length, j + 10); k++) {
|
|
673
|
+
if (msgs[k]?.role === "toolResult" && msgs[k]?.toolCallId === b.id && !msgs[k]?.isError) {
|
|
674
|
+
err.resolved = true;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (err.retryAttempted)
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return errors;
|
|
687
|
+
}
|
|
688
|
+
function extractDecisions(msgs) {
|
|
689
|
+
const tcIdx = buildToolCallIndex(msgs);
|
|
690
|
+
const decisions = [];
|
|
691
|
+
for (const [id, tc] of tcIdx) {
|
|
692
|
+
if (tc.name !== "ask_user")
|
|
693
|
+
continue;
|
|
694
|
+
const args = tc.arguments;
|
|
695
|
+
const question = typeof args === "string" ? args : args?.question ?? args?.prompt ?? "";
|
|
696
|
+
if (!question)
|
|
697
|
+
continue;
|
|
698
|
+
for (let i = tc.msgIndex + 1;i < Math.min(msgs.length, tc.msgIndex + 4); i++) {
|
|
699
|
+
if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
|
|
700
|
+
decisions.push({ index: tc.msgIndex, type: "explicit", summary: question.slice(0, 200), userResponse: extractText(msgs[i].content).slice(0, 300) });
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
706
|
+
if (msgs[i]?.role !== "user")
|
|
707
|
+
continue;
|
|
708
|
+
const txt = extractText(msgs[i].content);
|
|
709
|
+
if (CHOICE_RE.test(txt)) {
|
|
710
|
+
decisions.push({ index: i, type: "implicit", summary: txt.slice(0, 200) });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return decisions;
|
|
714
|
+
}
|
|
715
|
+
var CONSTRAINT_PATTERNS = [
|
|
716
|
+
{ re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: 0.85 },
|
|
717
|
+
{ re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: 0.8 },
|
|
718
|
+
{ re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: 0.6 },
|
|
719
|
+
{ re: /\b(?:kritik|kritikal|\u00f6nemli|onemli|\u015fart|sart|zorunlu|\u015fart ko\u015ful|\u00f6nemli \u015fart|kesinlikle|kesinlikle \u015fart|asla|sak\u0131n|sak\u0131nha|bunu yapma|b\u00f6yle olsun|b\u00f6yle yap\u0131n|\u015f\u00f6yle olsun|\u015f\u00f6yle yap\u0131n)\b/iu, cat: "requirement", conf: 0.8 },
|
|
720
|
+
{ re: /\b(?:yapma|kullanma|sak\u0131n|asla\s+(?:kullanma|yapma|getirme))\b/iu, cat: "prohibition", conf: 0.8 },
|
|
721
|
+
{ re: /\b(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)\b/iu, cat: "preference", conf: 0.6 }
|
|
722
|
+
];
|
|
723
|
+
function mineConstraints(msgs) {
|
|
724
|
+
const constraints = [];
|
|
725
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
726
|
+
if (msgs[i]?.role !== "user")
|
|
727
|
+
continue;
|
|
728
|
+
const txt = extractText(msgs[i].content);
|
|
729
|
+
if (txt.length < 10 || txt.startsWith("/"))
|
|
730
|
+
continue;
|
|
731
|
+
for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
|
|
732
|
+
if (re.test(txt)) {
|
|
733
|
+
constraints.push({ index: i, text: txt.slice(0, 300), category: cat, confidence: conf });
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return constraints;
|
|
739
|
+
}
|
|
740
|
+
function segmentTopicsHeuristic(msgs, pc, maxSegs = 20) {
|
|
741
|
+
const topics = [];
|
|
742
|
+
let startIdx = 0, tokenAcc = 0, lastFile = null, errAcc = 0;
|
|
743
|
+
const tcIdx = buildToolCallIndex(msgs);
|
|
744
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
745
|
+
const m = msgs[i];
|
|
746
|
+
const txt = extractText(m.content);
|
|
747
|
+
tokenAcc += estimateTokens(txt);
|
|
748
|
+
let brk = false;
|
|
749
|
+
let type = "exploration";
|
|
750
|
+
let primaryFile = null;
|
|
751
|
+
if (m.role === "assistant") {
|
|
752
|
+
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
753
|
+
for (const b of blocks) {
|
|
754
|
+
if (isToolCallBlock(b)) {
|
|
755
|
+
const fp = b.arguments?.path ?? b.arguments?.file_path;
|
|
756
|
+
if (fp) {
|
|
757
|
+
const fn = path3.basename(fp);
|
|
758
|
+
if (lastFile && fn !== lastFile && tokenAcc > pc.minChunkTokens)
|
|
759
|
+
brk = true;
|
|
760
|
+
lastFile = fn;
|
|
761
|
+
primaryFile = fp;
|
|
762
|
+
if (b.name?.includes("write") || b.name?.includes("edit"))
|
|
763
|
+
type = "implementation";
|
|
764
|
+
else if (b.name?.includes("read"))
|
|
765
|
+
type = "review";
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (m.role === "toolResult" && m.isError) {
|
|
771
|
+
errAcc++;
|
|
772
|
+
type = "debugging";
|
|
773
|
+
}
|
|
774
|
+
if (m.role === "toolResult" && !m.isError) {
|
|
775
|
+
const tc = tcIdx.get(m.toolCallId ?? "");
|
|
776
|
+
if (tc?.name === "bash" && /error|fail/i.test(txt)) {
|
|
777
|
+
errAcc++;
|
|
778
|
+
type = "debugging";
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (m.role === "user" && SHIFT_RE.test(txt) && tokenAcc > pc.minChunkTokens)
|
|
782
|
+
brk = true;
|
|
783
|
+
if (tokenAcc >= pc.maxChunkTokens)
|
|
784
|
+
brk = true;
|
|
785
|
+
if (brk && i > startIdx && topics.length < maxSegs - 1) {
|
|
786
|
+
topics.push({ startIndex: startIdx, endIndex: i, primaryFile, type, errorDensity: errAcc });
|
|
787
|
+
startIdx = i + 1;
|
|
788
|
+
tokenAcc = 0;
|
|
789
|
+
lastFile = null;
|
|
790
|
+
errAcc = 0;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (startIdx < msgs.length) {
|
|
794
|
+
topics.push({ startIndex: startIdx, endIndex: msgs.length - 1, primaryFile: null, type: "exploration", errorDensity: errAcc });
|
|
795
|
+
}
|
|
796
|
+
return topics;
|
|
797
|
+
}
|
|
798
|
+
function buildTimeline(msgs, errors) {
|
|
799
|
+
const timeline = [];
|
|
800
|
+
const errorIndices = new Set(errors.map((e) => e.index));
|
|
801
|
+
for (let i = 0;i < msgs.length; i++) {
|
|
802
|
+
const m = msgs[i];
|
|
803
|
+
if (m.role === "user") {
|
|
804
|
+
const txt = extractText(m.content);
|
|
805
|
+
if (!txt.startsWith("/"))
|
|
806
|
+
timeline.push({ index: i, event: "user_request", summary: txt.slice(0, 150) });
|
|
807
|
+
}
|
|
808
|
+
if (errorIndices.has(i))
|
|
809
|
+
timeline.push({ index: i, event: "error", summary: errors.find((e) => e.index === i)?.message.slice(0, 100) ?? "error" });
|
|
810
|
+
}
|
|
811
|
+
return timeline.length > 30 ? [...timeline.filter((t) => t.event === "user_request").slice(0, 10), ...timeline.filter((t) => t.event === "error")] : timeline;
|
|
812
|
+
}
|
|
813
|
+
function extractMainGoal(msgs) {
|
|
814
|
+
for (const m of msgs) {
|
|
815
|
+
if (m?.role !== "user")
|
|
816
|
+
continue;
|
|
817
|
+
const txt = extractText(m.content).trim();
|
|
818
|
+
if (txt && !txt.startsWith("/"))
|
|
819
|
+
return txt.slice(0, 300);
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
function extractOpenLoops(msgs, extraction) {
|
|
824
|
+
const loops = [];
|
|
825
|
+
let loopId = 0;
|
|
826
|
+
for (const err of extraction.errors.filter((e) => !e.resolved)) {
|
|
827
|
+
const errFiles = extraction.modifiedFiles.filter((f) => err.message.toLowerCase().includes(f.path.split("/").pop()?.toLowerCase() ?? "__none__")).map((f) => f.path);
|
|
828
|
+
loops.push({
|
|
829
|
+
id: "loop-" + ++loopId,
|
|
830
|
+
type: "bugfix",
|
|
831
|
+
priority: err.retryAttempted ? "high" : "normal",
|
|
832
|
+
status: "open",
|
|
833
|
+
summary: err.message.slice(0, 120),
|
|
834
|
+
files: errFiles,
|
|
835
|
+
sourceIndex: err.index
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const FOLLOWUP_RE = /(?:next\s+(?:step|thing)|todo|action item|follow\s*up|still (?:need|have) to|gotta|gotta|yapalim|yapmamiz|gerekiyor|eklenecek|d\u00FCzeltilecek|bitmedi|kaldi)/i;
|
|
839
|
+
for (const msg of msgs) {
|
|
840
|
+
if (msg.role !== "user")
|
|
841
|
+
continue;
|
|
842
|
+
const txt = extractText(msg.content);
|
|
843
|
+
if (txt.length < 10 || txt.startsWith("/"))
|
|
844
|
+
continue;
|
|
845
|
+
if (FOLLOWUP_RE.test(txt)) {
|
|
846
|
+
const idx = msgs.indexOf(msg);
|
|
847
|
+
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
848
|
+
if (!isDup) {
|
|
849
|
+
loops.push({
|
|
850
|
+
id: "loop-" + ++loopId,
|
|
851
|
+
type: "follow-up",
|
|
852
|
+
priority: "normal",
|
|
853
|
+
status: "open",
|
|
854
|
+
summary: txt.slice(0, 120),
|
|
855
|
+
files: [],
|
|
856
|
+
sourceIndex: idx
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const BLOCKED_RE = /blocked|waiting for|depend|ba[\u011Fg]li|bekliyor|engell/i;
|
|
862
|
+
for (const msg of msgs) {
|
|
863
|
+
if (msg.role !== "user")
|
|
864
|
+
continue;
|
|
865
|
+
const txt = extractText(msg.content);
|
|
866
|
+
if (BLOCKED_RE.test(txt)) {
|
|
867
|
+
const idx = msgs.indexOf(msg);
|
|
868
|
+
const isDup = loops.some((l) => Math.abs((l.sourceIndex ?? 0) - idx) < 5);
|
|
869
|
+
if (!isDup) {
|
|
870
|
+
loops.push({
|
|
871
|
+
id: "loop-" + ++loopId,
|
|
872
|
+
type: "blocked",
|
|
873
|
+
priority: "high",
|
|
874
|
+
status: "open",
|
|
875
|
+
summary: txt.slice(0, 120),
|
|
876
|
+
files: [],
|
|
877
|
+
sourceIndex: idx
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const err of extraction.errors.filter((e) => e.retryAttempted && !e.resolved)) {
|
|
883
|
+
const exists = loops.some((l) => l.type === "bugfix" && l.sourceIndex === err.index);
|
|
884
|
+
if (!exists) {
|
|
885
|
+
loops.push({
|
|
886
|
+
id: "loop-" + ++loopId,
|
|
887
|
+
type: "retry",
|
|
888
|
+
priority: "high",
|
|
889
|
+
status: "open",
|
|
890
|
+
summary: "Retried but unresolved: " + err.message.slice(0, 80),
|
|
891
|
+
files: [],
|
|
892
|
+
sourceIndex: err.index
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return loops;
|
|
897
|
+
}
|
|
898
|
+
function extractStructured(msgs, pc) {
|
|
899
|
+
const { modified, read, deleted } = trackFileOps(msgs);
|
|
900
|
+
const errors = catalogErrors(msgs);
|
|
901
|
+
const decisions = extractDecisions(msgs);
|
|
902
|
+
const constraints = mineConstraints(msgs);
|
|
903
|
+
const topics = segmentTopicsHeuristic(msgs, pc);
|
|
904
|
+
const timeline = buildTimeline(msgs, errors);
|
|
905
|
+
const mainGoal = extractMainGoal(msgs);
|
|
906
|
+
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
907
|
+
const lastErrors = errors.slice(-3).map((e) => e.message);
|
|
908
|
+
return {
|
|
909
|
+
modifiedFiles: modified,
|
|
910
|
+
readFiles: read,
|
|
911
|
+
deletedFiles: deleted,
|
|
912
|
+
errors,
|
|
913
|
+
decisions,
|
|
914
|
+
constraints,
|
|
915
|
+
topics,
|
|
916
|
+
timeline,
|
|
917
|
+
mainGoal,
|
|
918
|
+
lastUserMessages,
|
|
919
|
+
lastErrors,
|
|
920
|
+
messageCount: msgs.length
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/utils/state.ts
|
|
925
|
+
import fs3 from "fs";
|
|
926
|
+
import path4 from "path";
|
|
927
|
+
var STATE_DIR = path4.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "states");
|
|
928
|
+
function getStatePath(projectId) {
|
|
929
|
+
return path4.join(STATE_DIR, projectId + ".json");
|
|
930
|
+
}
|
|
931
|
+
function saveCompactionState(projectId, state) {
|
|
932
|
+
try {
|
|
933
|
+
if (!fs3.existsSync(STATE_DIR))
|
|
934
|
+
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
935
|
+
fs3.writeFileSync(getStatePath(projectId), JSON.stringify(state, null, 2));
|
|
936
|
+
} catch {}
|
|
937
|
+
}
|
|
938
|
+
function loadCompactionState(projectId) {
|
|
939
|
+
try {
|
|
940
|
+
const fp = getStatePath(projectId);
|
|
941
|
+
if (!fs3.existsSync(fp))
|
|
942
|
+
return null;
|
|
943
|
+
const data = JSON.parse(fs3.readFileSync(fp, "utf8"));
|
|
944
|
+
if (data.compactionVersion && Date.now() - 0 > 7 * 24 * 60 * 60 * 1000) {}
|
|
945
|
+
return data;
|
|
946
|
+
} catch {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function buildCompactionState(extraction, openLoops, report, nextActions, criticalContext) {
|
|
951
|
+
let decisionId = 0;
|
|
952
|
+
let constraintId = 0;
|
|
953
|
+
let errorId = 0;
|
|
954
|
+
return {
|
|
955
|
+
goal: extraction.mainGoal,
|
|
956
|
+
decisions: extraction.decisions.map((d) => ({
|
|
957
|
+
id: "decision-" + ++decisionId,
|
|
958
|
+
summary: d.summary.slice(0, 200),
|
|
959
|
+
...d.userResponse ? { userResponse: d.userResponse.slice(0, 300) } : {},
|
|
960
|
+
type: d.type
|
|
961
|
+
})),
|
|
962
|
+
constraints: extraction.constraints.map((c) => ({
|
|
963
|
+
id: "constraint-" + ++constraintId,
|
|
964
|
+
text: c.text.slice(0, 300),
|
|
965
|
+
category: c.category,
|
|
966
|
+
confidence: c.confidence
|
|
967
|
+
})),
|
|
968
|
+
modifiedFiles: extraction.modifiedFiles.map((f) => f.path),
|
|
969
|
+
readFiles: extraction.readFiles,
|
|
970
|
+
deletedFiles: extraction.deletedFiles,
|
|
971
|
+
unresolvedErrors: extraction.errors.filter((e) => !e.resolved).map((e) => {
|
|
972
|
+
const bn = extraction.modifiedFiles.find((f) => e.message.toLowerCase().includes(f.path.split("/").pop()?.toLowerCase() ?? "__none__"));
|
|
973
|
+
return {
|
|
974
|
+
id: "error-" + ++errorId,
|
|
975
|
+
message: e.message.slice(0, 300),
|
|
976
|
+
tool: e.tool,
|
|
977
|
+
files: bn ? [bn.path] : []
|
|
978
|
+
};
|
|
979
|
+
}),
|
|
980
|
+
resolvedErrors: extraction.errors.filter((e) => e.resolved).map((e) => ({
|
|
981
|
+
id: "error-" + ++errorId,
|
|
982
|
+
message: e.message.slice(0, 300),
|
|
983
|
+
tool: e.tool
|
|
984
|
+
})),
|
|
985
|
+
openLoops,
|
|
986
|
+
topics: extraction.topics.map((t, i) => ({
|
|
987
|
+
title: t.primaryFile ? t.primaryFile.split("/").pop() + " (" + t.type + ")" : "Topic " + (i + 1),
|
|
988
|
+
type: t.type,
|
|
989
|
+
priority: t.errorDensity > 2 ? "high" : "normal"
|
|
990
|
+
})),
|
|
991
|
+
nextActions,
|
|
992
|
+
criticalContext,
|
|
993
|
+
sessionType: report?.sessionType ?? "implementation",
|
|
994
|
+
compactionVersion: VERSION
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function injectOpenLoopsSection(summary, openLoops) {
|
|
998
|
+
if (!openLoops.length)
|
|
999
|
+
return summary;
|
|
1000
|
+
const lines = [
|
|
1001
|
+
"## Open Loops",
|
|
1002
|
+
"",
|
|
1003
|
+
...openLoops.map((l) => {
|
|
1004
|
+
const prio = l.priority === "critical" || l.priority === "high" ? "[" + l.priority + "] " : "";
|
|
1005
|
+
const files = l.files.length ? " \u2014 " + l.files.join(", ") : "";
|
|
1006
|
+
return "- " + prio + l.summary + files;
|
|
1007
|
+
}),
|
|
1008
|
+
""
|
|
1009
|
+
];
|
|
1010
|
+
const nextStepsIdx = summary.indexOf("## Next Steps");
|
|
1011
|
+
if (nextStepsIdx >= 0) {
|
|
1012
|
+
return summary.slice(0, nextStepsIdx) + lines.join(`
|
|
1013
|
+
`) + summary.slice(nextStepsIdx);
|
|
1014
|
+
}
|
|
1015
|
+
return summary + `
|
|
1016
|
+
` + lines.join(`
|
|
1017
|
+
`);
|
|
1018
|
+
}
|
|
1019
|
+
function computeDelta(prev, current) {
|
|
1020
|
+
const prevDecisionTexts = new Set(prev.decisions.map((d) => d.summary.toLowerCase().slice(0, 60)));
|
|
1021
|
+
const currDecisionTexts = new Set(current.decisions.map((d) => d.summary.toLowerCase().slice(0, 60)));
|
|
1022
|
+
const newDecisions = current.decisions.filter((d) => !prevDecisionTexts.has(d.summary.toLowerCase().slice(0, 60))).map((d) => d.summary);
|
|
1023
|
+
const removedDecisions = prev.decisions.filter((d) => !currDecisionTexts.has(d.summary.toLowerCase().slice(0, 60))).map((d) => d.summary);
|
|
1024
|
+
const prevLoopSummaries = new Map(prev.openLoops.map((l) => [l.summary.toLowerCase().slice(0, 50), l]));
|
|
1025
|
+
const currLoopSummaries = new Map(current.openLoops.map((l) => [l.summary.toLowerCase().slice(0, 50), l]));
|
|
1026
|
+
const resolvedLoops = [];
|
|
1027
|
+
const persistentLoops = [];
|
|
1028
|
+
for (const [key, loop] of prevLoopSummaries) {
|
|
1029
|
+
if (currLoopSummaries.has(key)) {
|
|
1030
|
+
persistentLoops.push(loop.summary);
|
|
1031
|
+
} else {
|
|
1032
|
+
resolvedLoops.push(loop.summary);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const newLoops = current.openLoops.filter((l) => !prevLoopSummaries.has(l.summary.toLowerCase().slice(0, 50))).map((l) => l.summary);
|
|
1036
|
+
const prevFiles = new Set(prev.modifiedFiles);
|
|
1037
|
+
const newModifiedFiles = current.modifiedFiles.filter((f) => !prevFiles.has(f));
|
|
1038
|
+
const prevErrorMsgs = new Set(prev.unresolvedErrors.map((e) => e.message.toLowerCase().slice(0, 40)));
|
|
1039
|
+
const currErrorMsgs = new Set(current.unresolvedErrors.map((e) => e.message.toLowerCase().slice(0, 40)));
|
|
1040
|
+
const resolvedErrors = prev.unresolvedErrors.filter((e) => !currErrorMsgs.has(e.message.toLowerCase().slice(0, 40))).map((e) => e.message);
|
|
1041
|
+
const newErrors = current.unresolvedErrors.filter((e) => !prevErrorMsgs.has(e.message.toLowerCase().slice(0, 40))).map((e) => e.message);
|
|
1042
|
+
const goalChanged = prev.goal !== current.goal && prev.goal !== null && current.goal !== null;
|
|
1043
|
+
return {
|
|
1044
|
+
newDecisions,
|
|
1045
|
+
removedDecisions,
|
|
1046
|
+
resolvedLoops,
|
|
1047
|
+
persistentLoops,
|
|
1048
|
+
newLoops,
|
|
1049
|
+
newModifiedFiles,
|
|
1050
|
+
resolvedErrors,
|
|
1051
|
+
newErrors,
|
|
1052
|
+
goalChanged,
|
|
1053
|
+
previousGoal: goalChanged ? prev.goal : null
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function formatDeltaSection(delta) {
|
|
1057
|
+
const lines = ["## Changes Since Last Compaction", ""];
|
|
1058
|
+
if (delta.goalChanged) {
|
|
1059
|
+
lines.push("- **Goal shifted**: " + (delta.previousGoal ?? "?") + " \u2192 see current goal above");
|
|
1060
|
+
}
|
|
1061
|
+
if (delta.resolvedLoops.length) {
|
|
1062
|
+
lines.push("- **Resolved loops**: " + delta.resolvedLoops.map((s) => "~~" + s.slice(0, 60) + "~~").join(", "));
|
|
1063
|
+
}
|
|
1064
|
+
if (delta.persistentLoops.length) {
|
|
1065
|
+
lines.push("- **Still open**: " + delta.persistentLoops.map((s) => s.slice(0, 60)).join("; "));
|
|
1066
|
+
}
|
|
1067
|
+
if (delta.newLoops.length) {
|
|
1068
|
+
lines.push("- **New loops**: " + delta.newLoops.map((s) => s.slice(0, 60)).join("; "));
|
|
1069
|
+
}
|
|
1070
|
+
if (delta.newDecisions.length) {
|
|
1071
|
+
lines.push("- **New decisions**: " + delta.newDecisions.map((s) => s.slice(0, 80)).join("; "));
|
|
1072
|
+
}
|
|
1073
|
+
if (delta.resolvedErrors.length) {
|
|
1074
|
+
lines.push("- **Resolved errors**: " + delta.resolvedErrors.map((s) => s.slice(0, 60)).join("; "));
|
|
1075
|
+
}
|
|
1076
|
+
if (delta.newErrors.length) {
|
|
1077
|
+
lines.push("- **New errors**: " + delta.newErrors.map((s) => s.slice(0, 60)).join("; "));
|
|
1078
|
+
}
|
|
1079
|
+
if (delta.newModifiedFiles.length) {
|
|
1080
|
+
lines.push("- **New files touched**: " + delta.newModifiedFiles.join(", "));
|
|
1081
|
+
}
|
|
1082
|
+
lines.push("");
|
|
1083
|
+
return lines.join(`
|
|
1084
|
+
`);
|
|
1085
|
+
}
|
|
1086
|
+
function injectDeltaSection(summary, delta) {
|
|
1087
|
+
const section = formatDeltaSection(delta);
|
|
1088
|
+
const hasChanges = delta.goalChanged || delta.resolvedLoops.length > 0 || delta.newLoops.length > 0 || delta.newDecisions.length > 0 || delta.newErrors.length > 0 || delta.newModifiedFiles.length > 0;
|
|
1089
|
+
if (!hasChanges)
|
|
1090
|
+
return summary;
|
|
1091
|
+
const openLoopsIdx = summary.indexOf("## Open Loops");
|
|
1092
|
+
const nextStepsIdx = summary.indexOf("## Next Steps");
|
|
1093
|
+
if (openLoopsIdx >= 0) {
|
|
1094
|
+
const afterOL = summary.indexOf("## ", openLoopsIdx + 1);
|
|
1095
|
+
if (afterOL >= 0) {
|
|
1096
|
+
return summary.slice(0, afterOL) + section + summary.slice(afterOL);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (nextStepsIdx >= 0) {
|
|
1100
|
+
return summary.slice(0, nextStepsIdx) + section + summary.slice(nextStepsIdx);
|
|
1101
|
+
}
|
|
1102
|
+
return summary + `
|
|
1103
|
+
` + section;
|
|
1104
|
+
}
|
|
1105
|
+
function extractNextActions(summary) {
|
|
1106
|
+
const match = summary.match(/## Next Steps\s*\n([\s\S]*?)(?=##|$)/);
|
|
1107
|
+
if (!match)
|
|
1108
|
+
return [];
|
|
1109
|
+
return match[1].split(`
|
|
1110
|
+
`).map((l) => l.replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 0);
|
|
1111
|
+
}
|
|
1112
|
+
function extractCriticalContext(summary) {
|
|
1113
|
+
const match = summary.match(/## Critical Context\s*\n([\s\S]*?)(?=##|$)/);
|
|
1114
|
+
if (!match)
|
|
1115
|
+
return [];
|
|
1116
|
+
return match[1].split(`
|
|
1117
|
+
`).map((l) => l.replace(/^-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/utils/pruning.ts
|
|
1121
|
+
var ACK_RE = /^(?:I'?ll |let me |sure|ok[,.]?|got it|i understand|i see|now i|next,? i|alright|great|perfect|sounds good|i can|i will|checking|looking|right away)/i;
|
|
1122
|
+
var MAX_TOOL_OUTPUT_CHARS = 800;
|
|
1123
|
+
function pruneRedundant(msgs) {
|
|
1124
|
+
if (msgs.length < 5)
|
|
1125
|
+
return { messages: msgs, prunedCount: 0, prunedTokenSaving: 0, reasons: [] };
|
|
1126
|
+
const tcIdx = buildToolCallIndex(msgs);
|
|
1127
|
+
const keep = new Set(msgs.map((_, i2) => i2));
|
|
1128
|
+
const reasonMap = new Map;
|
|
1129
|
+
const readIndices = new Map;
|
|
1130
|
+
for (let i2 = 0;i2 < msgs.length; i2++) {
|
|
1131
|
+
if (msgs[i2].role !== "toolResult")
|
|
1132
|
+
continue;
|
|
1133
|
+
const tc = tcIdx.get(msgs[i2].toolCallId ?? "");
|
|
1134
|
+
if (!tc || tc.name !== "read")
|
|
1135
|
+
continue;
|
|
1136
|
+
const fp = tc?.arguments?.path ?? tc?.arguments?.file_path;
|
|
1137
|
+
if (!fp)
|
|
1138
|
+
continue;
|
|
1139
|
+
const arr = readIndices.get(fp) ?? [];
|
|
1140
|
+
arr.push(i2);
|
|
1141
|
+
readIndices.set(fp, arr);
|
|
1142
|
+
}
|
|
1143
|
+
for (const [fp, indices] of readIndices) {
|
|
1144
|
+
for (let j = 0;j < indices.length - 1; j++) {
|
|
1145
|
+
keep.delete(indices[j]);
|
|
1146
|
+
const tc = tcIdx.get(msgs[indices[j]].toolCallId ?? "");
|
|
1147
|
+
if (tc)
|
|
1148
|
+
keep.delete(tc.msgIndex);
|
|
1149
|
+
}
|
|
1150
|
+
if (indices.length > 1) {
|
|
1151
|
+
reasonMap.set("Duplicate file reads", (reasonMap.get("Duplicate file reads") ?? 0) + indices.length - 1);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const failedToolResults = [];
|
|
1155
|
+
for (let i2 = 0;i2 < msgs.length; i2++) {
|
|
1156
|
+
if (msgs[i2].role !== "toolResult" || !msgs[i2].isError)
|
|
1157
|
+
continue;
|
|
1158
|
+
const tc = tcIdx.get(msgs[i2].toolCallId ?? "");
|
|
1159
|
+
failedToolResults.push({ index: i2, tool: tc?.name ?? "unknown", tcIndex: tc?.msgIndex ?? -1 });
|
|
1160
|
+
}
|
|
1161
|
+
let i = 0;
|
|
1162
|
+
while (i < failedToolResults.length) {
|
|
1163
|
+
const tool = failedToolResults[i].tool;
|
|
1164
|
+
let j = i + 1;
|
|
1165
|
+
while (j < failedToolResults.length && failedToolResults[j].tool === tool && failedToolResults[j].index - failedToolResults[j - 1].index < 10) {
|
|
1166
|
+
j++;
|
|
1167
|
+
}
|
|
1168
|
+
if (j - i >= 3) {
|
|
1169
|
+
for (let k = i + 1;k < j - 1; k++) {
|
|
1170
|
+
keep.delete(failedToolResults[k].index);
|
|
1171
|
+
if (failedToolResults[k].tcIndex >= 0)
|
|
1172
|
+
keep.delete(failedToolResults[k].tcIndex);
|
|
1173
|
+
}
|
|
1174
|
+
reasonMap.set("Collapsed error chains", (reasonMap.get("Collapsed error chains") ?? 0) + (j - i - 2));
|
|
1175
|
+
}
|
|
1176
|
+
i = j;
|
|
1177
|
+
}
|
|
1178
|
+
for (let idx = 0;idx < msgs.length; idx++) {
|
|
1179
|
+
if (msgs[idx].role !== "assistant")
|
|
1180
|
+
continue;
|
|
1181
|
+
const blocks = Array.isArray(msgs[idx].content) ? msgs[idx].content : [];
|
|
1182
|
+
const hasToolCall = blocks.some((b) => isToolCallBlock(b));
|
|
1183
|
+
if (hasToolCall)
|
|
1184
|
+
continue;
|
|
1185
|
+
const text = extractText(msgs[idx].content).trim();
|
|
1186
|
+
if (text.length > 0 && text.length < 100 && ACK_RE.test(text)) {
|
|
1187
|
+
keep.delete(idx);
|
|
1188
|
+
reasonMap.set("Agent acknowledgments", (reasonMap.get("Agent acknowledgments") ?? 0) + 1);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const kept = msgs.map((m, idx) => {
|
|
1192
|
+
if (!keep.has(idx))
|
|
1193
|
+
return null;
|
|
1194
|
+
if (m.role !== "toolResult")
|
|
1195
|
+
return m;
|
|
1196
|
+
const text = extractText(m.content);
|
|
1197
|
+
if (text.length > MAX_TOOL_OUTPUT_CHARS) {
|
|
1198
|
+
const head = text.slice(0, 400);
|
|
1199
|
+
const tail = text.slice(-400);
|
|
1200
|
+
const truncated = head + `
|
|
1201
|
+
... [truncated ` + (text.length - 800) + ` chars] ...
|
|
1202
|
+
` + tail;
|
|
1203
|
+
return { ...m, content: [{ type: "text", text: truncated }] };
|
|
1204
|
+
}
|
|
1205
|
+
return m;
|
|
1206
|
+
});
|
|
1207
|
+
const finalMsgs = kept.filter((m) => m !== null);
|
|
1208
|
+
const prunedCount = msgs.length - finalMsgs.length;
|
|
1209
|
+
const originalTokens = estimateTokens(msgs.map((m) => extractText(m.content)).join(""));
|
|
1210
|
+
const prunedTokens = estimateTokens(finalMsgs.map((m) => extractText(m.content)).join(""));
|
|
1211
|
+
const reasons = [...reasonMap.entries()].map(([reason, count]) => ({ count, reason }));
|
|
1212
|
+
return {
|
|
1213
|
+
messages: finalMsgs,
|
|
1214
|
+
prunedCount,
|
|
1215
|
+
prunedTokenSaving: Math.max(0, originalTokens - prunedTokens),
|
|
1216
|
+
reasons
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/utils/fingerprint.ts
|
|
1221
|
+
import fs4 from "fs";
|
|
1222
|
+
import path5 from "path";
|
|
1223
|
+
var FINGERPRINT_DIR = path5.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact", "projects");
|
|
1224
|
+
var LANG_MAP = {
|
|
1225
|
+
".ts": "typescript",
|
|
1226
|
+
".tsx": "typescript",
|
|
1227
|
+
".js": "javascript",
|
|
1228
|
+
".jsx": "javascript",
|
|
1229
|
+
".rs": "rust",
|
|
1230
|
+
".py": "python",
|
|
1231
|
+
".go": "go",
|
|
1232
|
+
".java": "java",
|
|
1233
|
+
".rb": "ruby",
|
|
1234
|
+
".cs": "csharp",
|
|
1235
|
+
".cpp": "cpp",
|
|
1236
|
+
".c": "c",
|
|
1237
|
+
".h": "c",
|
|
1238
|
+
".swift": "swift",
|
|
1239
|
+
".kt": "kotlin",
|
|
1240
|
+
".php": "php"
|
|
1241
|
+
};
|
|
1242
|
+
var FRAMEWORK_SIGNALS = [
|
|
1243
|
+
{ pattern: /next\.config/i, framework: "nextjs" },
|
|
1244
|
+
{ pattern: /nuxt\.config/i, framework: "nuxt" },
|
|
1245
|
+
{ pattern: /vite\.config/i, framework: "vite" },
|
|
1246
|
+
{ pattern: /astro\.config/i, framework: "astro" },
|
|
1247
|
+
{ pattern: /tailwind\.config/i, framework: "tailwind" },
|
|
1248
|
+
{ pattern: /django/i, framework: "django" },
|
|
1249
|
+
{ pattern: /flask/i, framework: "flask" },
|
|
1250
|
+
{ pattern: /cargo\.toml/i, framework: "cargo" },
|
|
1251
|
+
{ pattern: /go\.mod/i, framework: "go-modules" },
|
|
1252
|
+
{ pattern: /Gemfile/i, framework: "bundler" },
|
|
1253
|
+
{ pattern: /package\.json/i, framework: "node" }
|
|
1254
|
+
];
|
|
1255
|
+
function getFingerprintPath(projectId) {
|
|
1256
|
+
return path5.join(FINGERPRINT_DIR, projectId + ".json");
|
|
1257
|
+
}
|
|
1258
|
+
function deriveProjectId(extraction) {
|
|
1259
|
+
const allPaths = [
|
|
1260
|
+
...extraction.modifiedFiles.map((f) => f.path),
|
|
1261
|
+
...extraction.readFiles
|
|
1262
|
+
];
|
|
1263
|
+
if (!allPaths.length)
|
|
1264
|
+
return "unknown";
|
|
1265
|
+
const roots = new Map;
|
|
1266
|
+
for (const p of allPaths) {
|
|
1267
|
+
const parts = p.split("/");
|
|
1268
|
+
const root = parts.length > 1 ? parts.slice(0, Math.min(2, parts.length - 1)).join("/") : "root";
|
|
1269
|
+
roots.set(root, (roots.get(root) ?? 0) + 1);
|
|
1270
|
+
}
|
|
1271
|
+
const topRoot = [...roots.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
|
|
1272
|
+
let hash = 0;
|
|
1273
|
+
for (let i = 0;i < topRoot.length; i++) {
|
|
1274
|
+
hash = (hash << 5) - hash + topRoot.charCodeAt(i) | 0;
|
|
1275
|
+
}
|
|
1276
|
+
return "proj-" + Math.abs(hash).toString(36);
|
|
1277
|
+
}
|
|
1278
|
+
function detectLanguage(extraction) {
|
|
1279
|
+
const extCounts = new Map;
|
|
1280
|
+
for (const f of extraction.modifiedFiles) {
|
|
1281
|
+
const ext = path5.extname(f.path).toLowerCase();
|
|
1282
|
+
if (ext && LANG_MAP[ext]) {
|
|
1283
|
+
extCounts.set(LANG_MAP[ext], (extCounts.get(LANG_MAP[ext]) ?? 0) + 1);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
for (const f of extraction.readFiles) {
|
|
1287
|
+
const ext = path5.extname(f).toLowerCase();
|
|
1288
|
+
if (ext && LANG_MAP[ext]) {
|
|
1289
|
+
extCounts.set(LANG_MAP[ext], (extCounts.get(LANG_MAP[ext]) ?? 0) + 1);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (!extCounts.size)
|
|
1293
|
+
return "unknown";
|
|
1294
|
+
return [...extCounts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
1295
|
+
}
|
|
1296
|
+
function detectFramework(extraction) {
|
|
1297
|
+
const allPaths = extraction.readFiles.join(" ") + " " + extraction.modifiedFiles.map((f) => f.path).join(" ");
|
|
1298
|
+
for (const { pattern, framework } of FRAMEWORK_SIGNALS) {
|
|
1299
|
+
if (pattern.test(allPaths))
|
|
1300
|
+
return framework;
|
|
1301
|
+
}
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
function extractKeyDirs(extraction, maxDirs = 8) {
|
|
1305
|
+
const dirCounts = new Map;
|
|
1306
|
+
for (const f of extraction.modifiedFiles) {
|
|
1307
|
+
const parts = f.path.split("/");
|
|
1308
|
+
if (parts.length > 1) {
|
|
1309
|
+
const dir = parts.slice(0, -1).join("/");
|
|
1310
|
+
dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return [...dirCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, maxDirs).map(([d]) => d);
|
|
1314
|
+
}
|
|
1315
|
+
function loadProjectFingerprint(projectId) {
|
|
1316
|
+
try {
|
|
1317
|
+
const fp = getFingerprintPath(projectId);
|
|
1318
|
+
if (!fs4.existsSync(fp))
|
|
1319
|
+
return null;
|
|
1320
|
+
const data = JSON.parse(fs4.readFileSync(fp, "utf8"));
|
|
1321
|
+
if (Date.now() - data.updatedAt > 30 * 24 * 60 * 60 * 1000)
|
|
1322
|
+
return null;
|
|
1323
|
+
return data;
|
|
1324
|
+
} catch {
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
function saveProjectFingerprint(projectId, extraction) {
|
|
1329
|
+
try {
|
|
1330
|
+
if (!fs4.existsSync(FINGERPRINT_DIR))
|
|
1331
|
+
fs4.mkdirSync(FINGERPRINT_DIR, { recursive: true });
|
|
1332
|
+
const existing = loadProjectFingerprint(projectId);
|
|
1333
|
+
const newKnownFiles = [...new Set([
|
|
1334
|
+
...existing?.knownFiles ?? [],
|
|
1335
|
+
...extraction.modifiedFiles.map((f) => f.path),
|
|
1336
|
+
...extraction.readFiles
|
|
1337
|
+
])].slice(-50);
|
|
1338
|
+
const fingerprint = {
|
|
1339
|
+
id: projectId,
|
|
1340
|
+
language: existing?.language ?? detectLanguage(extraction),
|
|
1341
|
+
framework: existing?.framework ?? detectFramework(extraction),
|
|
1342
|
+
keyDirectories: extractKeyDirs(extraction),
|
|
1343
|
+
knownFiles: newKnownFiles,
|
|
1344
|
+
sessionCount: (existing?.sessionCount ?? 0) + 1,
|
|
1345
|
+
updatedAt: Date.now()
|
|
1346
|
+
};
|
|
1347
|
+
fs4.writeFileSync(getFingerprintPath(projectId), JSON.stringify(fingerprint, null, 2));
|
|
1348
|
+
} catch {}
|
|
1349
|
+
}
|
|
1350
|
+
function buildProjectContext(fingerprint) {
|
|
1351
|
+
if (!fingerprint)
|
|
1352
|
+
return "";
|
|
1353
|
+
return [
|
|
1354
|
+
"## Project Context (learned from " + fingerprint.sessionCount + " session(s))",
|
|
1355
|
+
"Language: " + fingerprint.language,
|
|
1356
|
+
fingerprint.framework ? "Framework: " + fingerprint.framework : "",
|
|
1357
|
+
fingerprint.keyDirectories.length ? "Key dirs: " + fingerprint.keyDirectories.join(", ") : ""
|
|
1358
|
+
].filter(Boolean).join(`
|
|
1359
|
+
`);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/utils/damage.ts
|
|
1363
|
+
import fs5 from "fs";
|
|
1364
|
+
import path6 from "path";
|
|
1365
|
+
var COMPLAINT_PATTERNS = [
|
|
1366
|
+
/(?:I already (?:told|said|mentioned|explained) you|(?:we|I) (?:already|just) (?:discussed|went over|covered) this|you forgot|you lost|nerede kald\u0131|hat\u0131rlam\u0131yor|unuttun)/i,
|
|
1367
|
+
/(?:that'?s? not (?:what I|right)|that'?s? wrong|yanl\u0131\u015F|hay\u0131r de\u011Fil|no that'|that doesn'?t match)/i,
|
|
1368
|
+
/(?:go back to|return to|(?:look|check) again|tekrar bak|geri d\u00F6n)/i
|
|
1369
|
+
];
|
|
1370
|
+
function detectDamage(postMessages, details) {
|
|
1371
|
+
const signals = [];
|
|
1372
|
+
const compactedFiles = new Set(details.modifiedFiles.map((f) => f.toLowerCase()));
|
|
1373
|
+
const compactedReadFiles = new Set(details.readFiles.map((f) => f.toLowerCase()));
|
|
1374
|
+
for (let i = 0;i < postMessages.length; i++) {
|
|
1375
|
+
const msg = postMessages[i];
|
|
1376
|
+
const text = extractText(msg.content).toLowerCase();
|
|
1377
|
+
if (msg.role === "assistant") {
|
|
1378
|
+
const blocks = Array.isArray(msg.content) ? msg.content : [];
|
|
1379
|
+
for (const b of blocks) {
|
|
1380
|
+
if (isToolCallBlock(b) && (b.name === "read" || b.name === "bash")) {
|
|
1381
|
+
const fp = b.arguments?.path ?? b.arguments?.file_path;
|
|
1382
|
+
if (fp) {
|
|
1383
|
+
const fpLower = fp.toLowerCase();
|
|
1384
|
+
if (compactedFiles.has(fpLower) || compactedReadFiles.has(fpLower)) {
|
|
1385
|
+
signals.push({
|
|
1386
|
+
type: "re-read",
|
|
1387
|
+
severity: "medium",
|
|
1388
|
+
detail: "Agent re-read compacted file: " + fp
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (msg.role === "user") {
|
|
1396
|
+
for (const pattern of COMPLAINT_PATTERNS) {
|
|
1397
|
+
if (pattern.test(text)) {
|
|
1398
|
+
signals.push({
|
|
1399
|
+
type: "user-complaint",
|
|
1400
|
+
severity: "high",
|
|
1401
|
+
detail: 'User complaint after compaction: "' + text.slice(0, 100) + '"'
|
|
1402
|
+
});
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
for (const t of details.topics) {
|
|
1407
|
+
const topicWords = t.toLowerCase().split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
|
|
1408
|
+
if (topicWords.length >= 2 && topicWords.some((w) => text.includes(w))) {
|
|
1409
|
+
signals.push({
|
|
1410
|
+
type: "re-question",
|
|
1411
|
+
severity: "low",
|
|
1412
|
+
detail: "User mentions compacted topic: " + t.slice(0, 80)
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
let damageScore = 0;
|
|
1419
|
+
for (const s of signals) {
|
|
1420
|
+
if (s.severity === "high")
|
|
1421
|
+
damageScore += 25;
|
|
1422
|
+
else if (s.severity === "medium")
|
|
1423
|
+
damageScore += 10;
|
|
1424
|
+
else
|
|
1425
|
+
damageScore += 3;
|
|
1426
|
+
}
|
|
1427
|
+
damageScore = Math.min(100, damageScore);
|
|
1428
|
+
const parts = [];
|
|
1429
|
+
const reReads = signals.filter((s) => s.type === "re-read").length;
|
|
1430
|
+
const complaints = signals.filter((s) => s.type === "user-complaint").length;
|
|
1431
|
+
const reQuestions = signals.filter((s) => s.type === "re-question").length;
|
|
1432
|
+
if (reReads)
|
|
1433
|
+
parts.push(reReads + " re-read(s)");
|
|
1434
|
+
if (complaints)
|
|
1435
|
+
parts.push(complaints + " user complaint(s)");
|
|
1436
|
+
if (reQuestions)
|
|
1437
|
+
parts.push(reQuestions + " re-question(s)");
|
|
1438
|
+
return {
|
|
1439
|
+
signals,
|
|
1440
|
+
damageScore,
|
|
1441
|
+
summary: parts.length ? "Damage score: " + damageScore + "/100 \u2014 " + parts.join(", ") : "No regression signals detected (score: 0)"
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
function logDamageReport(sessionId, report, details) {
|
|
1445
|
+
try {
|
|
1446
|
+
const dir = path6.join(process.env.HOME ?? "/tmp", ".pi", "agent", ".cache", "smart-compact");
|
|
1447
|
+
if (!fs5.existsSync(dir))
|
|
1448
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1449
|
+
const logPath = path6.join(dir, "damage-reports.jsonl");
|
|
1450
|
+
const entry = {
|
|
1451
|
+
ts: new Date().toISOString(),
|
|
1452
|
+
sessionId,
|
|
1453
|
+
method: details.method,
|
|
1454
|
+
profile: details.profile,
|
|
1455
|
+
qualityScore: details.qualityScore,
|
|
1456
|
+
damageScore: report.damageScore,
|
|
1457
|
+
signals: report.signals.length,
|
|
1458
|
+
summary: report.summary
|
|
1459
|
+
};
|
|
1460
|
+
fs5.appendFileSync(logPath, JSON.stringify(entry) + `
|
|
1461
|
+
`);
|
|
1462
|
+
} catch {}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/phases/explore.ts
|
|
1466
|
+
var _toolSupportCache = new Map;
|
|
1467
|
+
var TOOL_CACHE_TTL = 30 * 60 * 1000;
|
|
1468
|
+
function shouldExplore(extraction) {
|
|
1469
|
+
const unresolvedErrors = extraction.errors.filter((e) => !e.resolved).length;
|
|
1470
|
+
const topicCount = extraction.topics.length;
|
|
1471
|
+
const decisionCount = extraction.decisions.length;
|
|
1472
|
+
const crossFileWork = new Set(extraction.modifiedFiles.map((f) => {
|
|
1473
|
+
const parts = f.path.split("/");
|
|
1474
|
+
return parts.length > 1 ? parts.slice(0, -1).join("/") : "root";
|
|
1475
|
+
})).size;
|
|
1476
|
+
if (topicCount <= 3 && unresolvedErrors <= 1 && decisionCount <= 2 && crossFileWork <= 2) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
var EXPLORATION_TOOLS = [
|
|
1482
|
+
{
|
|
1483
|
+
name: "get_message_range",
|
|
1484
|
+
description: "Get compact summaries of messages from start to end index (0-based).",
|
|
1485
|
+
parameters: { type: "object", properties: { start: { type: "number" }, end: { type: "number" } }, required: ["start", "end"] }
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
name: "search_conversation",
|
|
1489
|
+
description: "Search for text in conversation messages.",
|
|
1490
|
+
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
name: "get_recent_user_messages",
|
|
1494
|
+
description: "Get the last N user messages.",
|
|
1495
|
+
parameters: { type: "object", properties: { count: { type: "number" } } }
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
name: "get_context_around",
|
|
1499
|
+
description: "Get context around a specific message index.",
|
|
1500
|
+
parameters: { type: "object", properties: { index: { type: "number" }, radius: { type: "number" } }, required: ["index"] }
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
name: "get_file_changes",
|
|
1504
|
+
description: "Get tool calls that modified a specific file.",
|
|
1505
|
+
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
name: "get_error_chain",
|
|
1509
|
+
description: "Get all messages related to a specific error.",
|
|
1510
|
+
parameters: { type: "object", properties: { index: { type: "number" }, context_radius: { type: "number" } }, required: ["index"] }
|
|
1511
|
+
}
|
|
1512
|
+
];
|
|
1513
|
+
function executeExplorationTool(call, llmMessages) {
|
|
1514
|
+
const args = call.arguments ?? {};
|
|
1515
|
+
switch (call.name) {
|
|
1516
|
+
case "get_message_range": {
|
|
1517
|
+
const s = args.start ?? 0, e = Math.min(args.end ?? llmMessages.length, llmMessages.length);
|
|
1518
|
+
return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
|
|
1519
|
+
idx: s + i,
|
|
1520
|
+
role: m?.role,
|
|
1521
|
+
preview: extractText(m?.content).slice(0, 150),
|
|
1522
|
+
toolCalls: getToolCallNames(m?.content),
|
|
1523
|
+
isError: m?.isError
|
|
1524
|
+
})));
|
|
1525
|
+
}
|
|
1526
|
+
case "search_conversation": {
|
|
1527
|
+
const q = (args.query ?? "").toLowerCase();
|
|
1528
|
+
return JSON.stringify(llmMessages.filter((m) => JSON.stringify(m).toLowerCase().includes(q)).slice(0, 10).map((m) => ({
|
|
1529
|
+
idx: llmMessages.indexOf(m),
|
|
1530
|
+
role: m?.role,
|
|
1531
|
+
preview: extractText(m?.content).slice(0, 150)
|
|
1532
|
+
})));
|
|
1533
|
+
}
|
|
1534
|
+
case "get_recent_user_messages": {
|
|
1535
|
+
const count = args.count ?? 10;
|
|
1536
|
+
return JSON.stringify(llmMessages.filter((m) => m?.role === "user").slice(-count).map((m) => extractText(m.content)));
|
|
1537
|
+
}
|
|
1538
|
+
case "get_context_around": {
|
|
1539
|
+
const idx = args.index ?? 0, radius = args.radius ?? 5;
|
|
1540
|
+
const s = Math.max(0, idx - radius), e = Math.min(llmMessages.length, idx + radius + 1);
|
|
1541
|
+
return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
|
|
1542
|
+
idx: s + i,
|
|
1543
|
+
role: m?.role,
|
|
1544
|
+
text: extractText(m?.content).slice(0, 300),
|
|
1545
|
+
toolCalls: getToolCallNames(m?.content),
|
|
1546
|
+
isError: m?.isError
|
|
1547
|
+
})));
|
|
1548
|
+
}
|
|
1549
|
+
case "get_file_changes": {
|
|
1550
|
+
const target = (args.path ?? "").toLowerCase();
|
|
1551
|
+
const results = [];
|
|
1552
|
+
for (let i = 0;i < llmMessages.length; i++) {
|
|
1553
|
+
const tcs = filterToolCalls(llmMessages[i]?.content);
|
|
1554
|
+
for (const block of tcs) {
|
|
1555
|
+
if (block.name === "edit" && JSON.stringify(block).toLowerCase().includes(target)) {
|
|
1556
|
+
results.push({ idx: i, role: "assistant", toolCall: "edit", args: block.arguments, preview: extractText(llmMessages[i]?.content).slice(0, 400) });
|
|
1557
|
+
}
|
|
1558
|
+
if (block.name === "write" && JSON.stringify(block).toLowerCase().includes(target)) {
|
|
1559
|
+
results.push({ idx: i, role: "assistant", toolCall: "write", preview: extractText(llmMessages[i]?.content).slice(0, 400) });
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return JSON.stringify(results.slice(0, 15) || [{ info: "No edits found for: " + args.path }]);
|
|
1564
|
+
}
|
|
1565
|
+
case "get_error_chain": {
|
|
1566
|
+
const errIdx = args.index ?? 0;
|
|
1567
|
+
const ctxRadius = args.context_radius ?? 8;
|
|
1568
|
+
const s = Math.max(0, errIdx - ctxRadius), e = Math.min(llmMessages.length, errIdx + ctxRadius + 1);
|
|
1569
|
+
return JSON.stringify(llmMessages.slice(s, e).map((m, i) => ({
|
|
1570
|
+
idx: s + i,
|
|
1571
|
+
role: m?.role,
|
|
1572
|
+
text: extractText(m?.content).slice(0, 500),
|
|
1573
|
+
isError: m?.isError,
|
|
1574
|
+
toolCalls: getToolCallNames(m?.content)
|
|
1575
|
+
})));
|
|
1576
|
+
}
|
|
1577
|
+
default:
|
|
1578
|
+
return "Unknown tool: " + call.name;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function parseExplorationReport(text, llmMessages) {
|
|
1582
|
+
let json = text.trim();
|
|
1583
|
+
const md = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1584
|
+
if (md)
|
|
1585
|
+
json = md[1].trim();
|
|
1586
|
+
let s = json.indexOf("{"), e = json.lastIndexOf("}");
|
|
1587
|
+
if (s === -1 || e === -1)
|
|
1588
|
+
return fallbackExplorationReport(llmMessages);
|
|
1589
|
+
let rawJson = json.slice(s, e + 1);
|
|
1590
|
+
try {
|
|
1591
|
+
return buildExplorationReportFromParsed(JSON.parse(rawJson), llmMessages);
|
|
1592
|
+
} catch {}
|
|
1593
|
+
const cleaned = rawJson.replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"').replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1594
|
+
try {
|
|
1595
|
+
return buildExplorationReportFromParsed(JSON.parse(cleaned), llmMessages);
|
|
1596
|
+
} catch {}
|
|
1597
|
+
const boundaryMatch = rawJson.match(/"boundaries"\s*:\s*\[([\s\S]*?)\]/);
|
|
1598
|
+
if (boundaryMatch) {
|
|
1599
|
+
try {
|
|
1600
|
+
const boundaries = JSON.parse("[" + boundaryMatch[1] + "]");
|
|
1601
|
+
return { ...fallbackExplorationReport(llmMessages), boundaries: boundaries.filter((b) => typeof b?.afterIndex === "number").map((b) => ({
|
|
1602
|
+
afterIndex: Math.min(b.afterIndex, llmMessages.length - 2),
|
|
1603
|
+
topic: String(b.topic ?? "").slice(0, 100),
|
|
1604
|
+
priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
|
|
1605
|
+
confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
|
|
1606
|
+
})) };
|
|
1607
|
+
} catch {}
|
|
1608
|
+
}
|
|
1609
|
+
return fallbackExplorationReport(llmMessages);
|
|
1610
|
+
}
|
|
1611
|
+
function buildExplorationReportFromParsed(parsed, llmMessages) {
|
|
1612
|
+
return {
|
|
1613
|
+
boundaries: (parsed.boundaries ?? []).filter((b) => typeof b?.afterIndex === "number").map((b) => ({
|
|
1614
|
+
afterIndex: Math.min(b.afterIndex, llmMessages.length - 2),
|
|
1615
|
+
topic: String(b.topic ?? "").slice(0, 100),
|
|
1616
|
+
priority: ["critical", "high", "normal", "low"].includes(b.priority) ? b.priority : "normal",
|
|
1617
|
+
confidence: Math.min(1, Math.max(0, b.confidence ?? 0.5))
|
|
1618
|
+
})),
|
|
1619
|
+
mainGoal: parsed.mainGoal ?? "",
|
|
1620
|
+
sessionType: ["implementation", "review", "debugging", "discussion"].includes(parsed.sessionType) ? parsed.sessionType : "implementation",
|
|
1621
|
+
enrichedConstraints: Array.isArray(parsed.enrichedConstraints) ? parsed.enrichedConstraints.map(String) : [],
|
|
1622
|
+
crossReferences: Array.isArray(parsed.crossReferences) ? parsed.crossReferences.map(String) : [],
|
|
1623
|
+
statusAssessment: {
|
|
1624
|
+
done: Array.isArray(parsed.statusAssessment?.done) ? parsed.statusAssessment.done.map(String) : [],
|
|
1625
|
+
inProgress: Array.isArray(parsed.statusAssessment?.inProgress) ? parsed.statusAssessment.inProgress.map(String) : [],
|
|
1626
|
+
blocked: Array.isArray(parsed.statusAssessment?.blocked) ? parsed.statusAssessment.blocked.map(String) : []
|
|
1627
|
+
},
|
|
1628
|
+
criticalContext: Array.isArray(parsed.criticalContext) ? parsed.criticalContext.map(String) : [],
|
|
1629
|
+
keyDecisions: Array.isArray(parsed.keyDecisions) ? parsed.keyDecisions.map(String) : []
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
function fallbackExplorationReport(llmMessages) {
|
|
1633
|
+
return {
|
|
1634
|
+
boundaries: [],
|
|
1635
|
+
mainGoal: extractMainGoal(llmMessages) ?? "",
|
|
1636
|
+
sessionType: "implementation",
|
|
1637
|
+
enrichedConstraints: [],
|
|
1638
|
+
crossReferences: [],
|
|
1639
|
+
statusAssessment: { done: [], inProgress: [], blocked: [] },
|
|
1640
|
+
criticalContext: [],
|
|
1641
|
+
keyDecisions: []
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = 8, notify) {
|
|
1645
|
+
const extractionContext = [
|
|
1646
|
+
"## Deterministic Extraction (verified facts)",
|
|
1647
|
+
"Message count: " + extraction.messageCount,
|
|
1648
|
+
"Main goal: " + (extraction.mainGoal ?? "unknown"),
|
|
1649
|
+
"Files modified (" + extraction.modifiedFiles.length + "): " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none"),
|
|
1650
|
+
"Files read (" + extraction.readFiles.length + "): " + (extraction.readFiles.join(", ") || "none"),
|
|
1651
|
+
"Errors (" + extraction.errors.length + "): " + (extraction.errors.map((e) => "[" + e.tool + "] " + e.message.slice(0, 80) + (e.resolved ? " (resolved)" : e.retryAttempted ? " (retry attempted)" : "")).join("; ") || "none"),
|
|
1652
|
+
"Decisions (" + extraction.decisions.length + "): " + (extraction.decisions.map((d) => d.type + ": " + d.summary.slice(0, 80)).join("; ") || "none"),
|
|
1653
|
+
"Constraints (" + extraction.constraints.length + "): " + (extraction.constraints.map((cc) => "[" + cc.category + "] " + cc.text.slice(0, 80)).join("; ") || "none"),
|
|
1654
|
+
"Heuristic topics (" + extraction.topics.length + "): " + (extraction.topics.map((t) => "[" + t.startIndex + "-" + t.endIndex + "] " + t.type).join("; ") || "none"),
|
|
1655
|
+
extraction.lastUserMessages.length ? "Last user messages: " + extraction.lastUserMessages.map((m) => m.slice(0, 100)).join(" | ") : "",
|
|
1656
|
+
extraction.lastErrors.length ? "Last errors: " + extraction.lastErrors.map((e) => e.slice(0, 100)).join(" | ") : ""
|
|
1657
|
+
].filter(Boolean).join(`
|
|
1658
|
+
`);
|
|
1659
|
+
const userContent = `Explore this conversation and produce the structured report.
|
|
1660
|
+
|
|
1661
|
+
` + extractionContext + (prevSummary ? `
|
|
1662
|
+
|
|
1663
|
+
## Previous Summary
|
|
1664
|
+
` + prevSummary : "") + (userNote ? `
|
|
1665
|
+
|
|
1666
|
+
## User Steering
|
|
1667
|
+
"` + userNote + '"' : "");
|
|
1668
|
+
const cacheKey = model.provider + "/" + model.id;
|
|
1669
|
+
const cachedSupport = _toolSupportCache.get(cacheKey);
|
|
1670
|
+
const cacheValid = cachedSupport && Date.now() - cachedSupport.timestamp < TOOL_CACHE_TTL;
|
|
1671
|
+
let supportsTools = false;
|
|
1672
|
+
try {
|
|
1673
|
+
if (cacheValid && !cachedSupport.result) {
|
|
1674
|
+
if (notify)
|
|
1675
|
+
notify("Tool support cached: unsupported (" + cacheKey + ")", "info");
|
|
1676
|
+
const report2 = await directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal);
|
|
1677
|
+
if (!report2.boundaries.length) {
|
|
1678
|
+
const retried = await explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal);
|
|
1679
|
+
if (retried.boundaries.length)
|
|
1680
|
+
return { report: retried, rounds: 1, toolSupported: false };
|
|
1681
|
+
}
|
|
1682
|
+
return { report: report2, rounds: 0, toolSupported: false };
|
|
1683
|
+
}
|
|
1684
|
+
const probeResp = await trackedComplete("explore", model, {
|
|
1685
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1686
|
+
messages: [{ role: "user", content: [{ type: "text", text: userContent }] }],
|
|
1687
|
+
tools: EXPLORATION_TOOLS
|
|
1688
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
|
|
1689
|
+
const toolCalls = probeResp.content.filter((c) => c.type === "toolCall");
|
|
1690
|
+
if (toolCalls.length > 0) {
|
|
1691
|
+
supportsTools = true;
|
|
1692
|
+
_toolSupportCache.set(cacheKey, { result: true, timestamp: Date.now() });
|
|
1693
|
+
const messages = [
|
|
1694
|
+
{ role: "user", content: [{ type: "text", text: userContent }], timestamp: Date.now() },
|
|
1695
|
+
{ role: "assistant", content: probeResp.content, timestamp: Date.now() }
|
|
1696
|
+
];
|
|
1697
|
+
for (const tc of toolCalls) {
|
|
1698
|
+
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
1699
|
+
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
1700
|
+
}
|
|
1701
|
+
let rounds = 1;
|
|
1702
|
+
while (rounds < maxRounds) {
|
|
1703
|
+
rounds++;
|
|
1704
|
+
let response;
|
|
1705
|
+
try {
|
|
1706
|
+
response = await trackedComplete("explore-loop", model, {
|
|
1707
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX + `
|
|
1708
|
+
|
|
1709
|
+
` + EXPLORER_SYSTEM_PROMPT,
|
|
1710
|
+
messages,
|
|
1711
|
+
tools: EXPLORATION_TOOLS
|
|
1712
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, signal }));
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
console.error("[smart-compact] Explore loop error:", err instanceof Error ? err.message : err);
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1717
|
+
const nextToolCalls = response.content.filter((c) => c.type === "toolCall");
|
|
1718
|
+
if (nextToolCalls.length === 0) {
|
|
1719
|
+
const text = response.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1720
|
+
`).trim();
|
|
1721
|
+
let report2 = parseExplorationReport(text, llmMessages);
|
|
1722
|
+
if (!report2.boundaries.length) {
|
|
1723
|
+
report2 = await directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal);
|
|
1724
|
+
if (report2.boundaries.length)
|
|
1725
|
+
rounds++;
|
|
1726
|
+
}
|
|
1727
|
+
return { report: report2, rounds, toolSupported: true };
|
|
1728
|
+
}
|
|
1729
|
+
messages.push({ role: "assistant", content: response.content, timestamp: Date.now() });
|
|
1730
|
+
for (const tc of nextToolCalls) {
|
|
1731
|
+
const result = executeExplorationTool({ name: tc.name, arguments: tc.arguments }, llmMessages);
|
|
1732
|
+
messages.push({ role: "toolResult", toolCallId: tc.id, toolName: tc.name, content: [{ type: "text", text: result }], isError: false, timestamp: Date.now() });
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
|
|
1736
|
+
if (lastAssistant?.content) {
|
|
1737
|
+
const text = lastAssistant.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1738
|
+
`).trim();
|
|
1739
|
+
const report2 = parseExplorationReport(text, llmMessages);
|
|
1740
|
+
if (report2.boundaries.length)
|
|
1741
|
+
return { report: report2, rounds, toolSupported: true };
|
|
1742
|
+
}
|
|
1743
|
+
} else {
|
|
1744
|
+
const text = probeResp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1745
|
+
`).trim();
|
|
1746
|
+
let report2 = parseExplorationReport(text, llmMessages);
|
|
1747
|
+
if (!report2.boundaries.length) {
|
|
1748
|
+
report2 = await directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal);
|
|
1749
|
+
}
|
|
1750
|
+
return { report: report2, rounds: 1, toolSupported: true };
|
|
1751
|
+
}
|
|
1752
|
+
} catch {
|
|
1753
|
+
console.error("[smart-compact] Tool calling probe failed for " + cacheKey);
|
|
1754
|
+
_toolSupportCache.set(cacheKey, { result: false, timestamp: Date.now() });
|
|
1755
|
+
if (notify)
|
|
1756
|
+
notify("Tool calling not supported, using direct exploration", "warning");
|
|
1757
|
+
}
|
|
1758
|
+
const report = await directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal);
|
|
1759
|
+
if (!report.boundaries.length) {
|
|
1760
|
+
const retried = await explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal);
|
|
1761
|
+
if (retried.boundaries.length)
|
|
1762
|
+
return { report: retried, rounds: 1, toolSupported: false };
|
|
1763
|
+
}
|
|
1764
|
+
return { report, rounds: 0, toolSupported: supportsTools };
|
|
1765
|
+
}
|
|
1766
|
+
async function explorationRetry(model, auth, llmMessages, extraction, prevSummary, userNote, signal) {
|
|
1767
|
+
const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 150)).join(`
|
|
1768
|
+
`);
|
|
1769
|
+
const retryPrompt = `IMPORTANT: Output ONLY valid raw JSON. No markdown. No explanation. No code fences. Just the JSON object.
|
|
1770
|
+
|
|
1771
|
+
` + `Produce this exact structure:
|
|
1772
|
+
{"mainGoal":"...","sessionType":"implementation|review|debugging|discussion","boundaries":[{"afterIndex":N,"topic":"...","priority":"normal","confidence":0.5}],"enrichedConstraints":[],"crossReferences":[],"statusAssessment":{"done":[],"inProgress":[],"blocked":[]},"criticalContext":[],"keyDecisions":[]}
|
|
1773
|
+
|
|
1774
|
+
` + `Context:
|
|
1775
|
+
Files: ` + extraction.modifiedFiles.map((f) => f.path).join(", ") + `
|
|
1776
|
+
` + "Topics heuristic: " + extraction.topics.map((t) => "[" + t.startIndex + "-" + t.endIndex + "]").join(", ") + `
|
|
1777
|
+
` + `Last messages:
|
|
1778
|
+
` + last5 + (userNote ? `
|
|
1779
|
+
User steering: ` + userNote : "");
|
|
1780
|
+
try {
|
|
1781
|
+
const resp = await trackedComplete("explore-retry", model, {
|
|
1782
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1783
|
+
messages: [{ role: "user", content: [{ type: "text", text: retryPrompt }] }]
|
|
1784
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
1785
|
+
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
|
|
1786
|
+
return parseExplorationReport(text, llmMessages);
|
|
1787
|
+
} catch {
|
|
1788
|
+
return fallbackExplorationReport(llmMessages);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
async function directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal) {
|
|
1792
|
+
const first3 = llmMessages.filter((m) => m?.role === "user").slice(0, 3).map((m) => extractText(m?.content).slice(0, 200)).join(`
|
|
1793
|
+
---
|
|
1794
|
+
`);
|
|
1795
|
+
const last30 = llmMessages.slice(-30).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, 300)).join(`
|
|
1796
|
+
`);
|
|
1797
|
+
const prompt = `Analyze this conversation and produce a JSON report.
|
|
1798
|
+
|
|
1799
|
+
First user messages:
|
|
1800
|
+
` + first3 + `
|
|
1801
|
+
|
|
1802
|
+
Deterministic data:
|
|
1803
|
+
` + "- Files modified: " + (extraction.modifiedFiles.map((f) => f.path).join(", ") || "none") + `
|
|
1804
|
+
- Errors: ` + (extraction.errors.map((e) => e.message.slice(0, 80)).join("; ") || "none") + `
|
|
1805
|
+
- Decisions: ` + (extraction.decisions.map((d) => d.summary.slice(0, 80)).join("; ") || "none") + `
|
|
1806
|
+
- Constraints: ` + (extraction.constraints.map((c) => c.text.slice(0, 80)).join("; ") || "none") + `
|
|
1807
|
+
|
|
1808
|
+
Last 30 messages:
|
|
1809
|
+
` + last30 + (prevSummary ? `
|
|
1810
|
+
|
|
1811
|
+
Previous summary:
|
|
1812
|
+
` + prevSummary : "") + (userNote ? `
|
|
1813
|
+
|
|
1814
|
+
User note: "` + userNote + '"' : "") + `
|
|
1815
|
+
|
|
1816
|
+
Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debugging|discussion","boundaries":[{"afterIndex":N,"topic":"...","priority":"normal","confidence":0.5}],"enrichedConstraints":[...],"crossReferences":[...],"statusAssessment":{"done":[...],"inProgress":[...],"blocked":[...]},"criticalContext":[...],"keyDecisions":[...]}`;
|
|
1817
|
+
try {
|
|
1818
|
+
const resp = await trackedComplete("explore-direct", model, {
|
|
1819
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1820
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }]
|
|
1821
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
1822
|
+
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1823
|
+
`).trim();
|
|
1824
|
+
return parseExplorationReport(text, llmMessages);
|
|
1825
|
+
} catch {
|
|
1826
|
+
return fallbackExplorationReport(llmMessages);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/phases/synthesize.ts
|
|
1831
|
+
function chunkLlmMessages(msgs, boundaries, pc) {
|
|
1832
|
+
if (!msgs.length)
|
|
1833
|
+
return [];
|
|
1834
|
+
if (!boundaries.length) {
|
|
1835
|
+
return [{
|
|
1836
|
+
startIndex: 0,
|
|
1837
|
+
endIndex: msgs.length - 1,
|
|
1838
|
+
tokenEstimate: estimateTokens(JSON.stringify(msgs)),
|
|
1839
|
+
topic: "Full conversation",
|
|
1840
|
+
priority: "normal",
|
|
1841
|
+
messages: msgs
|
|
1842
|
+
}];
|
|
1843
|
+
}
|
|
1844
|
+
const sorted = [...boundaries].sort((a, b) => a.afterIndex - b.afterIndex);
|
|
1845
|
+
const chunks = [];
|
|
1846
|
+
let start = 0;
|
|
1847
|
+
for (const bp of sorted) {
|
|
1848
|
+
const end = bp.afterIndex + 1;
|
|
1849
|
+
if (end > start && end <= msgs.length) {
|
|
1850
|
+
const slice = msgs.slice(start, end);
|
|
1851
|
+
chunks.push({
|
|
1852
|
+
startIndex: start,
|
|
1853
|
+
endIndex: end - 1,
|
|
1854
|
+
tokenEstimate: estimateTokens(JSON.stringify(slice)),
|
|
1855
|
+
topic: bp.topic || "Segment " + (chunks.length + 1),
|
|
1856
|
+
priority: bp.priority,
|
|
1857
|
+
messages: slice
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
start = end;
|
|
1861
|
+
}
|
|
1862
|
+
if (start < msgs.length) {
|
|
1863
|
+
const slice = msgs.slice(start);
|
|
1864
|
+
const lastTopic = sorted.length ? "After: " + sorted[sorted.length - 1].topic : "Full conversation";
|
|
1865
|
+
chunks.push({
|
|
1866
|
+
startIndex: start,
|
|
1867
|
+
endIndex: msgs.length - 1,
|
|
1868
|
+
tokenEstimate: estimateTokens(JSON.stringify(slice)),
|
|
1869
|
+
topic: lastTopic,
|
|
1870
|
+
priority: "normal",
|
|
1871
|
+
messages: slice
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
const merged = [];
|
|
1875
|
+
for (const ch of chunks) {
|
|
1876
|
+
if (merged.length && ch.tokenEstimate < pc.minChunkTokens) {
|
|
1877
|
+
const prev = merged[merged.length - 1];
|
|
1878
|
+
prev.endIndex = ch.endIndex;
|
|
1879
|
+
prev.tokenEstimate += ch.tokenEstimate;
|
|
1880
|
+
prev.messages = msgs.slice(prev.startIndex, prev.endIndex + 1);
|
|
1881
|
+
prev.topic = prev.topic + " + " + ch.topic;
|
|
1882
|
+
} else {
|
|
1883
|
+
merged.push(ch);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return merged;
|
|
1887
|
+
}
|
|
1888
|
+
async function singlePassCompact(convText, extraction, report, prevContext, model, auth, signal) {
|
|
1889
|
+
const extractionCtx = buildExtractionContext(extraction);
|
|
1890
|
+
const explorationCtx = report ? buildExplorationContext(report) : "";
|
|
1891
|
+
const sessionType = report?.sessionType ?? "implementation";
|
|
1892
|
+
const sessionInstruction = SESSION_TYPE_INSTRUCTIONS[sessionType] ?? SESSION_TYPE_INSTRUCTIONS.implementation;
|
|
1893
|
+
const adaptedPrefix = SINGLE_PASS_PREFIX + `
|
|
1894
|
+
Session-specific instructions:
|
|
1895
|
+
` + sessionInstruction;
|
|
1896
|
+
const dynamicSuffix = SINGLE_PASS_SUFFIX.replace("{PREV_CONTEXT}", prevContext).replace("{EXTRACTION_CONTEXT}", extractionCtx).replace("{EXPLORATION_CONTEXT}", explorationCtx).replace("{CONVERSATION}", convText);
|
|
1897
|
+
const resp = await trackedComplete("single-pass", model, {
|
|
1898
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1899
|
+
messages: [
|
|
1900
|
+
{ role: "user", content: [{ type: "text", text: adaptedPrefix }] },
|
|
1901
|
+
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
1902
|
+
]
|
|
1903
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
|
|
1904
|
+
const summary = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1905
|
+
`).trim();
|
|
1906
|
+
if (!summary.startsWith("##"))
|
|
1907
|
+
throw new Error("Single-pass malformed output");
|
|
1908
|
+
return { summary, llmCalls: 1 };
|
|
1909
|
+
}
|
|
1910
|
+
async function summarizeBatch(batch, extraction, model, auth, signal) {
|
|
1911
|
+
const range = { start: batch[0].startIndex, end: batch[batch.length - 1].endIndex };
|
|
1912
|
+
const extractionCtx = buildExtractionContext(extraction, range);
|
|
1913
|
+
const activeDecisions = extraction.decisions.filter((d) => d.index < range.start).map((d) => "- " + d.summary.slice(0, 120) + (d.userResponse ? " \u2192 " + d.userResponse.slice(0, 60) : ""));
|
|
1914
|
+
const decisionCtx = activeDecisions.length ? `
|
|
1915
|
+
## Active Decisions from previous segments (honour these):
|
|
1916
|
+
` + activeDecisions.join(`
|
|
1917
|
+
`) : "";
|
|
1918
|
+
const text = batch.map((ch) => "--- Topic: " + ch.topic + " (" + ch.priority + `) ---
|
|
1919
|
+
` + ch.messages.map((m) => {
|
|
1920
|
+
const role = m?.role ?? "unknown";
|
|
1921
|
+
const content = extractText(m?.content).slice(0, 500);
|
|
1922
|
+
return "[" + role + "] " + content;
|
|
1923
|
+
}).join(`
|
|
1924
|
+
`)).join(`
|
|
1925
|
+
|
|
1926
|
+
`);
|
|
1927
|
+
const dynamicSuffix = BATCH_PROMPT_SUFFIX.replace("{EXTRACTION_CONTEXT}", extractionCtx + decisionCtx).replace("{TEXT}", text);
|
|
1928
|
+
const resp = await trackedComplete("batch", model, {
|
|
1929
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1930
|
+
messages: [
|
|
1931
|
+
{ role: "user", content: [{ type: "text", text: BATCH_PROMPT_PREFIX }] },
|
|
1932
|
+
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
1933
|
+
]
|
|
1934
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 4096, signal }));
|
|
1935
|
+
const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1936
|
+
`);
|
|
1937
|
+
const sections = output.split(/^### /m).filter((s) => s.trim());
|
|
1938
|
+
return batch.map((ch, i) => {
|
|
1939
|
+
const sec = sections[i] ?? "";
|
|
1940
|
+
const f = (n) => {
|
|
1941
|
+
const m = sec.match(new RegExp("\\*\\*" + n + "\\*\\*:\\s*(.+?)(?:\\n|$)", "i"));
|
|
1942
|
+
return m ? m[1].trim() : "";
|
|
1943
|
+
};
|
|
1944
|
+
const l = (n) => {
|
|
1945
|
+
const v = f(n);
|
|
1946
|
+
return !v || v === "None" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1947
|
+
};
|
|
1948
|
+
const prio = f("Priority").toLowerCase();
|
|
1949
|
+
return {
|
|
1950
|
+
topic: ch.topic,
|
|
1951
|
+
startIndex: ch.startIndex,
|
|
1952
|
+
endIndex: ch.endIndex,
|
|
1953
|
+
summary: f("Summary") || sec.split(`
|
|
1954
|
+
`).slice(1).join(`
|
|
1955
|
+
`).trim().slice(0, 500),
|
|
1956
|
+
keyDecisions: l("Decisions"),
|
|
1957
|
+
filesModified: l("Modified"),
|
|
1958
|
+
filesRead: l("Read"),
|
|
1959
|
+
priority: ["critical", "high", "normal", "low"].includes(prio) ? prio : ch.priority
|
|
1960
|
+
};
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
async function assembleLLM(summaries, extraction, report, model, auth, budget, prevContext, signal) {
|
|
1964
|
+
const pp = preProcessSummaries(summaries, budget);
|
|
1965
|
+
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
1966
|
+
const detRead = extraction.readFiles;
|
|
1967
|
+
const explorationCtx = report ? buildExplorationContext(report) : "";
|
|
1968
|
+
const dynamicSuffix = ASSEMBLY_PROMPT_SUFFIX.replace("{DECISIONS}", pp.decisions.join("; ") || "None").replace("{MODIFIED}", detModified.join(", ") || "None").replace("{READ}", detRead.join(", ") || "None").replace("{EXPLORATION_CONTEXT}", explorationCtx).replace("{PREV_CONTEXT}", prevContext).replace("{SUMMARIES}", pp.text);
|
|
1969
|
+
const resp = await trackedComplete("assemble", model, {
|
|
1970
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
1971
|
+
messages: [
|
|
1972
|
+
{ role: "user", content: [{ type: "text", text: ASSEMBLY_PROMPT_PREFIX }] },
|
|
1973
|
+
{ role: "user", content: [{ type: "text", text: dynamicSuffix }] }
|
|
1974
|
+
]
|
|
1975
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: Math.min(budget, 8192), signal }));
|
|
1976
|
+
return resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
1977
|
+
`).trim();
|
|
1978
|
+
}
|
|
1979
|
+
function assembleFallback(summaries, extraction) {
|
|
1980
|
+
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
1981
|
+
const detRead = extraction.readFiles;
|
|
1982
|
+
return [
|
|
1983
|
+
"## Goal",
|
|
1984
|
+
extraction.mainGoal ?? "See topics below.",
|
|
1985
|
+
"",
|
|
1986
|
+
"## Constraints & Preferences",
|
|
1987
|
+
...extraction.constraints.map((c) => "- [" + c.category + "] " + c.text.slice(0, 200)),
|
|
1988
|
+
"",
|
|
1989
|
+
"## Progress",
|
|
1990
|
+
"### Done",
|
|
1991
|
+
"- See topics below",
|
|
1992
|
+
"### In Progress",
|
|
1993
|
+
...summaries.filter((s) => s.priority === "high").map((s) => "- [ ] " + s.summary.slice(0, 150)),
|
|
1994
|
+
"### Blocked",
|
|
1995
|
+
"- None",
|
|
1996
|
+
"",
|
|
1997
|
+
"## Key Decisions",
|
|
1998
|
+
...extraction.decisions.map((d) => "- **" + d.summary.slice(0, 100) + "**" + (d.userResponse ? " \u2192 " + d.userResponse : "")),
|
|
1999
|
+
"",
|
|
2000
|
+
"## Files Modified",
|
|
2001
|
+
...detModified.map((f) => "- " + f),
|
|
2002
|
+
"",
|
|
2003
|
+
"## Files Read",
|
|
2004
|
+
...detRead.map((f) => "- " + f),
|
|
2005
|
+
"",
|
|
2006
|
+
"## Next Steps",
|
|
2007
|
+
"1. See topics below",
|
|
2008
|
+
"",
|
|
2009
|
+
"## Critical Context",
|
|
2010
|
+
...extraction.errors.filter((e) => !e.resolved).map((e) => "- Unresolved error: " + e.message.slice(0, 100)),
|
|
2011
|
+
"",
|
|
2012
|
+
"## Topics Covered",
|
|
2013
|
+
...summaries.map((s) => "- **" + s.topic + "** [" + s.priority + "]: " + s.summary.slice(0, 200))
|
|
2014
|
+
].join(`
|
|
2015
|
+
`);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// src/phases/verify.ts
|
|
2019
|
+
function verifySummary(summary, extraction) {
|
|
2020
|
+
const gaps = [];
|
|
2021
|
+
const lower = summary.toLowerCase();
|
|
2022
|
+
let score = 100;
|
|
2023
|
+
for (const f of extraction.modifiedFiles) {
|
|
2024
|
+
const pathLower = f.path.toLowerCase();
|
|
2025
|
+
const parts = pathLower.split("/");
|
|
2026
|
+
const suffixes = [];
|
|
2027
|
+
for (let j = 0;j < parts.length; j++) {
|
|
2028
|
+
suffixes.push(parts.slice(j).join("/"));
|
|
2029
|
+
}
|
|
2030
|
+
const pathMatch = suffixes.some((s) => s.length > 2 && lower.includes(s));
|
|
2031
|
+
if (!pathMatch) {
|
|
2032
|
+
gaps.push("Missing modified file: " + f.path);
|
|
2033
|
+
score -= 5;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
for (const e of extraction.errors.filter((e2) => !e2.resolved)) {
|
|
2037
|
+
const snippet = e.message.slice(0, 30).toLowerCase();
|
|
2038
|
+
if (snippet.length > 5 && !lower.includes(snippet)) {
|
|
2039
|
+
gaps.push("Missing error: " + e.message.slice(0, 80));
|
|
2040
|
+
score -= 5;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
for (const c of extraction.constraints.filter((c2) => c2.confidence >= 0.8)) {
|
|
2044
|
+
const keywords = c.text.split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
|
|
2045
|
+
const found = keywords.some((k) => lower.includes(k.toLowerCase()));
|
|
2046
|
+
if (!found && keywords.length > 0) {
|
|
2047
|
+
gaps.push("Missing constraint: " + c.text.slice(0, 100));
|
|
2048
|
+
score -= 3;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (extraction.mainGoal) {
|
|
2052
|
+
const goalWords = extraction.mainGoal.split(/\s+/).filter((w) => w.length > 3).slice(0, 4);
|
|
2053
|
+
const goalFound = goalWords.some((w) => lower.includes(w.toLowerCase()));
|
|
2054
|
+
if (!goalFound) {
|
|
2055
|
+
gaps.push("Main goal may be missing from summary");
|
|
2056
|
+
score -= 10;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (!lower.includes("## goal"))
|
|
2060
|
+
score -= 5;
|
|
2061
|
+
if (!lower.includes("## progress"))
|
|
2062
|
+
score -= 5;
|
|
2063
|
+
if (!lower.includes("## critical context"))
|
|
2064
|
+
score -= 3;
|
|
2065
|
+
const summaryFileRefs = (summary.match(/[\w.\/-]+\.[\w]+/g) ?? []).filter((p) => p.includes("/") || p.match(/\.(ts|tsx|js|jsx|rs|py|go|java|rb|css|html|json|yaml|yml|toml|md|sh|sql)$/i));
|
|
2066
|
+
const knownFiles = new Set([
|
|
2067
|
+
...extraction.modifiedFiles.map((f) => f.path.toLowerCase()),
|
|
2068
|
+
...extraction.readFiles.map((f) => f.toLowerCase())
|
|
2069
|
+
]);
|
|
2070
|
+
for (const ref of summaryFileRefs) {
|
|
2071
|
+
const refLower = ref.toLowerCase();
|
|
2072
|
+
const isKnown = [...knownFiles].some((kf) => kf.endsWith("/" + refLower) || kf === refLower || kf.endsWith(refLower) && refLower.length > 3);
|
|
2073
|
+
if (!isKnown) {
|
|
2074
|
+
gaps.push("Potentially fabricated file: " + ref);
|
|
2075
|
+
score -= 4;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
const errorFiles = new Set(extraction.errors.map((e) => e.message));
|
|
2079
|
+
if (errorFiles.size > 0) {
|
|
2080
|
+
const doneSection = (summary.match(/### Done[\s\S]*?(?=###|$)/i) ?? [""])[0];
|
|
2081
|
+
if (doneSection) {
|
|
2082
|
+
for (const f of extraction.modifiedFiles) {
|
|
2083
|
+
const bn = f.path.split("/").pop() ?? "";
|
|
2084
|
+
const hasError = [...errorFiles].some((e) => e.toLowerCase().includes(bn.toLowerCase()));
|
|
2085
|
+
const markedDone = doneSection.toLowerCase().includes(bn.toLowerCase());
|
|
2086
|
+
if (hasError && markedDone) {
|
|
2087
|
+
const unresolved = extraction.errors.find((e) => e.message.toLowerCase().includes(bn.toLowerCase()) && !e.resolved);
|
|
2088
|
+
if (unresolved) {
|
|
2089
|
+
gaps.push("Inconsistency: " + bn + " marked Done but has unresolved error");
|
|
2090
|
+
score -= 5;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
const highConfDecisions = extraction.decisions.filter((d) => d.type === "explicit");
|
|
2097
|
+
if (highConfDecisions.length > 0) {
|
|
2098
|
+
const decisionSection = (summary.match(/## Key Decisions[\s\S]*?(?=##|$)/i) ?? [""])[0];
|
|
2099
|
+
for (const d of highConfDecisions) {
|
|
2100
|
+
const keywords = d.summary.split(/\s+/).filter((w) => w.length > 4).slice(0, 3);
|
|
2101
|
+
if (keywords.length > 0 && !keywords.some((k) => decisionSection.toLowerCase().includes(k.toLowerCase()))) {
|
|
2102
|
+
gaps.push("Missing decision: " + d.summary.slice(0, 100));
|
|
2103
|
+
score -= 3;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (extraction.errors.some((e) => !e.resolved)) {
|
|
2108
|
+
const hasOpenLoops = lower.includes("## open loops") || lower.includes("unresolved") || lower.includes("open loop");
|
|
2109
|
+
if (!hasOpenLoops && extraction.errors.filter((e) => !e.resolved).length >= 2) {
|
|
2110
|
+
gaps.push("Missing Open Loops section despite " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors");
|
|
2111
|
+
score -= 5;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return { ok: gaps.length === 0, gaps, score: Math.max(0, score) };
|
|
2115
|
+
}
|
|
2116
|
+
function patchDeterministic(summary, gaps, extraction) {
|
|
2117
|
+
let patched = summary;
|
|
2118
|
+
const fileGaps = gaps.filter((g) => g.startsWith("Missing modified file:"));
|
|
2119
|
+
const errorGaps = gaps.filter((g) => g.startsWith("Missing error:"));
|
|
2120
|
+
const constraintGaps = gaps.filter((g) => g.startsWith("Missing constraint:"));
|
|
2121
|
+
const decisionGaps = gaps.filter((g) => g.startsWith("Missing decision:"));
|
|
2122
|
+
const otherGaps = gaps.filter((g) => !g.startsWith("Missing modified file:") && !g.startsWith("Missing error:") && !g.startsWith("Missing constraint:") && !g.startsWith("Missing decision:") && !g.startsWith("Potentially fabricated") && !g.startsWith("Inconsistency"));
|
|
2123
|
+
if (fileGaps.length > 0) {
|
|
2124
|
+
const filesSection = patched.match(/## Files Modified\n/);
|
|
2125
|
+
if (filesSection) {
|
|
2126
|
+
const insertPos = filesSection.index + filesSection[0].length;
|
|
2127
|
+
const entries = fileGaps.map((g) => "- " + g.replace("Missing modified file: ", "")).join(`
|
|
2128
|
+
`) + `
|
|
2129
|
+
`;
|
|
2130
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
if (errorGaps.length > 0) {
|
|
2134
|
+
const ctxSection = patched.match(/## Critical Context\n/);
|
|
2135
|
+
if (ctxSection) {
|
|
2136
|
+
const insertPos = ctxSection.index + ctxSection[0].length;
|
|
2137
|
+
const entries = errorGaps.map((g) => "- " + g).join(`
|
|
2138
|
+
`) + `
|
|
2139
|
+
`;
|
|
2140
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (constraintGaps.length > 0) {
|
|
2144
|
+
const constrSection = patched.match(/## Constraints & Preferences\n/);
|
|
2145
|
+
if (constrSection) {
|
|
2146
|
+
const insertPos = constrSection.index + constrSection[0].length;
|
|
2147
|
+
const entries = constraintGaps.map((g) => "- " + g).join(`
|
|
2148
|
+
`) + `
|
|
2149
|
+
`;
|
|
2150
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (decisionGaps.length > 0) {
|
|
2154
|
+
const decSection = patched.match(/## Key Decisions\n/);
|
|
2155
|
+
if (decSection) {
|
|
2156
|
+
const insertPos = decSection.index + decSection[0].length;
|
|
2157
|
+
const entries = decisionGaps.map((g) => "- **" + g.replace("Missing decision: ", "") + "**").join(`
|
|
2158
|
+
`) + `
|
|
2159
|
+
`;
|
|
2160
|
+
patched = patched.slice(0, insertPos) + entries + patched.slice(insertPos);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
if (otherGaps.length > 0) {
|
|
2164
|
+
patched += `
|
|
2165
|
+
## Verification Note
|
|
2166
|
+
` + otherGaps.map((g) => "- " + g).join(`
|
|
2167
|
+
`);
|
|
2168
|
+
}
|
|
2169
|
+
return patched;
|
|
2170
|
+
}
|
|
2171
|
+
async function patchSummary(summary, gaps, model, auth, signal) {
|
|
2172
|
+
const patchPrompt = `The summary below is missing some critical information. Add the missing items WITHOUT restructuring the summary.
|
|
2173
|
+
|
|
2174
|
+
Missing items:
|
|
2175
|
+
` + gaps.map((g, i) => i + 1 + ". " + g).join(`
|
|
2176
|
+
`) + `
|
|
2177
|
+
|
|
2178
|
+
Current summary:
|
|
2179
|
+
` + summary + `
|
|
2180
|
+
|
|
2181
|
+
Return the COMPLETE updated summary with missing items integrated. Keep the same format.`;
|
|
2182
|
+
try {
|
|
2183
|
+
const resp = await trackedComplete("patch", model, {
|
|
2184
|
+
systemPrompt: COMPACT_SYSTEM_PREFIX,
|
|
2185
|
+
messages: [{ role: "user", content: [{ type: "text", text: patchPrompt }] }]
|
|
2186
|
+
}, cacheOpts({ apiKey: auth.apiKey, headers: auth.headers, maxTokens: 8192, signal }));
|
|
2187
|
+
const patched = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
2188
|
+
`).trim();
|
|
2189
|
+
return patched.startsWith("##") ? patched : summary;
|
|
2190
|
+
} catch {
|
|
2191
|
+
return summary;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/ui/overlays.ts
|
|
2196
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
2197
|
+
import { Container, SelectList, Text } from "@earendil-works/pi-tui";
|
|
2198
|
+
import path7 from "path";
|
|
2199
|
+
function renderContextBar(theme, pct, tokens, barLen = 24) {
|
|
2200
|
+
const clamped = Math.min(Math.max(pct, 0), 100);
|
|
2201
|
+
const filled = Math.min(barLen, Math.round(clamped / 100 * barLen));
|
|
2202
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
2203
|
+
const color = clamped > 80 ? "error" : clamped > 50 ? "warning" : "success";
|
|
2204
|
+
return theme.fg("text", " Context: ") + theme.fg(color, bar) + theme.fg("text", " " + clamped + "%") + theme.fg("dim", " (" + (tokens ?? 0).toLocaleString() + "t)");
|
|
2205
|
+
}
|
|
2206
|
+
function renderTokenBar(theme, before, after, label, barLen = 30) {
|
|
2207
|
+
const ratio = before > 0 ? after / before : 0;
|
|
2208
|
+
const savedPct = Math.round((1 - ratio) * 100);
|
|
2209
|
+
const filled = Math.min(barLen, Math.round(ratio * barLen));
|
|
2210
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
2211
|
+
const savedColor = savedPct >= 50 ? "success" : savedPct >= 25 ? "warning" : "error";
|
|
2212
|
+
return theme.fg("text", " " + label + ": ") + theme.fg(savedColor, bar) + theme.fg("text", " " + (after ?? 0).toLocaleString() + "t") + theme.fg(savedColor, " (saved " + savedPct + "%)");
|
|
2213
|
+
}
|
|
2214
|
+
var _toolSupportCache2 = new Map;
|
|
2215
|
+
async function selectModel(ctx, opts) {
|
|
2216
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
2217
|
+
const options = available.map((m) => ({
|
|
2218
|
+
value: m.provider + "/" + m.id,
|
|
2219
|
+
label: m.provider + "/" + m.id + (m.contextWindow >= 200000 ? " (" + Math.round(m.contextWindow / 1000) + "K)" : ""),
|
|
2220
|
+
model: m,
|
|
2221
|
+
supportsTools: true
|
|
2222
|
+
}));
|
|
2223
|
+
const items = options.map((o, i) => ({
|
|
2224
|
+
value: "model:" + i,
|
|
2225
|
+
label: o.label,
|
|
2226
|
+
description: i === opts.defaultModelIndex ? "\u2190 session model" : undefined
|
|
2227
|
+
}));
|
|
2228
|
+
const result = await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
2229
|
+
const c = new Container;
|
|
2230
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
2231
|
+
c.addChild(new Text(theme.fg("accent", theme.bold(" \uD83D\uDD0D Smart Compact \u2014 Step 1/2")), 1, 0));
|
|
2232
|
+
c.addChild(new Text(theme.fg("dim", " Architecture: EESV (Extract \u2192 Explore \u2192 Synthesize \u2192 Verify)"), 0, 0));
|
|
2233
|
+
c.addChild(new Text("", 0, 0));
|
|
2234
|
+
c.addChild(new Text(renderContextBar(theme, opts.contextPercent, opts.contextTokens), 0, 0));
|
|
2235
|
+
c.addChild(new Text(theme.fg("dim", " Session: " + opts.currentModel), 0, 0));
|
|
2236
|
+
c.addChild(new Text("", 0, 0));
|
|
2237
|
+
c.addChild(new Text(theme.fg("text", " Select model for compaction:"), 1, 0));
|
|
2238
|
+
c.addChild(new Text("", 0, 0));
|
|
2239
|
+
const sel = new SelectList(items, Math.min(items.length, 12), {
|
|
2240
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
2241
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
2242
|
+
description: (t) => theme.fg("muted", t),
|
|
2243
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
2244
|
+
noMatch: (t) => theme.fg("warning", t)
|
|
2245
|
+
});
|
|
2246
|
+
sel.selectedIndex = opts.defaultModelIndex;
|
|
2247
|
+
sel.onSelect = (item) => done(item.value);
|
|
2248
|
+
sel.onCancel = () => done(null);
|
|
2249
|
+
c.addChild(sel);
|
|
2250
|
+
c.addChild(new Text("", 0, 0));
|
|
2251
|
+
c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter select \u2022 esc cancel"), 0, 0));
|
|
2252
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
2253
|
+
return {
|
|
2254
|
+
render: (w) => c.render(w),
|
|
2255
|
+
invalidate: () => c.invalidate(),
|
|
2256
|
+
handleInput: (d) => {
|
|
2257
|
+
sel.handleInput(d);
|
|
2258
|
+
tui.requestRender();
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
});
|
|
2262
|
+
if (!result?.startsWith("model:"))
|
|
2263
|
+
return null;
|
|
2264
|
+
return options[parseInt(result.slice(6), 10)] ?? null;
|
|
2265
|
+
}
|
|
2266
|
+
async function selectProfile(ctx, selectedModel, opts) {
|
|
2267
|
+
const estAfter = (budget, keep) => budget + Math.min(opts.contextTokens, keep);
|
|
2268
|
+
const profiles = [
|
|
2269
|
+
{ value: "light", label: "\u2601\uFE0F Light", desc: "Max detail", budget: 1e4, keep: 30000 },
|
|
2270
|
+
{ value: "balanced", label: "\u2696\uFE0F Balanced", desc: "Recommended", budget: 6000, keep: 20000 },
|
|
2271
|
+
{ value: "aggressive", label: "\uD83D\uDD25 Aggressive", desc: "Minimal", budget: 3000, keep: 1e4 }
|
|
2272
|
+
];
|
|
2273
|
+
const items = profiles.map((p) => {
|
|
2274
|
+
const after = estAfter(p.budget, p.keep);
|
|
2275
|
+
const pct = opts.contextTokens > 0 ? Math.round((1 - after / opts.contextTokens) * 100) : 0;
|
|
2276
|
+
return { value: p.value, label: p.label, description: p.desc + " \u2014 est. ~" + after.toLocaleString() + "t after (save ~" + pct + "%)" };
|
|
2277
|
+
});
|
|
2278
|
+
const result = await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
2279
|
+
const c = new Container;
|
|
2280
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
2281
|
+
c.addChild(new Text(theme.fg("accent", theme.bold(" \uD83D\uDD0D Smart Compact \u2014 Step 2/2")), 1, 0));
|
|
2282
|
+
c.addChild(new Text("", 0, 0));
|
|
2283
|
+
c.addChild(new Text(theme.fg("dim", " Model: " + selectedModel.label), 0, 0));
|
|
2284
|
+
c.addChild(new Text(renderContextBar(theme, opts.contextPercent, opts.contextTokens), 0, 0));
|
|
2285
|
+
c.addChild(new Text("", 0, 0));
|
|
2286
|
+
c.addChild(new Text(theme.fg("text", " Select compression profile:"), 1, 0));
|
|
2287
|
+
c.addChild(new Text("", 0, 0));
|
|
2288
|
+
const sel = new SelectList(items, 3, {
|
|
2289
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
2290
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
2291
|
+
description: (t) => theme.fg("muted", t),
|
|
2292
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
2293
|
+
noMatch: (t) => theme.fg("warning", t)
|
|
2294
|
+
});
|
|
2295
|
+
sel.selectedIndex = 1;
|
|
2296
|
+
sel.onSelect = (item) => done(item.value);
|
|
2297
|
+
sel.onCancel = () => done(null);
|
|
2298
|
+
c.addChild(sel);
|
|
2299
|
+
c.addChild(new Text("", 0, 0));
|
|
2300
|
+
c.addChild(new Text(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter select \u2022 esc cancel"), 0, 0));
|
|
2301
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
2302
|
+
return {
|
|
2303
|
+
render: (w) => c.render(w),
|
|
2304
|
+
invalidate: () => c.invalidate(),
|
|
2305
|
+
handleInput: (d) => {
|
|
2306
|
+
sel.handleInput(d);
|
|
2307
|
+
tui.requestRender();
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
});
|
|
2311
|
+
if (!result)
|
|
2312
|
+
return null;
|
|
2313
|
+
return profiles.find((p) => p.value === result)?.value ?? null;
|
|
2314
|
+
}
|
|
2315
|
+
function showProgressOverlay(ctx, state) {
|
|
2316
|
+
const phaseNames = ["Extract", "Explore", "Synthesize", "Verify"];
|
|
2317
|
+
const progress = Math.round(state.phase / 4 * 100);
|
|
2318
|
+
const name = phaseNames[state.phase - 1] ?? "?";
|
|
2319
|
+
const detail = state.detail ? " (" + state.detail + ")" : "";
|
|
2320
|
+
const type = state.phase >= 4 ? "success" : "info";
|
|
2321
|
+
ctx.ui.notify("EESV [" + progress + "%] Phase " + state.phase + "/4: " + name + detail, type);
|
|
2322
|
+
}
|
|
2323
|
+
async function showResultScreen(ctx, details, extraction) {
|
|
2324
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
2325
|
+
const c = new Container;
|
|
2326
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
2327
|
+
c.addChild(new Text(theme.fg("accent", theme.bold(" \u2705 Smart Compact Complete")), 1, 0));
|
|
2328
|
+
c.addChild(new Text("", 0, 0));
|
|
2329
|
+
const estimatedAfter = (details.tokensBefore ?? 0) - (details.tokensSaved ?? 0);
|
|
2330
|
+
c.addChild(new Text(renderTokenBar(theme, details.tokensBefore, estimatedAfter, "Result "), 0, 0));
|
|
2331
|
+
c.addChild(new Text(theme.fg("dim", " Before: " + (details.tokensBefore ?? 0).toLocaleString() + "t \u2192 After: ~" + estimatedAfter.toLocaleString() + "t \u2192 Saved: " + (details.tokensSaved ?? 0).toLocaleString() + "t"), 0, 0));
|
|
2332
|
+
c.addChild(new Text("", 0, 0));
|
|
2333
|
+
const methodColors = { eesv: "accent", "single-pass": "success", heuristic: "warning" };
|
|
2334
|
+
const methodColor = methodColors[details.method] ?? "text";
|
|
2335
|
+
c.addChild(new Text(theme.fg("text", " Method: ") + theme.fg(methodColor, details.method.toUpperCase()) + theme.fg("dim", " \u2022 " + details.llmCalls + " LLM call(s) \u2022 Profile: " + details.profile), 0, 0));
|
|
2336
|
+
if (details.model) {
|
|
2337
|
+
c.addChild(new Text(theme.fg("dim", " Model: " + details.model), 0, 0));
|
|
2338
|
+
}
|
|
2339
|
+
const scoreColor = details.qualityScore >= 80 ? "success" : details.qualityScore >= 50 ? "warning" : "error";
|
|
2340
|
+
c.addChild(new Text(theme.fg("text", " Quality: ") + theme.fg(scoreColor, details.qualityScore + "/100"), 0, 0));
|
|
2341
|
+
c.addChild(new Text("", 0, 0));
|
|
2342
|
+
c.addChild(new Text(theme.fg("text", theme.bold(" \uD83D\uDCCB Extraction")), 0, 0));
|
|
2343
|
+
const ms = getMetricsSummary();
|
|
2344
|
+
if (ms.totalCalls > 0) {
|
|
2345
|
+
const cachePct = Math.round(ms.cacheHitRate * 100);
|
|
2346
|
+
const cacheColor = cachePct >= 50 ? "success" : cachePct >= 20 ? "warning" : "dim";
|
|
2347
|
+
c.addChild(new Text(theme.fg("dim", " LLM: ") + theme.fg("text", ms.totalCalls + " calls") + theme.fg("dim", " \u2022 ") + theme.fg("text", ms.totalInput.toLocaleString() + "t in") + theme.fg("dim", " \u2022 ") + theme.fg(cacheColor, cachePct + "% cache hit") + theme.fg("dim", " \u2022 ") + theme.fg("dim", ms.avgLatency + "ms avg"), 0, 0));
|
|
2348
|
+
}
|
|
2349
|
+
const modFiles = details.modifiedFiles;
|
|
2350
|
+
const errCount = extraction.errors.length;
|
|
2351
|
+
const resolvedErr = extraction.errors.filter((e) => e.resolved).length;
|
|
2352
|
+
const unresolvedErr = errCount - resolvedErr;
|
|
2353
|
+
c.addChild(new Text(theme.fg("dim", " Files: ") + theme.fg("success", modFiles.length + " modified") + theme.fg("dim", " \u2022 ") + theme.fg("text", details.readFiles.length + " read") + theme.fg("dim", " \u2022 ") + theme.fg("text", details.totalMessages + " messages"), 0, 0));
|
|
2354
|
+
if (errCount > 0) {
|
|
2355
|
+
c.addChild(new Text(theme.fg("dim", " Errors: ") + theme.fg("warning", errCount + " total") + theme.fg("dim", " \u2022 ") + theme.fg("success", resolvedErr + " resolved") + theme.fg("dim", " \u2022 ") + theme.fg("error", unresolvedErr + " unresolved"), 0, 0));
|
|
2356
|
+
}
|
|
2357
|
+
if (extraction.decisions.length > 0) {
|
|
2358
|
+
const expD = extraction.decisions.filter((d) => d.type === "explicit").length;
|
|
2359
|
+
const impD = extraction.decisions.filter((d) => d.type === "implicit").length;
|
|
2360
|
+
c.addChild(new Text(theme.fg("dim", " Decisions: " + extraction.decisions.length + " (" + expD + " explicit, " + impD + " implicit)"), 0, 0));
|
|
2361
|
+
}
|
|
2362
|
+
if (extraction.constraints.length > 0) {
|
|
2363
|
+
const reqC = extraction.constraints.filter((cc) => cc.category === "requirement").length;
|
|
2364
|
+
const proC = extraction.constraints.filter((cc) => cc.category === "prohibition").length;
|
|
2365
|
+
const preC = extraction.constraints.filter((cc) => cc.category === "preference").length;
|
|
2366
|
+
c.addChild(new Text(theme.fg("dim", " Constraints: " + extraction.constraints.length + " (" + reqC + " req, " + proC + " prohibit, " + preC + " pref)"), 0, 0));
|
|
2367
|
+
}
|
|
2368
|
+
c.addChild(new Text("", 0, 0));
|
|
2369
|
+
if (modFiles.length > 0) {
|
|
2370
|
+
c.addChild(new Text(theme.fg("text", theme.bold(" \uD83D\uDCC1 Modified Files")), 0, 0));
|
|
2371
|
+
const maxShow = 8;
|
|
2372
|
+
for (let i = 0;i < Math.min(modFiles.length, maxShow); i++) {
|
|
2373
|
+
const f = modFiles[i];
|
|
2374
|
+
const fc = extraction.modifiedFiles.find((e) => e.path === f);
|
|
2375
|
+
const count = fc ? " (" + fc.toolCalls + "x)" : "";
|
|
2376
|
+
c.addChild(new Text(theme.fg("success", " \u270E ") + theme.fg("text", path7.basename(f)) + theme.fg("dim", count + " \u2192 " + f), 0, 0));
|
|
2377
|
+
}
|
|
2378
|
+
if (modFiles.length > maxShow) {
|
|
2379
|
+
c.addChild(new Text(theme.fg("dim", " + " + (modFiles.length - maxShow) + " more"), 0, 0));
|
|
2380
|
+
}
|
|
2381
|
+
c.addChild(new Text("", 0, 0));
|
|
2382
|
+
}
|
|
2383
|
+
if (details.topics.length > 0) {
|
|
2384
|
+
c.addChild(new Text(theme.fg("text", theme.bold(" \uD83D\uDCE6 Topics")), 0, 0));
|
|
2385
|
+
const maxTopics = 10;
|
|
2386
|
+
for (let i = 0;i < Math.min(details.topics.length, maxTopics); i++) {
|
|
2387
|
+
c.addChild(new Text(theme.fg("dim", " " + (i + 1) + ". " + details.topics[i]), 0, 0));
|
|
2388
|
+
}
|
|
2389
|
+
if (details.topics.length > maxTopics) {
|
|
2390
|
+
c.addChild(new Text(theme.fg("dim", " + " + (details.topics.length - maxTopics) + " more"), 0, 0));
|
|
2391
|
+
}
|
|
2392
|
+
c.addChild(new Text("", 0, 0));
|
|
2393
|
+
}
|
|
2394
|
+
c.addChild(new Text(theme.fg("text", theme.bold(" \uD83D\uDD0D Verification")), 0, 0));
|
|
2395
|
+
if (details.verified) {
|
|
2396
|
+
c.addChild(new Text(theme.fg("success", " \u2705 All facts verified \u2014 no gaps detected"), 0, 0));
|
|
2397
|
+
} else if (details.gaps.length > 0) {
|
|
2398
|
+
c.addChild(new Text(theme.fg("warning", " \u26A0\uFE0F " + details.gaps.length + " gap(s) patched:"), 0, 0));
|
|
2399
|
+
for (const g of details.gaps.slice(0, 5)) {
|
|
2400
|
+
c.addChild(new Text(theme.fg("dim", " \u2022 " + g), 0, 0));
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
c.addChild(new Text("", 0, 0));
|
|
2404
|
+
c.addChild(new Text(theme.fg("text", theme.bold(" \uD83D\uDD04 Pipeline")), 0, 0));
|
|
2405
|
+
const phase1Status = theme.fg("success", "\u2713");
|
|
2406
|
+
const phase2Status = details.explorationRounds > 0 ? theme.fg("success", "\u2713 " + details.explorationRounds + " rounds") : theme.fg("warning", "\u26A0 skipped");
|
|
2407
|
+
const phase2Bounds = details.explorationBoundaries > 0 ? theme.fg("text", " (" + details.explorationBoundaries + " boundaries)") : theme.fg("dim", " (heuristic fallback)");
|
|
2408
|
+
const phase4Status = details.verified ? theme.fg("success", "\u2713 verified") : details.gaps.length > 0 ? theme.fg("warning", "\u2713 patched (" + details.gaps.length + " gaps)") : theme.fg("dim", "\u2014");
|
|
2409
|
+
c.addChild(new Text(theme.fg("dim", " Phase 1 Extract: ") + phase1Status, 0, 0));
|
|
2410
|
+
c.addChild(new Text(theme.fg("dim", " Phase 2 Explore: ") + phase2Status + phase2Bounds, 0, 0));
|
|
2411
|
+
c.addChild(new Text(theme.fg("dim", " Phase 3 Synthesize: ") + theme.fg("success", "\u2713 " + details.chunkCount + " chunks"), 0, 0));
|
|
2412
|
+
c.addChild(new Text(theme.fg("dim", " Phase 4 Verify: ") + phase4Status, 0, 0));
|
|
2413
|
+
c.addChild(new Text("", 0, 0));
|
|
2414
|
+
if (details.backupPath) {
|
|
2415
|
+
c.addChild(new Text(theme.fg("dim", " \uD83D\uDCBE Backup: " + details.backupPath), 0, 0));
|
|
2416
|
+
c.addChild(new Text("", 0, 0));
|
|
2417
|
+
}
|
|
2418
|
+
c.addChild(new Text(theme.fg("dim", " Press any key to close"), 0, 0));
|
|
2419
|
+
c.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
2420
|
+
return {
|
|
2421
|
+
render: (w) => c.render(w),
|
|
2422
|
+
invalidate: () => c.invalidate(),
|
|
2423
|
+
handleInput: (_d) => done(undefined)
|
|
2424
|
+
};
|
|
2425
|
+
}, { overlay: true, overlayOptions: { width: "70%", anchor: "center", maxHeight: "80%" } });
|
|
2426
|
+
}
|
|
2427
|
+
async function showCompactUI(ctx, opts) {
|
|
2428
|
+
const selectedModel = await selectModel(ctx, opts);
|
|
2429
|
+
if (!selectedModel)
|
|
2430
|
+
return null;
|
|
2431
|
+
const selectedProfile = await selectProfile(ctx, selectedModel, { contextTokens: opts.contextTokens, contextPercent: opts.contextPercent });
|
|
2432
|
+
if (!selectedProfile)
|
|
2433
|
+
return null;
|
|
2434
|
+
return { model: selectedModel, profile: selectedProfile };
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// src/core.ts
|
|
2438
|
+
async function runSmartCompact(ctx, summaryModel, segModel, profile, verbose, dryRun, pendingRef, isRunning, autoTriggered, userNote, skipCompact) {
|
|
2439
|
+
if (isRunning.value)
|
|
2440
|
+
return;
|
|
2441
|
+
isRunning.value = true;
|
|
2442
|
+
const pipelineStart = Date.now();
|
|
2443
|
+
resetCompactSessionId();
|
|
2444
|
+
resetMetrics();
|
|
2445
|
+
if (!summaryModel || !segModel) {
|
|
2446
|
+
isRunning.value = false;
|
|
2447
|
+
if (!autoTriggered)
|
|
2448
|
+
ctx.ui.notify("Model resolve failed", "error");
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
try {
|
|
2452
|
+
const config = loadConfig();
|
|
2453
|
+
const pc = { ...PROFILES[profile], ...config.profiles?.[profile] ?? {} };
|
|
2454
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(summaryModel);
|
|
2455
|
+
const segAuth = segModel !== summaryModel ? await ctx.modelRegistry.getApiKeyAndHeaders(segModel) : auth;
|
|
2456
|
+
if (!auth.ok || !auth.apiKey || (!segAuth.ok || !segAuth.apiKey)) {
|
|
2457
|
+
isRunning.value = false;
|
|
2458
|
+
if (!autoTriggered)
|
|
2459
|
+
ctx.ui.notify("Auth failed", "error");
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
const usage = ctx.getContextUsage();
|
|
2463
|
+
const totalTokens = usage?.tokens ?? 0;
|
|
2464
|
+
if (!totalTokens || totalTokens < 5000) {
|
|
2465
|
+
isRunning.value = false;
|
|
2466
|
+
if (!autoTriggered)
|
|
2467
|
+
ctx.ui.notify("Context OK or unknown", "info");
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const notify = (msg, type = "info") => {
|
|
2471
|
+
ctx.ui.notify(msg, type);
|
|
2472
|
+
};
|
|
2473
|
+
const ctrl = new AbortController;
|
|
2474
|
+
const signal = ctrl.signal;
|
|
2475
|
+
const modelLabel = summaryModel.provider + "/" + summaryModel.id;
|
|
2476
|
+
notify("Smart compact: " + modelLabel + ", " + profile + ", tokens=" + totalTokens, "info");
|
|
2477
|
+
notify("EESV Compact (" + modelLabel + ", " + profile + ") \u2014 " + (totalTokens ?? 0).toLocaleString() + "t", "info");
|
|
2478
|
+
const branch = ctx.sessionManager.getBranch();
|
|
2479
|
+
const msgs = branch.filter((e) => e.type === "message" && e.message != null);
|
|
2480
|
+
if (msgs.length < 3) {
|
|
2481
|
+
isRunning.value = false;
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
let accTokens = 0, keepFrom = msgs.length;
|
|
2485
|
+
for (let i = msgs.length - 1;i >= 0; i--) {
|
|
2486
|
+
const msg = msgs[i].message;
|
|
2487
|
+
const contentRaw = msg?.content;
|
|
2488
|
+
const contentText = typeof contentRaw === "string" ? contentRaw : contentRaw != null ? JSON.stringify(contentRaw) : "";
|
|
2489
|
+
accTokens += estimateTokens(contentText);
|
|
2490
|
+
if (accTokens >= pc.keepRecentTokens) {
|
|
2491
|
+
keepFrom = i;
|
|
2492
|
+
break;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
keepFrom = smartKeepBoundary(msgs, keepFrom);
|
|
2496
|
+
const toCompact = msgs.slice(0, keepFrom);
|
|
2497
|
+
if (!toCompact.length) {
|
|
2498
|
+
isRunning.value = false;
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
const firstKeptId = msgs[keepFrom]?.id ?? msgs[msgs.length - 1]?.id ?? "";
|
|
2502
|
+
if (!autoTriggered) {
|
|
2503
|
+
showProgressOverlay(ctx, { phase: 1, phaseName: "Extract", detail: "Preparing...", model: modelLabel, profile });
|
|
2504
|
+
}
|
|
2505
|
+
const llmMessages = convertToLlm(toCompact.map((e) => e.message));
|
|
2506
|
+
const pruning = pruneRedundant(llmMessages);
|
|
2507
|
+
if (pruning.prunedCount > 0) {
|
|
2508
|
+
notify("Pruning: " + pruning.prunedCount + " msgs removed (" + pruning.reasons.map((r) => r.count + "x " + r.reason).join(", ") + ")", "info");
|
|
2509
|
+
}
|
|
2510
|
+
const prunedMessages = pruning.messages;
|
|
2511
|
+
const convText = serializeConversation(prunedMessages);
|
|
2512
|
+
const convTokens = estimateTokens(convText);
|
|
2513
|
+
const sessionId = ctx.sessionManager.getSessionId?.() ?? "unknown";
|
|
2514
|
+
const backupPath = backupConversation(convText, sessionId);
|
|
2515
|
+
const prevContext = getPreviousCompactionContext(branch);
|
|
2516
|
+
const cachedExt = loadCachedExtraction(sessionId);
|
|
2517
|
+
let extraction;
|
|
2518
|
+
if (cachedExt && cachedExt.lastMessageIndex < llmMessages.length - 1) {
|
|
2519
|
+
const newMsgs = llmMessages.slice(cachedExt.lastMessageIndex + 1);
|
|
2520
|
+
const delta = extractStructured(newMsgs, pc);
|
|
2521
|
+
extraction = mergeExtractions(cachedExt.extraction, delta, cachedExt.messageCount);
|
|
2522
|
+
notify("Phase 1 Incremental: " + (cachedExt.lastMessageIndex + 1) + " cached + " + newMsgs.length + " new messages", "info");
|
|
2523
|
+
} else {
|
|
2524
|
+
extraction = extractStructured(llmMessages, pc);
|
|
2525
|
+
notify("Phase 1 Full: " + extraction.modifiedFiles.length + " files, " + extraction.errors.length + " errors", "info");
|
|
2526
|
+
}
|
|
2527
|
+
saveCachedExtraction(sessionId, extraction, llmMessages.length);
|
|
2528
|
+
const projectId = deriveProjectId(extraction);
|
|
2529
|
+
const fingerprint = loadProjectFingerprint(projectId);
|
|
2530
|
+
if (fingerprint) {
|
|
2531
|
+
notify("Project: " + fingerprint.language + (fingerprint.framework ? "/" + fingerprint.framework : "") + " (" + fingerprint.sessionCount + " sessions)", "info");
|
|
2532
|
+
}
|
|
2533
|
+
const projectCtx = buildProjectContext(fingerprint);
|
|
2534
|
+
let finalSummary;
|
|
2535
|
+
let method;
|
|
2536
|
+
let llmCalls = 0;
|
|
2537
|
+
let summaries = [];
|
|
2538
|
+
let explorationReport = null;
|
|
2539
|
+
let explorationRounds = 0;
|
|
2540
|
+
let chunkCount = 0;
|
|
2541
|
+
if (convTokens < pc.singlePassMaxTokens) {
|
|
2542
|
+
if (!autoTriggered)
|
|
2543
|
+
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Single-pass (" + convTokens.toLocaleString() + "t)", model: modelLabel, profile, extraction });
|
|
2544
|
+
try {
|
|
2545
|
+
const r = await singlePassCompact(convText, extraction, null, prevContext + projectCtx, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
|
|
2546
|
+
finalSummary = r.summary;
|
|
2547
|
+
method = "single-pass";
|
|
2548
|
+
llmCalls = r.llmCalls;
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
notify("Single-pass failed: " + (err instanceof Error ? err.message : String(err)), "warning");
|
|
2551
|
+
finalSummary = assembleFallback([], extraction);
|
|
2552
|
+
method = "heuristic";
|
|
2553
|
+
llmCalls = 0;
|
|
2554
|
+
}
|
|
2555
|
+
} else {
|
|
2556
|
+
const needsExploration = shouldExplore(extraction);
|
|
2557
|
+
if (needsExploration) {
|
|
2558
|
+
if (!autoTriggered)
|
|
2559
|
+
showProgressOverlay(ctx, { phase: 2, phaseName: "Explore", detail: "Exploring...", model: modelLabel, profile, extraction });
|
|
2560
|
+
try {
|
|
2561
|
+
const expResult = await exploreConversation(llmMessages, extraction, segModel, { apiKey: segAuth.apiKey, headers: segAuth.headers }, prevContext || undefined, userNote, signal, 8, notify);
|
|
2562
|
+
explorationReport = expResult.report;
|
|
2563
|
+
explorationRounds = expResult.rounds;
|
|
2564
|
+
notify("Phase 2 Explore: " + expResult.rounds + " rounds, " + explorationReport.boundaries.length + " boundaries" + (expResult.toolSupported ? "" : " (no tool support)"), "info");
|
|
2565
|
+
} catch (err) {
|
|
2566
|
+
notify("Phase 2 Explore: failed - " + (err instanceof Error ? err.message : String(err)), "warning");
|
|
2567
|
+
}
|
|
2568
|
+
} else {
|
|
2569
|
+
notify("Phase 2 Explore: skipped (simple session: " + extraction.topics.length + " topics, " + extraction.errors.filter((e) => !e.resolved).length + " unresolved errors)", "info");
|
|
2570
|
+
}
|
|
2571
|
+
let boundaries;
|
|
2572
|
+
if (explorationReport?.boundaries.length) {
|
|
2573
|
+
const llmBounds = explorationReport.boundaries.filter((b) => b.confidence >= 0.4);
|
|
2574
|
+
const heuristicBounds = extraction.topics.map((t) => ({
|
|
2575
|
+
afterIndex: t.endIndex,
|
|
2576
|
+
topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
|
|
2577
|
+
priority: t.errorDensity > 2 ? "high" : "normal",
|
|
2578
|
+
confidence: 0.6
|
|
2579
|
+
}));
|
|
2580
|
+
if (llmBounds.length > 0) {
|
|
2581
|
+
const merged = [...llmBounds];
|
|
2582
|
+
for (const hb of heuristicBounds) {
|
|
2583
|
+
const nearby = merged.find((m) => Math.abs(m.afterIndex - hb.afterIndex) <= 3);
|
|
2584
|
+
if (!nearby)
|
|
2585
|
+
merged.push(hb);
|
|
2586
|
+
}
|
|
2587
|
+
boundaries = merged.sort((a, b) => a.afterIndex - b.afterIndex);
|
|
2588
|
+
} else {
|
|
2589
|
+
boundaries = heuristicBounds;
|
|
2590
|
+
}
|
|
2591
|
+
} else {
|
|
2592
|
+
boundaries = extraction.topics.map((t) => ({
|
|
2593
|
+
afterIndex: t.endIndex,
|
|
2594
|
+
topic: t.primaryFile ? "Working on " + t.primaryFile.split("/").pop() : "Segment",
|
|
2595
|
+
priority: t.errorDensity > 2 ? "high" : "normal",
|
|
2596
|
+
confidence: 0.6
|
|
2597
|
+
}));
|
|
2598
|
+
}
|
|
2599
|
+
const chunks = chunkLlmMessages(llmMessages, boundaries, pc);
|
|
2600
|
+
chunkCount = chunks.length;
|
|
2601
|
+
notify("Chunked: " + chunkCount + " chunks", "info");
|
|
2602
|
+
const batches = createBatches(chunks, pc.batchMaxTokens);
|
|
2603
|
+
const totalBatches = batches.length;
|
|
2604
|
+
if (!autoTriggered)
|
|
2605
|
+
showProgressOverlay(ctx, { phase: 3, phaseName: "Synthesize", detail: "0/" + totalBatches + " batches", model: modelLabel, profile, extraction, totalBatches });
|
|
2606
|
+
const caps = getProviderCaps(summaryModel.provider);
|
|
2607
|
+
const concurrency = caps.concurrencyLimit;
|
|
2608
|
+
if (totalBatches <= 1) {
|
|
2609
|
+
try {
|
|
2610
|
+
summaries.push(...await summarizeBatch(batches[0], extraction, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal));
|
|
2611
|
+
} catch (err) {
|
|
2612
|
+
summaries.push(...batches[0].map((ch) => ({
|
|
2613
|
+
topic: ch.topic,
|
|
2614
|
+
startIndex: ch.startIndex,
|
|
2615
|
+
endIndex: ch.endIndex,
|
|
2616
|
+
summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
|
|
2617
|
+
`).slice(0, 300),
|
|
2618
|
+
keyDecisions: [],
|
|
2619
|
+
filesModified: [],
|
|
2620
|
+
filesRead: [],
|
|
2621
|
+
priority: ch.priority
|
|
2622
|
+
})));
|
|
2623
|
+
}
|
|
2624
|
+
} else {
|
|
2625
|
+
const results = new Array(totalBatches);
|
|
2626
|
+
const errors = new Array(totalBatches).fill(null);
|
|
2627
|
+
let completed = 0;
|
|
2628
|
+
for (let wave = 0;wave < totalBatches; wave += concurrency) {
|
|
2629
|
+
const waveBatches = batches.slice(wave, Math.min(wave + concurrency, totalBatches));
|
|
2630
|
+
const wavePromises = waveBatches.map(async (batch, i) => {
|
|
2631
|
+
const idx = wave + i;
|
|
2632
|
+
try {
|
|
2633
|
+
results[idx] = await summarizeBatch(batch, extraction, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
|
|
2634
|
+
} catch (err) {
|
|
2635
|
+
errors[idx] = err instanceof Error ? err : new Error(String(err));
|
|
2636
|
+
results[idx] = batch.map((ch) => ({
|
|
2637
|
+
topic: ch.topic,
|
|
2638
|
+
startIndex: ch.startIndex,
|
|
2639
|
+
endIndex: ch.endIndex,
|
|
2640
|
+
summary: "[Failed] " + ch.messages.map((m) => extractText(m.content)).join(`
|
|
2641
|
+
`).slice(0, 300),
|
|
2642
|
+
keyDecisions: [],
|
|
2643
|
+
filesModified: [],
|
|
2644
|
+
filesRead: [],
|
|
2645
|
+
priority: ch.priority
|
|
2646
|
+
}));
|
|
2647
|
+
}
|
|
2648
|
+
completed++;
|
|
2649
|
+
if (!autoTriggered)
|
|
2650
|
+
showProgressOverlay(ctx, { phase: 3, phaseName: "Synthesize", detail: completed + "/" + totalBatches + " batches", model: modelLabel, profile, extraction, totalBatches, currentBatch: completed });
|
|
2651
|
+
});
|
|
2652
|
+
await Promise.all(wavePromises);
|
|
2653
|
+
}
|
|
2654
|
+
for (const r of results)
|
|
2655
|
+
if (r)
|
|
2656
|
+
summaries.push(...r);
|
|
2657
|
+
for (let i = 0;i < errors.length; i++)
|
|
2658
|
+
if (errors[i])
|
|
2659
|
+
notify("Batch " + (i + 1) + " failed: " + errors[i].message, "warning");
|
|
2660
|
+
}
|
|
2661
|
+
if (!autoTriggered)
|
|
2662
|
+
showProgressOverlay(ctx, { phase: 3, phaseName: "Synthesize", detail: "Assembling...", model: modelLabel, profile, extraction, totalBatches: batches.length });
|
|
2663
|
+
let assemblyCalls = 1;
|
|
2664
|
+
try {
|
|
2665
|
+
const r = await assembleLLM(summaries, extraction, explorationReport, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, pc.summaryBudgetTokens, prevContext, signal);
|
|
2666
|
+
if (r?.startsWith("##"))
|
|
2667
|
+
finalSummary = r;
|
|
2668
|
+
else
|
|
2669
|
+
throw new Error("bad");
|
|
2670
|
+
} catch (err) {
|
|
2671
|
+
console.error("[smart-compact] Assembly failed:", err instanceof Error ? err.message : err);
|
|
2672
|
+
finalSummary = assembleFallback(summaries, extraction);
|
|
2673
|
+
assemblyCalls = 0;
|
|
2674
|
+
}
|
|
2675
|
+
method = "eesv";
|
|
2676
|
+
llmCalls = explorationRounds + batches.length + assemblyCalls;
|
|
2677
|
+
}
|
|
2678
|
+
if (!autoTriggered)
|
|
2679
|
+
showProgressOverlay(ctx, { phase: 4, phaseName: "Verify", detail: "Checking...", model: modelLabel, profile, extraction, explorationRounds });
|
|
2680
|
+
const verification = verifySummary(finalSummary, extraction);
|
|
2681
|
+
if (!verification.ok) {
|
|
2682
|
+
if (verification.score < 85) {
|
|
2683
|
+
notify("Phase 4 Verify: " + verification.gaps.length + " gap(s), score=" + verification.score + ", applying deterministic patch", "warning");
|
|
2684
|
+
finalSummary = patchDeterministic(finalSummary, verification.gaps, extraction);
|
|
2685
|
+
const recheck = verifySummary(finalSummary, extraction);
|
|
2686
|
+
if (!recheck.ok && recheck.score < 75) {
|
|
2687
|
+
notify("Phase 4 Verify: deterministic patch insufficient (score=" + recheck.score + "), trying LLM patch", "warning");
|
|
2688
|
+
try {
|
|
2689
|
+
finalSummary = await patchSummary(finalSummary, recheck.gaps, summaryModel, { apiKey: auth.apiKey, headers: auth.headers }, signal);
|
|
2690
|
+
llmCalls++;
|
|
2691
|
+
} catch (err) {
|
|
2692
|
+
console.error("[smart-compact] LLM patch failed:", err instanceof Error ? err.message : err);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
} else {
|
|
2696
|
+
notify("Phase 4 Verify: " + verification.gaps.length + " gap(s), score=" + verification.score + " \u2265 85 \u2014 skipping patch", "info");
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
const detModified = extraction.modifiedFiles.map((f) => f.path);
|
|
2700
|
+
const detRead = extraction.readFiles;
|
|
2701
|
+
const estimatedAfter = estimateTokens(finalSummary) + accTokens;
|
|
2702
|
+
const tokensSaved = Math.max(0, totalTokens - estimatedAfter);
|
|
2703
|
+
const pipelineInfo = method === "eesv" ? "EESV: Extract > Explore (" + explorationRounds + "r) > Synthesize (" + (chunkCount || 1) + " chunks) > Verify (" + (verification.ok ? "pass" : verification.gaps.length + " gaps") + ")" : method + " (" + (chunkCount || 1) + " chunks, " + llmCalls + " calls)";
|
|
2704
|
+
const pipelineMs = Date.now() - pipelineStart;
|
|
2705
|
+
const durationStr = pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s";
|
|
2706
|
+
notify("Done: " + pipelineInfo + " \u2014 saved " + (tokensSaved ?? 0).toLocaleString() + "t (" + durationStr + ")", "success");
|
|
2707
|
+
const openLoops = extractOpenLoops(llmMessages, extraction);
|
|
2708
|
+
if (openLoops.length > 0) {
|
|
2709
|
+
notify("Open Loops: " + openLoops.length + " detected (" + openLoops.filter((l) => l.priority === "high").length + " high)", "info");
|
|
2710
|
+
finalSummary = injectOpenLoopsSection(finalSummary, openLoops);
|
|
2711
|
+
}
|
|
2712
|
+
const nextActions = extractNextActions(finalSummary);
|
|
2713
|
+
const criticalContextItems = extractCriticalContext(finalSummary);
|
|
2714
|
+
const compactionState = buildCompactionState(extraction, openLoops, explorationReport, nextActions, criticalContextItems);
|
|
2715
|
+
const prevState = loadCompactionState(projectId);
|
|
2716
|
+
if (prevState) {
|
|
2717
|
+
const delta = computeDelta(prevState, compactionState);
|
|
2718
|
+
if (delta.newLoops.length || delta.resolvedLoops.length || delta.newDecisions.length || delta.newErrors.length || delta.newModifiedFiles.length) {
|
|
2719
|
+
finalSummary = injectDeltaSection(finalSummary, delta);
|
|
2720
|
+
notify("Delta: " + delta.newLoops.length + " new loops, " + delta.resolvedLoops.length + " resolved, " + delta.newModifiedFiles.length + " new files", "info");
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
const details = {
|
|
2724
|
+
method,
|
|
2725
|
+
chunkCount: chunkCount || 1,
|
|
2726
|
+
topics: summaries.length ? summaries.map((s) => s.topic) : [method],
|
|
2727
|
+
readFiles: detRead,
|
|
2728
|
+
modifiedFiles: detModified,
|
|
2729
|
+
totalMessages: toCompact.length,
|
|
2730
|
+
totalTokensSummarized: convTokens,
|
|
2731
|
+
llmCalls,
|
|
2732
|
+
profile,
|
|
2733
|
+
backupPath,
|
|
2734
|
+
tokensSaved,
|
|
2735
|
+
verified: verification.ok,
|
|
2736
|
+
gaps: verification.gaps,
|
|
2737
|
+
explorationRounds,
|
|
2738
|
+
explorationBoundaries: explorationReport?.boundaries.length ?? 0,
|
|
2739
|
+
model: modelLabel,
|
|
2740
|
+
qualityScore: verification.score,
|
|
2741
|
+
tokensBefore: totalTokens,
|
|
2742
|
+
compactionState,
|
|
2743
|
+
openLoops
|
|
2744
|
+
};
|
|
2745
|
+
if (dryRun) {
|
|
2746
|
+
notify("DRY RUN (" + method + ", " + profile + ") \u2014 " + toCompact.length + " msgs, " + llmCalls + " calls", "info");
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
pendingRef.value = { summary: finalSummary, firstKeptEntryId: firstKeptId, tokensBefore: totalTokens, details, compactionState };
|
|
2750
|
+
pendingRef.createdAt = Date.now();
|
|
2751
|
+
saveProjectFingerprint(projectId, extraction);
|
|
2752
|
+
saveCompactionState(projectId, compactionState);
|
|
2753
|
+
appendMetricsLog(sessionId);
|
|
2754
|
+
try {
|
|
2755
|
+
const postCompactMsgs = msgs.slice(keepFrom).map((e) => convertToLlm([e.message])).flat().map((m) => m);
|
|
2756
|
+
if (postCompactMsgs.length > 2) {
|
|
2757
|
+
const lastCompaction = branch.filter((e) => e.type === "compaction").slice(-1)[0];
|
|
2758
|
+
if (lastCompaction?.details) {
|
|
2759
|
+
const prevDetails = lastCompaction.details;
|
|
2760
|
+
const damage = detectDamage(postCompactMsgs.slice(0, Math.min(15, postCompactMsgs.length)), prevDetails);
|
|
2761
|
+
if (damage.damageScore > 0) {
|
|
2762
|
+
notify("Previous compaction damage: " + damage.summary, "warning");
|
|
2763
|
+
}
|
|
2764
|
+
logDamageReport(sessionId, damage, prevDetails);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
} catch (err) {
|
|
2768
|
+
console.error("[smart-compact] Damage detection error:", err instanceof Error ? err.message : err);
|
|
2769
|
+
}
|
|
2770
|
+
const ms = getMetricsSummary();
|
|
2771
|
+
if (ms.totalCalls > 0) {
|
|
2772
|
+
notify("Metrics: " + ms.totalCalls + " calls, " + ms.totalInput + "t in, " + ms.totalOutput + "t out, cache " + Math.round(ms.cacheHitRate * 100) + "%, " + ms.avgLatency + "ms avg", "info");
|
|
2773
|
+
}
|
|
2774
|
+
if (!autoTriggered) {
|
|
2775
|
+
try {
|
|
2776
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
2777
|
+
await Promise.race([showResultScreen(ctx, details, extraction), timeout]);
|
|
2778
|
+
} catch (err) {
|
|
2779
|
+
console.error("[smart-compact] Result screen error:", err instanceof Error ? err.message : err);
|
|
2780
|
+
notify("Result screen skipped", "info");
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
if (!skipCompact) {
|
|
2784
|
+
ctx.compact({
|
|
2785
|
+
customInstructions: "Use pre-computed smart summary from /smart-compact",
|
|
2786
|
+
onComplete: () => {
|
|
2787
|
+
if (!autoTriggered)
|
|
2788
|
+
ctx.ui.notify("Applied \u2713", "success");
|
|
2789
|
+
},
|
|
2790
|
+
onError: (e) => {
|
|
2791
|
+
if (!autoTriggered)
|
|
2792
|
+
ctx.ui.notify("Failed: " + e.message, "error");
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
} finally {
|
|
2797
|
+
isRunning.value = false;
|
|
2798
|
+
const pipelineMs = Date.now() - pipelineStart;
|
|
2799
|
+
if (autoTriggered) {
|
|
2800
|
+
ctx.ui.notify("Compaction completed in " + (pipelineMs < 1000 ? pipelineMs + "ms" : (pipelineMs / 1000).toFixed(1) + "s"), "info");
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/index.ts
|
|
2806
|
+
function resolveModelArg(ctx, modelArg) {
|
|
2807
|
+
const [p, ...r] = modelArg.split("/");
|
|
2808
|
+
return ctx.modelRegistry.find(p, r.join("/"));
|
|
2809
|
+
}
|
|
2810
|
+
function resolveModels(ctx, primary, config) {
|
|
2811
|
+
const fallback = primary ?? ctx.model;
|
|
2812
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
2813
|
+
let sumModel = fallback;
|
|
2814
|
+
const configuredSumModels = [config.summaryModel].filter(Boolean);
|
|
2815
|
+
for (const modelId of configuredSumModels) {
|
|
2816
|
+
const [p, ...r] = modelId.split("/");
|
|
2817
|
+
const found = ctx.modelRegistry.find(p, r.join("/"));
|
|
2818
|
+
if (found) {
|
|
2819
|
+
sumModel = found;
|
|
2820
|
+
break;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
if (sumModel === fallback && !fallback)
|
|
2824
|
+
sumModel = available[0];
|
|
2825
|
+
let segModel = sumModel;
|
|
2826
|
+
if (config.segmentationModel) {
|
|
2827
|
+
const [p, ...r] = config.segmentationModel.split("/");
|
|
2828
|
+
segModel = ctx.modelRegistry.find(p, r.join("/")) ?? sumModel;
|
|
2829
|
+
}
|
|
2830
|
+
return { segModel, sumModel };
|
|
2831
|
+
}
|
|
2832
|
+
function smartCompactExtension(pi) {
|
|
2833
|
+
const pendingRef = { value: null, createdAt: 0 };
|
|
2834
|
+
const isRunning = { value: false };
|
|
2835
|
+
const PENDING_TTL_MS = 5 * 60 * 1000;
|
|
2836
|
+
pi.registerCommand("smart-compact", {
|
|
2837
|
+
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model] [light|balanced|aggressive] [verbose|debug|dry-run] [note]",
|
|
2838
|
+
getArgumentCompletions: (prefix) => {
|
|
2839
|
+
const m = ["verbose", "debug", "dry-run", "light", "balanced", "aggressive"].filter((o) => o.startsWith(prefix)).map((o) => ({ value: o, label: o }));
|
|
2840
|
+
return m.length ? m : null;
|
|
2841
|
+
},
|
|
2842
|
+
handler: async (args, ctx) => {
|
|
2843
|
+
try {
|
|
2844
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
2845
|
+
const flags = tokens.map((t) => t.toLowerCase());
|
|
2846
|
+
const verbose = flags.includes("verbose") || flags.includes("debug");
|
|
2847
|
+
const dryRun = flags.includes("dry-run");
|
|
2848
|
+
const modelArg = tokens.find((t) => t.includes("/"));
|
|
2849
|
+
const profileArg = tokens.find((t) => ["light", "balanced", "aggressive"].includes(t));
|
|
2850
|
+
const profile = profileArg ?? loadConfig().profile;
|
|
2851
|
+
if (!tokens.length) {
|
|
2852
|
+
const usage = ctx.getContextUsage();
|
|
2853
|
+
const totalTokens = usage?.tokens ?? 0;
|
|
2854
|
+
const pct = ctx.model && totalTokens ? Math.round(totalTokens / ctx.model.contextWindow * 100) : 0;
|
|
2855
|
+
if (!totalTokens || totalTokens < 5000) {
|
|
2856
|
+
ctx.ui.notify("Context OK or unknown", "info");
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
const cur = ctx.model;
|
|
2860
|
+
const avail = ctx.modelRegistry.getAvailable();
|
|
2861
|
+
const opts = avail.map((m) => ({ value: m.provider + "/" + m.id, label: m.provider + "/" + m.id + (m.contextWindow >= 200000 ? " (" + Math.round(m.contextWindow / 1000) + "K)" : ""), model: m }));
|
|
2862
|
+
const defIdx = cur ? opts.findIndex((o) => o.value === cur.provider + "/" + cur.id) : 0;
|
|
2863
|
+
const selected = await showCompactUI(ctx, { contextTokens: totalTokens, contextPercent: pct, currentModel: cur ? cur.provider + "/" + cur.id : "?", defaultModelIndex: defIdx >= 0 ? defIdx : 0 });
|
|
2864
|
+
if (!selected) {
|
|
2865
|
+
ctx.ui.notify("Cancelled", "info");
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
const { segModel: segModel2, sumModel: sumModel2 } = resolveModels(ctx, selected.model.model, loadConfig());
|
|
2869
|
+
if (!sumModel2) {
|
|
2870
|
+
ctx.ui.notify("Could not resolve model", "error");
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
await runSmartCompact(ctx, sumModel2, segModel2 ?? sumModel2, selected.profile, false, false, pendingRef, isRunning, false);
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
const { segModel, sumModel } = resolveModels(ctx, modelArg ? resolveModelArg(ctx, modelArg) : ctx.model, loadConfig());
|
|
2877
|
+
if (!sumModel) {
|
|
2878
|
+
ctx.ui.notify("Could not resolve model", "error");
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
const note = extractUserNote(args);
|
|
2882
|
+
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, profile, verbose, dryRun, pendingRef, isRunning, false, note);
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
const msg = error instanceof Error ? error.message + `
|
|
2885
|
+
` + error.stack : String(error);
|
|
2886
|
+
ctx.ui.notify("smart-compact error: " + msg, "error");
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
pi.on("session_before_compact", async (_event, ctx) => {
|
|
2891
|
+
if (pendingRef.value) {
|
|
2892
|
+
const age = Date.now() - pendingRef.createdAt;
|
|
2893
|
+
if (age > PENDING_TTL_MS) {
|
|
2894
|
+
pendingRef.value = null;
|
|
2895
|
+
pendingRef.createdAt = 0;
|
|
2896
|
+
} else {
|
|
2897
|
+
const c = pendingRef.value;
|
|
2898
|
+
pendingRef.value = null;
|
|
2899
|
+
pendingRef.createdAt = 0;
|
|
2900
|
+
return { compaction: { summary: c.summary, firstKeptEntryId: c.firstKeptEntryId, tokensBefore: c.tokensBefore, details: c.details } };
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
const config = loadConfig();
|
|
2904
|
+
if (!config.autoTrigger)
|
|
2905
|
+
return;
|
|
2906
|
+
try {
|
|
2907
|
+
const usage = ctx.getContextUsage();
|
|
2908
|
+
const totalTokens = usage?.tokens ?? 0;
|
|
2909
|
+
if (!totalTokens || totalTokens < 5000)
|
|
2910
|
+
return;
|
|
2911
|
+
const cur = ctx.model;
|
|
2912
|
+
if (!cur)
|
|
2913
|
+
return;
|
|
2914
|
+
const { segModel, sumModel } = resolveModels(ctx, cur, config);
|
|
2915
|
+
if (!sumModel)
|
|
2916
|
+
return;
|
|
2917
|
+
if (!isRunning.value) {
|
|
2918
|
+
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, config.profile, false, false, pendingRef, isRunning, true);
|
|
2919
|
+
if (pendingRef.value) {
|
|
2920
|
+
const c = pendingRef.value;
|
|
2921
|
+
pendingRef.value = null;
|
|
2922
|
+
pendingRef.createdAt = 0;
|
|
2923
|
+
return { compaction: { summary: c.summary, firstKeptEntryId: c.firstKeptEntryId, tokensBefore: c.tokensBefore, details: c.details } };
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
} catch {}
|
|
2927
|
+
});
|
|
2928
|
+
pi.registerTool({
|
|
2929
|
+
name: "smart_compact",
|
|
2930
|
+
label: "Smart Compact",
|
|
2931
|
+
description: "EESV smart compaction v" + VERSION + " with deterministic extraction, exploration, and verification.",
|
|
2932
|
+
promptSnippet: "Smart compaction",
|
|
2933
|
+
promptGuidelines: ["Use for long conversations.", "Prefer over default compact."],
|
|
2934
|
+
parameters: {
|
|
2935
|
+
type: "object",
|
|
2936
|
+
properties: {
|
|
2937
|
+
profile: { type: "string", description: "light, balanced, or aggressive" },
|
|
2938
|
+
verbose: { type: "boolean" },
|
|
2939
|
+
dry_run: { type: "boolean" }
|
|
2940
|
+
}
|
|
2941
|
+
},
|
|
2942
|
+
async execute(_id, params, _sig, _onUp, ctx) {
|
|
2943
|
+
const profile = params.profile === "light" || params.profile === "balanced" || params.profile === "aggressive" ? params.profile : undefined;
|
|
2944
|
+
const verbose = !!params.verbose;
|
|
2945
|
+
const dryRun = !!params.dry_run;
|
|
2946
|
+
const config = loadConfig();
|
|
2947
|
+
const resolvedProfile = profile ?? config.profile;
|
|
2948
|
+
const cur = "model" in ctx ? ctx.model : undefined;
|
|
2949
|
+
const { segModel, sumModel } = resolveModels(ctx, cur, config);
|
|
2950
|
+
if (!sumModel) {
|
|
2951
|
+
return { content: [{ type: "text", text: "Error: Could not resolve model." }] };
|
|
2952
|
+
}
|
|
2953
|
+
try {
|
|
2954
|
+
const toolStart = Date.now();
|
|
2955
|
+
await runSmartCompact(ctx, sumModel, segModel ?? sumModel, resolvedProfile, verbose, dryRun, pendingRef, isRunning, true, undefined, true);
|
|
2956
|
+
const toolSecs = ((Date.now() - toolStart) / 1000).toFixed(1);
|
|
2957
|
+
if (pendingRef.value) {
|
|
2958
|
+
return { content: [{ type: "text", text: "Smart summary generated (" + resolvedProfile + "). Tokens: " + (pendingRef.value.tokensBefore ?? "?") + " -> " + (pendingRef.value.summary?.length ?? 0) + " chars (" + toolSecs + `s).
|
|
2959
|
+
|
|
2960
|
+
Now run tree compact to apply \u2014 the session_before_compact hook will use this summary.
|
|
2961
|
+
TTL: ` + Math.round(PENDING_TTL_MS / 60000) + " minutes." }] };
|
|
2962
|
+
}
|
|
2963
|
+
return { content: [{ type: "text", text: "Compaction finished (" + resolvedProfile + ") but no summary was generated." }] };
|
|
2964
|
+
} catch (error) {
|
|
2965
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2966
|
+
return { content: [{ type: "text", text: "Compaction error: " + msg }] };
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
function extractUserNote(args) {
|
|
2972
|
+
const SKIP = new Set(["verbose", "debug", "dry-run", "light", "balanced", "aggressive"]);
|
|
2973
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
2974
|
+
const nonFlags = tokens.filter((t) => !t.includes("/") && !SKIP.has(t.toLowerCase()));
|
|
2975
|
+
return nonFlags.length > 0 ? nonFlags.join(" ") : undefined;
|
|
2976
|
+
}
|
|
2977
|
+
export {
|
|
2978
|
+
smartCompactExtension as default
|
|
2979
|
+
};
|