squads-cli 0.4.10 → 0.4.11
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 +66 -2
- package/dist/cli.js +868 -241
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +569 -5
- package/dist/index.js +1030 -0
- package/dist/index.js.map +1 -1
- package/docker/docker-compose.engram.yml +55 -66
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,1037 @@ import { createRequire } from "module";
|
|
|
3
3
|
var require2 = createRequire(import.meta.url);
|
|
4
4
|
var pkg = require2("../package.json");
|
|
5
5
|
var version = pkg.version;
|
|
6
|
+
|
|
7
|
+
// src/lib/squad-parser.ts
|
|
8
|
+
import { readFileSync, existsSync, readdirSync, writeFileSync } from "fs";
|
|
9
|
+
import { join, basename } from "path";
|
|
10
|
+
import matter from "gray-matter";
|
|
11
|
+
function findSquadsDir() {
|
|
12
|
+
let dir = process.cwd();
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
const squadsPath = join(dir, ".agents", "squads");
|
|
15
|
+
if (existsSync(squadsPath)) {
|
|
16
|
+
return squadsPath;
|
|
17
|
+
}
|
|
18
|
+
const parent = join(dir, "..");
|
|
19
|
+
if (parent === dir) break;
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function findProjectRoot() {
|
|
25
|
+
const squadsDir = findSquadsDir();
|
|
26
|
+
if (!squadsDir) return null;
|
|
27
|
+
return join(squadsDir, "..", "..");
|
|
28
|
+
}
|
|
29
|
+
function listSquads(squadsDir) {
|
|
30
|
+
const squads = [];
|
|
31
|
+
const entries = readdirSync(squadsDir, { withFileTypes: true });
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.isDirectory() && !entry.name.startsWith("_")) {
|
|
34
|
+
const squadFile = join(squadsDir, entry.name, "SQUAD.md");
|
|
35
|
+
if (existsSync(squadFile)) {
|
|
36
|
+
squads.push(entry.name);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return squads;
|
|
41
|
+
}
|
|
42
|
+
function listAgents(squadsDir, squadName) {
|
|
43
|
+
const agents = [];
|
|
44
|
+
const dirs = squadName ? [squadName] : readdirSync(squadsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_")).map((e) => e.name);
|
|
45
|
+
for (const dir of dirs) {
|
|
46
|
+
const squadPath = join(squadsDir, dir);
|
|
47
|
+
if (!existsSync(squadPath)) continue;
|
|
48
|
+
const files = readdirSync(squadPath);
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
if (file.endsWith(".md") && file !== "SQUAD.md") {
|
|
51
|
+
const agentName = file.replace(".md", "");
|
|
52
|
+
agents.push({
|
|
53
|
+
name: agentName,
|
|
54
|
+
role: `Agent in ${dir}`,
|
|
55
|
+
trigger: "manual",
|
|
56
|
+
filePath: join(squadPath, file)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return agents;
|
|
62
|
+
}
|
|
63
|
+
function parseSquadFile(filePath) {
|
|
64
|
+
const rawContent = readFileSync(filePath, "utf-8");
|
|
65
|
+
const { data: frontmatter, content: bodyContent } = matter(rawContent);
|
|
66
|
+
const fm = frontmatter;
|
|
67
|
+
const lines = bodyContent.split("\n");
|
|
68
|
+
const squad = {
|
|
69
|
+
name: fm.name || basename(filePath).replace(".md", ""),
|
|
70
|
+
mission: fm.mission || "",
|
|
71
|
+
agents: [],
|
|
72
|
+
pipelines: [],
|
|
73
|
+
triggers: { scheduled: [], event: [], manual: [] },
|
|
74
|
+
dependencies: [],
|
|
75
|
+
outputPath: "",
|
|
76
|
+
goals: [],
|
|
77
|
+
// Apply frontmatter fields
|
|
78
|
+
effort: fm.effort,
|
|
79
|
+
context: fm.context,
|
|
80
|
+
repo: fm.repo,
|
|
81
|
+
stack: fm.stack
|
|
82
|
+
};
|
|
83
|
+
let currentSection = "";
|
|
84
|
+
let inTable = false;
|
|
85
|
+
let tableHeaders = [];
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line.startsWith("# Squad:")) {
|
|
88
|
+
squad.name = line.replace("# Squad:", "").trim().toLowerCase();
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (line.startsWith("## ")) {
|
|
92
|
+
currentSection = line.replace("## ", "").trim().toLowerCase();
|
|
93
|
+
inTable = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (currentSection === "mission" && line.trim() && !line.startsWith("#")) {
|
|
97
|
+
if (!squad.mission) {
|
|
98
|
+
squad.mission = line.trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const effortMatch = line.match(/^effort:\s*(high|medium|low)/i);
|
|
102
|
+
if (effortMatch && !squad.effort) {
|
|
103
|
+
squad.effort = effortMatch[1].toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
if (currentSection.includes("agent") || currentSection.includes("orchestrator") || currentSection.includes("evaluator") || currentSection.includes("builder") || currentSection.includes("priority")) {
|
|
106
|
+
if (line.includes("|") && line.includes("Agent")) {
|
|
107
|
+
inTable = true;
|
|
108
|
+
tableHeaders = line.split("|").map((h) => h.trim().toLowerCase());
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (inTable && line.includes("|") && !line.includes("---")) {
|
|
112
|
+
const cells = line.split("|").map((c) => c.trim().replace(/`/g, ""));
|
|
113
|
+
const agentIdx = tableHeaders.findIndex((h) => h === "agent");
|
|
114
|
+
const roleIdx = tableHeaders.findIndex((h) => h === "role");
|
|
115
|
+
const triggerIdx = tableHeaders.findIndex((h) => h === "trigger");
|
|
116
|
+
const statusIdx = tableHeaders.findIndex((h) => h === "status");
|
|
117
|
+
const effortIdx = tableHeaders.findIndex((h) => h === "effort");
|
|
118
|
+
if (agentIdx >= 0 && cells[agentIdx]) {
|
|
119
|
+
const effortValue = effortIdx >= 0 ? cells[effortIdx]?.toLowerCase() : void 0;
|
|
120
|
+
const effort = ["high", "medium", "low"].includes(effortValue || "") ? effortValue : void 0;
|
|
121
|
+
squad.agents.push({
|
|
122
|
+
name: cells[agentIdx],
|
|
123
|
+
role: roleIdx >= 0 ? cells[roleIdx] : "",
|
|
124
|
+
trigger: triggerIdx >= 0 ? cells[triggerIdx] : "manual",
|
|
125
|
+
status: statusIdx >= 0 ? cells[statusIdx] : "active",
|
|
126
|
+
effort
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (line.includes("\u2192") && line.includes("`")) {
|
|
132
|
+
const pipelineMatch = line.match(/`([^`]+)`\s*→\s*`([^`]+)`/g);
|
|
133
|
+
if (pipelineMatch) {
|
|
134
|
+
const agentNames = line.match(/`([^`]+)`/g)?.map((m) => m.replace(/`/g, "")) || [];
|
|
135
|
+
if (agentNames.length >= 2) {
|
|
136
|
+
squad.pipelines.push({
|
|
137
|
+
name: "default",
|
|
138
|
+
agents: agentNames
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (line.toLowerCase().includes("pipeline:")) {
|
|
144
|
+
const pipelineContent = line.split(":")[1];
|
|
145
|
+
if (pipelineContent && pipelineContent.includes("\u2192")) {
|
|
146
|
+
const agentNames = pipelineContent.match(/`([^`]+)`/g)?.map((m) => m.replace(/`/g, "")) || [];
|
|
147
|
+
if (agentNames.length >= 2) {
|
|
148
|
+
squad.pipelines.push({
|
|
149
|
+
name: "default",
|
|
150
|
+
agents: agentNames
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (line.toLowerCase().includes("primary") && line.includes("`")) {
|
|
156
|
+
const match = line.match(/`([^`]+)`/);
|
|
157
|
+
if (match) {
|
|
158
|
+
squad.outputPath = match[1].replace(/\/$/, "");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (currentSection === "goals") {
|
|
162
|
+
const goalMatch = line.match(/^-\s*\[([ x])\]\s*(.+)$/);
|
|
163
|
+
if (goalMatch) {
|
|
164
|
+
const completed = goalMatch[1] === "x";
|
|
165
|
+
let description = goalMatch[2].trim();
|
|
166
|
+
let progress;
|
|
167
|
+
const progressMatch = description.match(/\(progress:\s*([^)]+)\)/i);
|
|
168
|
+
if (progressMatch) {
|
|
169
|
+
progress = progressMatch[1];
|
|
170
|
+
description = description.replace(progressMatch[0], "").trim();
|
|
171
|
+
}
|
|
172
|
+
squad.goals.push({
|
|
173
|
+
description,
|
|
174
|
+
completed,
|
|
175
|
+
progress
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return squad;
|
|
181
|
+
}
|
|
182
|
+
function loadSquad(squadName) {
|
|
183
|
+
const squadsDir = findSquadsDir();
|
|
184
|
+
if (!squadsDir) return null;
|
|
185
|
+
const squadFile = join(squadsDir, squadName, "SQUAD.md");
|
|
186
|
+
if (!existsSync(squadFile)) return null;
|
|
187
|
+
return parseSquadFile(squadFile);
|
|
188
|
+
}
|
|
189
|
+
function loadAgentDefinition(agentPath) {
|
|
190
|
+
if (!existsSync(agentPath)) return "";
|
|
191
|
+
return readFileSync(agentPath, "utf-8");
|
|
192
|
+
}
|
|
193
|
+
function addGoalToSquad(squadName, goal) {
|
|
194
|
+
const squadsDir = findSquadsDir();
|
|
195
|
+
if (!squadsDir) return false;
|
|
196
|
+
const squadFile = join(squadsDir, squadName, "SQUAD.md");
|
|
197
|
+
if (!existsSync(squadFile)) return false;
|
|
198
|
+
let content = readFileSync(squadFile, "utf-8");
|
|
199
|
+
if (!content.includes("## Goals")) {
|
|
200
|
+
const insertPoint = content.indexOf("## Dependencies");
|
|
201
|
+
if (insertPoint > 0) {
|
|
202
|
+
content = content.slice(0, insertPoint) + `## Goals
|
|
203
|
+
|
|
204
|
+
- [ ] ${goal}
|
|
205
|
+
|
|
206
|
+
` + content.slice(insertPoint);
|
|
207
|
+
} else {
|
|
208
|
+
content += `
|
|
209
|
+
## Goals
|
|
210
|
+
|
|
211
|
+
- [ ] ${goal}
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
const goalsIdx = content.indexOf("## Goals");
|
|
216
|
+
const nextSectionIdx = content.indexOf("\n## ", goalsIdx + 1);
|
|
217
|
+
const endIdx = nextSectionIdx > 0 ? nextSectionIdx : content.length;
|
|
218
|
+
const goalsSection = content.slice(goalsIdx, endIdx);
|
|
219
|
+
const lastGoalMatch = goalsSection.match(/^-\s*\[[ x]\].+$/gm);
|
|
220
|
+
if (lastGoalMatch) {
|
|
221
|
+
const lastGoal = lastGoalMatch[lastGoalMatch.length - 1];
|
|
222
|
+
const lastGoalIdx = content.lastIndexOf(lastGoal, endIdx);
|
|
223
|
+
const insertPos = lastGoalIdx + lastGoal.length;
|
|
224
|
+
content = content.slice(0, insertPos) + `
|
|
225
|
+
- [ ] ${goal}` + content.slice(insertPos);
|
|
226
|
+
} else {
|
|
227
|
+
const headerEnd = goalsIdx + "## Goals".length;
|
|
228
|
+
content = content.slice(0, headerEnd) + `
|
|
229
|
+
|
|
230
|
+
- [ ] ${goal}` + content.slice(headerEnd);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
writeFileSync(squadFile, content);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
function updateGoalInSquad(squadName, goalIndex, updates) {
|
|
237
|
+
const squadsDir = findSquadsDir();
|
|
238
|
+
if (!squadsDir) return false;
|
|
239
|
+
const squadFile = join(squadsDir, squadName, "SQUAD.md");
|
|
240
|
+
if (!existsSync(squadFile)) return false;
|
|
241
|
+
const content = readFileSync(squadFile, "utf-8");
|
|
242
|
+
const lines = content.split("\n");
|
|
243
|
+
let currentSection = "";
|
|
244
|
+
let goalCount = 0;
|
|
245
|
+
for (let i = 0; i < lines.length; i++) {
|
|
246
|
+
const line = lines[i];
|
|
247
|
+
if (line.startsWith("## ")) {
|
|
248
|
+
currentSection = line.replace("## ", "").trim().toLowerCase();
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (currentSection === "goals") {
|
|
252
|
+
const goalMatch = line.match(/^-\s*\[([ x])\]\s*(.+)$/);
|
|
253
|
+
if (goalMatch) {
|
|
254
|
+
if (goalCount === goalIndex) {
|
|
255
|
+
let newLine = "- [" + (updates.completed ? "x" : " ") + "] " + goalMatch[2];
|
|
256
|
+
if (updates.progress !== void 0) {
|
|
257
|
+
newLine = newLine.replace(/\s*\(progress:\s*[^)]+\)/i, "");
|
|
258
|
+
if (updates.progress) {
|
|
259
|
+
newLine += ` (progress: ${updates.progress})`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
lines[i] = newLine;
|
|
263
|
+
writeFileSync(squadFile, lines.join("\n"));
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
goalCount++;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/lib/condenser/tokens.ts
|
|
274
|
+
var RATIOS = {
|
|
275
|
+
english: 4,
|
|
276
|
+
// Standard English text
|
|
277
|
+
code: 3.5,
|
|
278
|
+
// Code tends to have more tokens per char
|
|
279
|
+
json: 3,
|
|
280
|
+
// JSON has many punctuation tokens
|
|
281
|
+
mixed: 3.75
|
|
282
|
+
// Default for mixed content
|
|
283
|
+
};
|
|
284
|
+
function estimateTokens(text, type = "mixed") {
|
|
285
|
+
if (!text) return 0;
|
|
286
|
+
const ratio = RATIOS[type];
|
|
287
|
+
return Math.ceil(text.length / ratio);
|
|
288
|
+
}
|
|
289
|
+
function estimateMessageTokens(message) {
|
|
290
|
+
let tokens = 4;
|
|
291
|
+
if (typeof message.content === "string") {
|
|
292
|
+
tokens += estimateTokens(message.content);
|
|
293
|
+
} else if (Array.isArray(message.content)) {
|
|
294
|
+
for (const part of message.content) {
|
|
295
|
+
if (part.text) {
|
|
296
|
+
tokens += estimateTokens(part.text);
|
|
297
|
+
}
|
|
298
|
+
tokens += 10;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return tokens;
|
|
302
|
+
}
|
|
303
|
+
var MODEL_LIMITS = {
|
|
304
|
+
// Anthropic models
|
|
305
|
+
"claude-opus-4-5-20251101": 2e5,
|
|
306
|
+
"claude-sonnet-4-20250514": 2e5,
|
|
307
|
+
"claude-3-5-haiku-20241022": 2e5,
|
|
308
|
+
// Aliases
|
|
309
|
+
opus: 2e5,
|
|
310
|
+
sonnet: 2e5,
|
|
311
|
+
haiku: 2e5,
|
|
312
|
+
// Default fallback
|
|
313
|
+
default: 2e5
|
|
314
|
+
};
|
|
315
|
+
function getModelLimit(model) {
|
|
316
|
+
return MODEL_LIMITS[model] ?? MODEL_LIMITS.default;
|
|
317
|
+
}
|
|
318
|
+
function createTracker(model = "default") {
|
|
319
|
+
return {
|
|
320
|
+
used: 0,
|
|
321
|
+
limit: getModelLimit(model),
|
|
322
|
+
percentage: 0,
|
|
323
|
+
breakdown: {
|
|
324
|
+
system: 0,
|
|
325
|
+
user: 0,
|
|
326
|
+
assistant: 0,
|
|
327
|
+
tools: 0
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function updateTracker(tracker, content, category = "assistant") {
|
|
332
|
+
const tokens = estimateTokens(content);
|
|
333
|
+
tracker.used += tokens;
|
|
334
|
+
tracker.breakdown[category] += tokens;
|
|
335
|
+
tracker.percentage = tracker.used / tracker.limit;
|
|
336
|
+
}
|
|
337
|
+
function updateTrackerFromMessage(tracker, message) {
|
|
338
|
+
const tokens = estimateMessageTokens(message);
|
|
339
|
+
const category = mapRoleToCategory(message.role);
|
|
340
|
+
tracker.used += tokens;
|
|
341
|
+
tracker.breakdown[category] += tokens;
|
|
342
|
+
tracker.percentage = tracker.used / tracker.limit;
|
|
343
|
+
}
|
|
344
|
+
function mapRoleToCategory(role) {
|
|
345
|
+
switch (role) {
|
|
346
|
+
case "system":
|
|
347
|
+
return "system";
|
|
348
|
+
case "user":
|
|
349
|
+
return "user";
|
|
350
|
+
case "assistant":
|
|
351
|
+
return "assistant";
|
|
352
|
+
case "tool":
|
|
353
|
+
case "tool_result":
|
|
354
|
+
return "tools";
|
|
355
|
+
default:
|
|
356
|
+
return "assistant";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
var DEFAULT_THRESHOLDS = {
|
|
360
|
+
light: 0.7,
|
|
361
|
+
medium: 0.85,
|
|
362
|
+
heavy: 0.95
|
|
363
|
+
};
|
|
364
|
+
function getCompressionLevel(tracker, thresholds = DEFAULT_THRESHOLDS) {
|
|
365
|
+
if (tracker.percentage >= thresholds.heavy) return "heavy";
|
|
366
|
+
if (tracker.percentage >= thresholds.medium) return "medium";
|
|
367
|
+
if (tracker.percentage >= thresholds.light) return "light";
|
|
368
|
+
return "none";
|
|
369
|
+
}
|
|
370
|
+
function formatTrackerStatus(tracker) {
|
|
371
|
+
const pct = (tracker.percentage * 100).toFixed(1);
|
|
372
|
+
const used = (tracker.used / 1e3).toFixed(1);
|
|
373
|
+
const limit = (tracker.limit / 1e3).toFixed(0);
|
|
374
|
+
const level = getCompressionLevel(tracker);
|
|
375
|
+
const levelIndicator = level === "none" ? "" : level === "light" ? " [!]" : level === "medium" ? " [!!]" : " [!!!]";
|
|
376
|
+
return `${used}K / ${limit}K tokens (${pct}%)${levelIndicator}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/lib/condenser/deduplication.ts
|
|
380
|
+
function hashContent(content) {
|
|
381
|
+
const prefix = content.slice(0, 100);
|
|
382
|
+
return `${content.length}:${prefix}`;
|
|
383
|
+
}
|
|
384
|
+
var FileDeduplicator = class {
|
|
385
|
+
/** Map of file path to all reads of that file */
|
|
386
|
+
reads = /* @__PURE__ */ new Map();
|
|
387
|
+
/** Current turn index */
|
|
388
|
+
currentTurn = 0;
|
|
389
|
+
/**
|
|
390
|
+
* Record a file read.
|
|
391
|
+
*
|
|
392
|
+
* @param path - File path that was read
|
|
393
|
+
* @param content - File content that was read
|
|
394
|
+
*/
|
|
395
|
+
trackRead(path, content) {
|
|
396
|
+
const record = {
|
|
397
|
+
path,
|
|
398
|
+
turnIndex: this.currentTurn,
|
|
399
|
+
tokenCount: estimateTokens(content, "code"),
|
|
400
|
+
contentHash: hashContent(content)
|
|
401
|
+
};
|
|
402
|
+
const existing = this.reads.get(path) || [];
|
|
403
|
+
existing.push(record);
|
|
404
|
+
this.reads.set(path, existing);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Advance to next turn.
|
|
408
|
+
*/
|
|
409
|
+
nextTurn() {
|
|
410
|
+
this.currentTurn++;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get current turn index.
|
|
414
|
+
*/
|
|
415
|
+
getTurn() {
|
|
416
|
+
return this.currentTurn;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Check if a file has been read before.
|
|
420
|
+
*
|
|
421
|
+
* @param path - File path to check
|
|
422
|
+
* @returns Previous read record if exists
|
|
423
|
+
*/
|
|
424
|
+
getPreviousRead(path) {
|
|
425
|
+
const reads = this.reads.get(path);
|
|
426
|
+
if (!reads || reads.length === 0) return void 0;
|
|
427
|
+
return reads[reads.length - 1];
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get all files that have been read multiple times.
|
|
431
|
+
*
|
|
432
|
+
* @returns Map of path to read count
|
|
433
|
+
*/
|
|
434
|
+
getDuplicateReads() {
|
|
435
|
+
const duplicates = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const [path, reads] of this.reads) {
|
|
437
|
+
if (reads.length > 1) {
|
|
438
|
+
duplicates.set(path, reads.length);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return duplicates;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Calculate potential token savings from deduplication.
|
|
445
|
+
*/
|
|
446
|
+
getPotentialSavings() {
|
|
447
|
+
let savings = 0;
|
|
448
|
+
for (const reads of this.reads.values()) {
|
|
449
|
+
if (reads.length > 1) {
|
|
450
|
+
for (let i = 0; i < reads.length - 1; i++) {
|
|
451
|
+
savings += reads[i].tokenCount;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return savings;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Generate a deduplication reference message.
|
|
459
|
+
*
|
|
460
|
+
* @param path - File path
|
|
461
|
+
* @param previousTurn - Turn where file was previously read
|
|
462
|
+
*/
|
|
463
|
+
static createReference(path, previousTurn) {
|
|
464
|
+
return `[File "${path}" was read at turn ${previousTurn}. Content unchanged.]`;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Reset tracker state.
|
|
468
|
+
*/
|
|
469
|
+
reset() {
|
|
470
|
+
this.reads.clear();
|
|
471
|
+
this.currentTurn = 0;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get statistics for debugging.
|
|
475
|
+
*/
|
|
476
|
+
getStats() {
|
|
477
|
+
let totalReads = 0;
|
|
478
|
+
let duplicateReads = 0;
|
|
479
|
+
for (const reads of this.reads.values()) {
|
|
480
|
+
totalReads += reads.length;
|
|
481
|
+
if (reads.length > 1) {
|
|
482
|
+
duplicateReads += reads.length - 1;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
filesTracked: this.reads.size,
|
|
487
|
+
totalReads,
|
|
488
|
+
duplicateReads,
|
|
489
|
+
potentialSavings: this.getPotentialSavings()
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// src/lib/condenser/pruning.ts
|
|
495
|
+
var DEFAULT_CONFIG = {
|
|
496
|
+
protectRecent: 4e4,
|
|
497
|
+
minimumPrunable: 2e4,
|
|
498
|
+
protectedTools: ["skill", "memory", "goal"]
|
|
499
|
+
};
|
|
500
|
+
function createPrunedPlaceholder(toolName, tokensSaved) {
|
|
501
|
+
return `[Tool output pruned: ${toolName} (~${Math.round(tokensSaved / 1e3)}K tokens)]`;
|
|
502
|
+
}
|
|
503
|
+
var TokenPruner = class {
|
|
504
|
+
config;
|
|
505
|
+
constructor(config = {}) {
|
|
506
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Prune messages to reduce token count.
|
|
510
|
+
*
|
|
511
|
+
* Strategy:
|
|
512
|
+
* 1. Scan messages backward from newest to oldest
|
|
513
|
+
* 2. Accumulate tokens for tool outputs
|
|
514
|
+
* 3. Mark outputs beyond protection window for pruning
|
|
515
|
+
* 4. Replace pruned outputs with placeholders
|
|
516
|
+
*
|
|
517
|
+
* @param messages - Messages to prune
|
|
518
|
+
* @returns Pruned messages (new array, originals not mutated)
|
|
519
|
+
*/
|
|
520
|
+
pruneMessages(messages) {
|
|
521
|
+
const annotated = messages.map((msg) => ({
|
|
522
|
+
...msg,
|
|
523
|
+
_tokens: estimateMessageTokens(msg)
|
|
524
|
+
}));
|
|
525
|
+
const analysis = this.analyzePrunability(annotated);
|
|
526
|
+
if (analysis.prunableTokens < this.config.minimumPrunable) {
|
|
527
|
+
return messages;
|
|
528
|
+
}
|
|
529
|
+
return this.applyPruning(annotated, analysis.protectionIndex);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Analyze which messages can be pruned.
|
|
533
|
+
*/
|
|
534
|
+
analyzePrunability(messages) {
|
|
535
|
+
let protectedTokens = 0;
|
|
536
|
+
let protectionIndex = messages.length;
|
|
537
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
538
|
+
const msg = messages[i];
|
|
539
|
+
if (!this.isToolResult(msg)) {
|
|
540
|
+
protectedTokens += msg._tokens;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (this.isProtectedTool(msg)) {
|
|
544
|
+
protectedTokens += msg._tokens;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (protectedTokens + msg._tokens <= this.config.protectRecent) {
|
|
548
|
+
protectedTokens += msg._tokens;
|
|
549
|
+
} else {
|
|
550
|
+
protectionIndex = i + 1;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
let prunableTokens = 0;
|
|
555
|
+
for (let i = 0; i < protectionIndex; i++) {
|
|
556
|
+
const msg = messages[i];
|
|
557
|
+
if (this.isToolResult(msg) && !this.isProtectedTool(msg)) {
|
|
558
|
+
prunableTokens += msg._tokens;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { prunableTokens, protectedTokens, protectionIndex };
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Apply pruning to messages before the protection index.
|
|
565
|
+
*/
|
|
566
|
+
applyPruning(messages, protectionIndex) {
|
|
567
|
+
return messages.map((msg, i) => {
|
|
568
|
+
if (i >= protectionIndex) {
|
|
569
|
+
const { _tokens, _prunable, ...clean } = msg;
|
|
570
|
+
return clean;
|
|
571
|
+
}
|
|
572
|
+
if (!this.isToolResult(msg)) {
|
|
573
|
+
const { _tokens, _prunable, ...clean } = msg;
|
|
574
|
+
return clean;
|
|
575
|
+
}
|
|
576
|
+
if (this.isProtectedTool(msg)) {
|
|
577
|
+
const { _tokens, _prunable, ...clean } = msg;
|
|
578
|
+
return clean;
|
|
579
|
+
}
|
|
580
|
+
return this.createPrunedMessage(msg);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Create a pruned version of a message.
|
|
585
|
+
*/
|
|
586
|
+
createPrunedMessage(msg) {
|
|
587
|
+
const toolName = this.getToolName(msg);
|
|
588
|
+
const placeholder = createPrunedPlaceholder(toolName, msg._tokens);
|
|
589
|
+
if (typeof msg.content === "string") {
|
|
590
|
+
return {
|
|
591
|
+
role: msg.role,
|
|
592
|
+
content: placeholder
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const content = msg.content.map((part) => {
|
|
596
|
+
if (part.type === "tool_result" && part.text) {
|
|
597
|
+
return {
|
|
598
|
+
...part,
|
|
599
|
+
text: placeholder,
|
|
600
|
+
_pruned: true,
|
|
601
|
+
_prunedAt: Date.now()
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return part;
|
|
605
|
+
});
|
|
606
|
+
return {
|
|
607
|
+
role: msg.role,
|
|
608
|
+
content
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Check if a message is a tool result.
|
|
613
|
+
*/
|
|
614
|
+
isToolResult(msg) {
|
|
615
|
+
if (msg.role === "tool") return true;
|
|
616
|
+
if (Array.isArray(msg.content)) {
|
|
617
|
+
return msg.content.some((part) => part.type === "tool_result");
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Check if a tool is in the protected list.
|
|
623
|
+
*/
|
|
624
|
+
isProtectedTool(msg) {
|
|
625
|
+
const toolName = this.getToolName(msg);
|
|
626
|
+
return this.config.protectedTools.includes(toolName.toLowerCase());
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Extract tool name from a message.
|
|
630
|
+
*/
|
|
631
|
+
getToolName(msg) {
|
|
632
|
+
if (Array.isArray(msg.content)) {
|
|
633
|
+
for (const part of msg.content) {
|
|
634
|
+
if (part.name) return part.name;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (typeof msg.content === "string") {
|
|
638
|
+
const match = msg.content.match(/Tool (?:output|result).*?:\s*(\w+)/i);
|
|
639
|
+
if (match) return match[1];
|
|
640
|
+
}
|
|
641
|
+
return "unknown";
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Get statistics about potential pruning.
|
|
645
|
+
*/
|
|
646
|
+
getStats(messages) {
|
|
647
|
+
const annotated = messages.map((msg) => ({
|
|
648
|
+
...msg,
|
|
649
|
+
_tokens: estimateMessageTokens(msg)
|
|
650
|
+
}));
|
|
651
|
+
const totalTokens = annotated.reduce((sum, msg) => sum + msg._tokens, 0);
|
|
652
|
+
const analysis = this.analyzePrunability(annotated);
|
|
653
|
+
return {
|
|
654
|
+
totalTokens,
|
|
655
|
+
prunableTokens: analysis.prunableTokens,
|
|
656
|
+
protectedTokens: analysis.protectedTokens,
|
|
657
|
+
savingsPercentage: totalTokens > 0 ? analysis.prunableTokens / totalTokens * 100 : 0
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// src/lib/condenser/summarizer.ts
|
|
663
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
664
|
+
var DEFAULT_CONFIG2 = {
|
|
665
|
+
keepFirst: 4,
|
|
666
|
+
keepLast: 20,
|
|
667
|
+
model: "claude-3-5-haiku-20241022",
|
|
668
|
+
// Cheap and fast
|
|
669
|
+
maxSummaryTokens: 2e3
|
|
670
|
+
};
|
|
671
|
+
var SUMMARIZATION_PROMPT = `You are summarizing a conversation between a user and an AI assistant to reduce context length while preserving essential information.
|
|
672
|
+
|
|
673
|
+
<conversation_to_summarize>
|
|
674
|
+
{{MIDDLE_CONTENT}}
|
|
675
|
+
</conversation_to_summarize>
|
|
676
|
+
|
|
677
|
+
Create a concise summary that preserves:
|
|
678
|
+
1. **User's goals** - What the user is trying to accomplish
|
|
679
|
+
2. **Progress made** - Key actions taken and their outcomes
|
|
680
|
+
3. **Current state** - What's done vs. what still needs to be done
|
|
681
|
+
4. **Critical context** - File paths, error messages, decisions made
|
|
682
|
+
5. **Blockers** - Any issues that need to be resolved
|
|
683
|
+
|
|
684
|
+
Format your summary as a structured note that the assistant can reference to continue the work.
|
|
685
|
+
|
|
686
|
+
Keep the summary under {{MAX_TOKENS}} tokens. Focus on actionable information.`;
|
|
687
|
+
var ConversationSummarizer = class {
|
|
688
|
+
config;
|
|
689
|
+
client = null;
|
|
690
|
+
constructor(config = {}) {
|
|
691
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Get or create Anthropic client.
|
|
695
|
+
*/
|
|
696
|
+
getClient() {
|
|
697
|
+
if (!this.client) {
|
|
698
|
+
this.client = new Anthropic();
|
|
699
|
+
}
|
|
700
|
+
return this.client;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Summarize messages to reduce token count.
|
|
704
|
+
*
|
|
705
|
+
* Strategy:
|
|
706
|
+
* 1. Keep first N messages (system prompt, initial context)
|
|
707
|
+
* 2. Keep last M messages (recent context, current task)
|
|
708
|
+
* 3. Summarize everything in between
|
|
709
|
+
*
|
|
710
|
+
* @param messages - Messages to summarize
|
|
711
|
+
* @returns Summarized messages
|
|
712
|
+
*/
|
|
713
|
+
async summarize(messages) {
|
|
714
|
+
if (messages.length <= this.config.keepFirst + this.config.keepLast) {
|
|
715
|
+
return messages;
|
|
716
|
+
}
|
|
717
|
+
const firstMessages = messages.slice(0, this.config.keepFirst);
|
|
718
|
+
const middleMessages = messages.slice(this.config.keepFirst, -this.config.keepLast);
|
|
719
|
+
const lastMessages = messages.slice(-this.config.keepLast);
|
|
720
|
+
const summary = await this.generateSummary(middleMessages);
|
|
721
|
+
const summaryMessage = {
|
|
722
|
+
role: "user",
|
|
723
|
+
content: `[Context Summary - ${middleMessages.length} messages condensed]
|
|
724
|
+
|
|
725
|
+
${summary}`
|
|
726
|
+
};
|
|
727
|
+
return [...firstMessages, summaryMessage, ...lastMessages];
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Generate a summary of the middle messages.
|
|
731
|
+
*/
|
|
732
|
+
async generateSummary(messages) {
|
|
733
|
+
const middleContent = this.formatMessagesForSummary(messages);
|
|
734
|
+
const prompt = SUMMARIZATION_PROMPT.replace("{{MIDDLE_CONTENT}}", middleContent).replace(
|
|
735
|
+
"{{MAX_TOKENS}}",
|
|
736
|
+
String(this.config.maxSummaryTokens)
|
|
737
|
+
);
|
|
738
|
+
try {
|
|
739
|
+
const client = this.getClient();
|
|
740
|
+
const response = await client.messages.create({
|
|
741
|
+
model: this.config.model,
|
|
742
|
+
max_tokens: this.config.maxSummaryTokens,
|
|
743
|
+
messages: [
|
|
744
|
+
{
|
|
745
|
+
role: "user",
|
|
746
|
+
content: prompt
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
});
|
|
750
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
751
|
+
if (textBlock && "text" in textBlock) {
|
|
752
|
+
return textBlock.text;
|
|
753
|
+
}
|
|
754
|
+
return "[Summary generation failed - no text in response]";
|
|
755
|
+
} catch (error) {
|
|
756
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
757
|
+
return `[Summary generation failed: ${message}]`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Format messages for the summarization prompt.
|
|
762
|
+
*/
|
|
763
|
+
formatMessagesForSummary(messages) {
|
|
764
|
+
return messages.map((msg, i) => {
|
|
765
|
+
const role = msg.role.toUpperCase();
|
|
766
|
+
const content = this.extractContent(msg);
|
|
767
|
+
return `[${i + 1}] ${role}:
|
|
768
|
+
${content}`;
|
|
769
|
+
}).join("\n\n---\n\n");
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Extract text content from a message.
|
|
773
|
+
*/
|
|
774
|
+
extractContent(msg) {
|
|
775
|
+
if (typeof msg.content === "string") {
|
|
776
|
+
return this.truncateContent(msg.content);
|
|
777
|
+
}
|
|
778
|
+
const parts = [];
|
|
779
|
+
for (const part of msg.content) {
|
|
780
|
+
if (part.text) {
|
|
781
|
+
parts.push(this.truncateContent(part.text));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return parts.join("\n");
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Truncate very long content for summary input.
|
|
788
|
+
*/
|
|
789
|
+
truncateContent(content, maxChars = 2e3) {
|
|
790
|
+
if (content.length <= maxChars) return content;
|
|
791
|
+
return content.slice(0, maxChars) + `
|
|
792
|
+
[...truncated ${content.length - maxChars} chars]`;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Estimate the cost of summarization.
|
|
796
|
+
*
|
|
797
|
+
* @param messages - Messages that would be summarized
|
|
798
|
+
* @returns Estimated cost in USD
|
|
799
|
+
*/
|
|
800
|
+
estimateCost(messages) {
|
|
801
|
+
if (messages.length <= this.config.keepFirst + this.config.keepLast) {
|
|
802
|
+
return 0;
|
|
803
|
+
}
|
|
804
|
+
const middleMessages = messages.slice(this.config.keepFirst, -this.config.keepLast);
|
|
805
|
+
const inputTokens = middleMessages.reduce((sum, msg) => {
|
|
806
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
807
|
+
return sum + estimateTokens(content);
|
|
808
|
+
}, 0);
|
|
809
|
+
const inputCost = inputTokens / 1e6 * 0.25;
|
|
810
|
+
const outputCost = this.config.maxSummaryTokens / 1e6 * 1.25;
|
|
811
|
+
return inputCost + outputCost;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Get statistics about potential summarization.
|
|
815
|
+
*/
|
|
816
|
+
getStats(messages) {
|
|
817
|
+
const wouldKeep = Math.min(messages.length, this.config.keepFirst + this.config.keepLast);
|
|
818
|
+
const wouldSummarize = Math.max(0, messages.length - wouldKeep);
|
|
819
|
+
return {
|
|
820
|
+
totalMessages: messages.length,
|
|
821
|
+
wouldKeep,
|
|
822
|
+
wouldSummarize,
|
|
823
|
+
estimatedCost: this.estimateCost(messages)
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// src/lib/condenser/index.ts
|
|
829
|
+
var DEFAULT_CONFIG3 = {
|
|
830
|
+
enabled: true,
|
|
831
|
+
lightThreshold: 0.7,
|
|
832
|
+
mediumThreshold: 0.85,
|
|
833
|
+
heavyThreshold: 0.95,
|
|
834
|
+
modelLimit: 2e5,
|
|
835
|
+
model: "claude-sonnet-4-20250514",
|
|
836
|
+
pruning: {},
|
|
837
|
+
summarization: {}
|
|
838
|
+
};
|
|
839
|
+
var ContextCondenser = class {
|
|
840
|
+
config;
|
|
841
|
+
tracker;
|
|
842
|
+
deduplicator;
|
|
843
|
+
pruner;
|
|
844
|
+
summarizer;
|
|
845
|
+
/** Metrics for tracking */
|
|
846
|
+
metrics = {
|
|
847
|
+
condensationCount: 0,
|
|
848
|
+
tokensRecovered: 0,
|
|
849
|
+
lastLevel: "none"
|
|
850
|
+
};
|
|
851
|
+
constructor(config = {}) {
|
|
852
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
853
|
+
this.tracker = createTracker(this.config.model);
|
|
854
|
+
this.tracker.limit = this.config.modelLimit;
|
|
855
|
+
this.deduplicator = new FileDeduplicator();
|
|
856
|
+
this.pruner = new TokenPruner(this.config.pruning);
|
|
857
|
+
this.summarizer = new ConversationSummarizer(this.config.summarization);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Main entry point - condense messages if needed.
|
|
861
|
+
*
|
|
862
|
+
* @param messages - Current conversation messages
|
|
863
|
+
* @returns Condensed messages and metadata
|
|
864
|
+
*/
|
|
865
|
+
async condense(messages) {
|
|
866
|
+
const startTime = Date.now();
|
|
867
|
+
if (!this.config.enabled) {
|
|
868
|
+
return this.createResult(messages, messages, "none", startTime);
|
|
869
|
+
}
|
|
870
|
+
this.updateTrackerFromMessages(messages);
|
|
871
|
+
const level = getCompressionLevel(this.tracker, {
|
|
872
|
+
light: this.config.lightThreshold,
|
|
873
|
+
medium: this.config.mediumThreshold,
|
|
874
|
+
heavy: this.config.heavyThreshold
|
|
875
|
+
});
|
|
876
|
+
if (level === "none") {
|
|
877
|
+
return this.createResult(messages, messages, level, startTime);
|
|
878
|
+
}
|
|
879
|
+
let condensed;
|
|
880
|
+
switch (level) {
|
|
881
|
+
case "light":
|
|
882
|
+
condensed = this.applyDeduplication(messages);
|
|
883
|
+
break;
|
|
884
|
+
case "medium":
|
|
885
|
+
condensed = this.applyPruning(messages);
|
|
886
|
+
break;
|
|
887
|
+
case "heavy":
|
|
888
|
+
condensed = await this.applySummarization(messages);
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
this.metrics.condensationCount++;
|
|
892
|
+
this.metrics.lastLevel = level;
|
|
893
|
+
return this.createResult(messages, condensed, level, startTime);
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Apply light compression (deduplication).
|
|
897
|
+
*/
|
|
898
|
+
applyDeduplication(messages) {
|
|
899
|
+
return messages;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Apply medium compression (pruning).
|
|
903
|
+
*/
|
|
904
|
+
applyPruning(messages) {
|
|
905
|
+
return this.pruner.pruneMessages(messages);
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Apply heavy compression (summarization).
|
|
909
|
+
*/
|
|
910
|
+
async applySummarization(messages) {
|
|
911
|
+
const pruned = this.applyPruning(messages);
|
|
912
|
+
const summarized = await this.summarizer.summarize(pruned);
|
|
913
|
+
return summarized;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Update tracker from messages.
|
|
917
|
+
*/
|
|
918
|
+
updateTrackerFromMessages(messages) {
|
|
919
|
+
this.tracker = createTracker(this.config.model);
|
|
920
|
+
this.tracker.limit = this.config.modelLimit;
|
|
921
|
+
for (const msg of messages) {
|
|
922
|
+
updateTrackerFromMessage(this.tracker, msg);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Create result object.
|
|
927
|
+
*/
|
|
928
|
+
createResult(before, after, level, startTime) {
|
|
929
|
+
const tokensBefore = this.estimateTokens(before);
|
|
930
|
+
const tokensAfter = this.estimateTokens(after);
|
|
931
|
+
const savings = tokensBefore > 0 ? (tokensBefore - tokensAfter) / tokensBefore * 100 : 0;
|
|
932
|
+
if (level !== "none") {
|
|
933
|
+
this.metrics.tokensRecovered += tokensBefore - tokensAfter;
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
messages: after,
|
|
937
|
+
level,
|
|
938
|
+
tokensBefore,
|
|
939
|
+
tokensAfter,
|
|
940
|
+
savingsPercentage: savings,
|
|
941
|
+
durationMs: Date.now() - startTime
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Estimate tokens for messages.
|
|
946
|
+
*/
|
|
947
|
+
estimateTokens(messages) {
|
|
948
|
+
const tempTracker = createTracker(this.config.model);
|
|
949
|
+
for (const msg of messages) {
|
|
950
|
+
updateTrackerFromMessage(tempTracker, msg);
|
|
951
|
+
}
|
|
952
|
+
return tempTracker.used;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Get current tracker status.
|
|
956
|
+
*/
|
|
957
|
+
getStatus() {
|
|
958
|
+
return formatTrackerStatus(this.tracker);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Get tracker for external monitoring.
|
|
962
|
+
*/
|
|
963
|
+
getTracker() {
|
|
964
|
+
return { ...this.tracker };
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Get metrics.
|
|
968
|
+
*/
|
|
969
|
+
getMetrics() {
|
|
970
|
+
return { ...this.metrics };
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Check if compression is needed.
|
|
974
|
+
*/
|
|
975
|
+
needsCompression() {
|
|
976
|
+
return getCompressionLevel(this.tracker, {
|
|
977
|
+
light: this.config.lightThreshold,
|
|
978
|
+
medium: this.config.mediumThreshold,
|
|
979
|
+
heavy: this.config.heavyThreshold
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Reset condenser state.
|
|
984
|
+
*/
|
|
985
|
+
reset() {
|
|
986
|
+
this.tracker = createTracker(this.config.model);
|
|
987
|
+
this.tracker.limit = this.config.modelLimit;
|
|
988
|
+
this.deduplicator.reset();
|
|
989
|
+
this.metrics = {
|
|
990
|
+
condensationCount: 0,
|
|
991
|
+
tokensRecovered: 0,
|
|
992
|
+
lastLevel: "none"
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Get file deduplicator for integration with tool layer.
|
|
997
|
+
*/
|
|
998
|
+
getDeduplicator() {
|
|
999
|
+
return this.deduplicator;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
function createCondenser(squadConfig) {
|
|
1003
|
+
const condenserConfig = squadConfig?.condenser || {};
|
|
1004
|
+
const modelConfig = squadConfig?.model || {};
|
|
1005
|
+
return new ContextCondenser({
|
|
1006
|
+
enabled: condenserConfig.enabled ?? true,
|
|
1007
|
+
lightThreshold: condenserConfig.light_threshold ?? 0.7,
|
|
1008
|
+
mediumThreshold: condenserConfig.medium_threshold ?? 0.85,
|
|
1009
|
+
heavyThreshold: condenserConfig.heavy_threshold ?? 0.95,
|
|
1010
|
+
model: modelConfig.default ?? "claude-sonnet-4-20250514",
|
|
1011
|
+
pruning: {
|
|
1012
|
+
protectRecent: condenserConfig.protect_recent ?? 4e4
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
6
1016
|
export {
|
|
1017
|
+
ContextCondenser,
|
|
1018
|
+
ConversationSummarizer,
|
|
1019
|
+
FileDeduplicator,
|
|
1020
|
+
TokenPruner,
|
|
1021
|
+
addGoalToSquad,
|
|
1022
|
+
createCondenser,
|
|
1023
|
+
createTracker,
|
|
1024
|
+
estimateMessageTokens,
|
|
1025
|
+
estimateTokens,
|
|
1026
|
+
findProjectRoot,
|
|
1027
|
+
findSquadsDir,
|
|
1028
|
+
formatTrackerStatus,
|
|
1029
|
+
getCompressionLevel,
|
|
1030
|
+
listAgents,
|
|
1031
|
+
listSquads,
|
|
1032
|
+
loadAgentDefinition,
|
|
1033
|
+
loadSquad,
|
|
1034
|
+
parseSquadFile,
|
|
1035
|
+
updateGoalInSquad,
|
|
1036
|
+
updateTracker,
|
|
7
1037
|
version
|
|
8
1038
|
};
|
|
9
1039
|
//# sourceMappingURL=index.js.map
|