@compilr-dev/agents 0.0.1
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 +1277 -0
- package/dist/agent.d.ts +1272 -0
- package/dist/agent.js +1912 -0
- package/dist/anchors/builtin.d.ts +24 -0
- package/dist/anchors/builtin.js +61 -0
- package/dist/anchors/index.d.ts +6 -0
- package/dist/anchors/index.js +5 -0
- package/dist/anchors/manager.d.ts +115 -0
- package/dist/anchors/manager.js +412 -0
- package/dist/anchors/types.d.ts +168 -0
- package/dist/anchors/types.js +10 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +10 -0
- package/dist/context/manager.d.ts +224 -0
- package/dist/context/manager.js +770 -0
- package/dist/context/types.d.ts +377 -0
- package/dist/context/types.js +7 -0
- package/dist/costs/index.d.ts +8 -0
- package/dist/costs/index.js +7 -0
- package/dist/costs/tracker.d.ts +121 -0
- package/dist/costs/tracker.js +295 -0
- package/dist/costs/types.d.ts +157 -0
- package/dist/costs/types.js +8 -0
- package/dist/errors.d.ts +178 -0
- package/dist/errors.js +249 -0
- package/dist/guardrails/builtin.d.ts +27 -0
- package/dist/guardrails/builtin.js +223 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.js +5 -0
- package/dist/guardrails/manager.d.ts +117 -0
- package/dist/guardrails/manager.js +288 -0
- package/dist/guardrails/types.d.ts +159 -0
- package/dist/guardrails/types.js +7 -0
- package/dist/hooks/index.d.ts +31 -0
- package/dist/hooks/index.js +29 -0
- package/dist/hooks/manager.d.ts +147 -0
- package/dist/hooks/manager.js +600 -0
- package/dist/hooks/types.d.ts +368 -0
- package/dist/hooks/types.js +12 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +73 -0
- package/dist/mcp/client.d.ts +93 -0
- package/dist/mcp/client.js +287 -0
- package/dist/mcp/errors.d.ts +60 -0
- package/dist/mcp/errors.js +78 -0
- package/dist/mcp/index.d.ts +43 -0
- package/dist/mcp/index.js +45 -0
- package/dist/mcp/manager.d.ts +120 -0
- package/dist/mcp/manager.js +276 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.js +99 -0
- package/dist/mcp/types.d.ts +150 -0
- package/dist/mcp/types.js +40 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/loader.d.ts +114 -0
- package/dist/memory/loader.js +463 -0
- package/dist/memory/types.d.ts +182 -0
- package/dist/memory/types.js +8 -0
- package/dist/messages/index.d.ts +82 -0
- package/dist/messages/index.js +155 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +4 -0
- package/dist/permissions/manager.d.ts +125 -0
- package/dist/permissions/manager.js +379 -0
- package/dist/permissions/types.d.ts +162 -0
- package/dist/permissions/types.js +7 -0
- package/dist/providers/claude.d.ts +90 -0
- package/dist/providers/claude.js +348 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +11 -0
- package/dist/providers/mock.d.ts +133 -0
- package/dist/providers/mock.js +204 -0
- package/dist/providers/types.d.ts +168 -0
- package/dist/providers/types.js +4 -0
- package/dist/rate-limit/index.d.ts +45 -0
- package/dist/rate-limit/index.js +47 -0
- package/dist/rate-limit/limiter.d.ts +104 -0
- package/dist/rate-limit/limiter.js +326 -0
- package/dist/rate-limit/provider-wrapper.d.ts +112 -0
- package/dist/rate-limit/provider-wrapper.js +201 -0
- package/dist/rate-limit/retry.d.ts +108 -0
- package/dist/rate-limit/retry.js +287 -0
- package/dist/rate-limit/types.d.ts +181 -0
- package/dist/rate-limit/types.js +22 -0
- package/dist/rehearsal/file-analyzer.d.ts +22 -0
- package/dist/rehearsal/file-analyzer.js +351 -0
- package/dist/rehearsal/git-analyzer.d.ts +22 -0
- package/dist/rehearsal/git-analyzer.js +472 -0
- package/dist/rehearsal/index.d.ts +35 -0
- package/dist/rehearsal/index.js +36 -0
- package/dist/rehearsal/manager.d.ts +100 -0
- package/dist/rehearsal/manager.js +290 -0
- package/dist/rehearsal/types.d.ts +235 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/skills/index.d.ts +160 -0
- package/dist/skills/index.js +282 -0
- package/dist/state/agent-state.d.ts +41 -0
- package/dist/state/agent-state.js +88 -0
- package/dist/state/checkpointer.d.ts +110 -0
- package/dist/state/checkpointer.js +362 -0
- package/dist/state/errors.d.ts +66 -0
- package/dist/state/errors.js +88 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.js +37 -0
- package/dist/state/serializer.d.ts +55 -0
- package/dist/state/serializer.js +172 -0
- package/dist/state/types.d.ts +312 -0
- package/dist/state/types.js +14 -0
- package/dist/tools/builtin/bash-output.d.ts +61 -0
- package/dist/tools/builtin/bash-output.js +90 -0
- package/dist/tools/builtin/bash.d.ts +150 -0
- package/dist/tools/builtin/bash.js +354 -0
- package/dist/tools/builtin/edit.d.ts +50 -0
- package/dist/tools/builtin/edit.js +215 -0
- package/dist/tools/builtin/glob.d.ts +62 -0
- package/dist/tools/builtin/glob.js +244 -0
- package/dist/tools/builtin/grep.d.ts +74 -0
- package/dist/tools/builtin/grep.js +363 -0
- package/dist/tools/builtin/index.d.ts +44 -0
- package/dist/tools/builtin/index.js +69 -0
- package/dist/tools/builtin/kill-shell.d.ts +44 -0
- package/dist/tools/builtin/kill-shell.js +80 -0
- package/dist/tools/builtin/read-file.d.ts +57 -0
- package/dist/tools/builtin/read-file.js +184 -0
- package/dist/tools/builtin/shell-manager.d.ts +176 -0
- package/dist/tools/builtin/shell-manager.js +337 -0
- package/dist/tools/builtin/task.d.ts +202 -0
- package/dist/tools/builtin/task.js +350 -0
- package/dist/tools/builtin/todo.d.ts +207 -0
- package/dist/tools/builtin/todo.js +453 -0
- package/dist/tools/builtin/utils.d.ts +27 -0
- package/dist/tools/builtin/utils.js +70 -0
- package/dist/tools/builtin/web-fetch.d.ts +96 -0
- package/dist/tools/builtin/web-fetch.js +290 -0
- package/dist/tools/builtin/write-file.d.ts +54 -0
- package/dist/tools/builtin/write-file.js +147 -0
- package/dist/tools/define.d.ts +60 -0
- package/dist/tools/define.js +65 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/registry.d.ts +79 -0
- package/dist/tools/registry.js +151 -0
- package/dist/tools/types.d.ts +59 -0
- package/dist/tools/types.js +4 -0
- package/dist/tracing/hooks.d.ts +58 -0
- package/dist/tracing/hooks.js +377 -0
- package/dist/tracing/index.d.ts +51 -0
- package/dist/tracing/index.js +55 -0
- package/dist/tracing/logging.d.ts +78 -0
- package/dist/tracing/logging.js +310 -0
- package/dist/tracing/manager.d.ts +160 -0
- package/dist/tracing/manager.js +468 -0
- package/dist/tracing/otel.d.ts +102 -0
- package/dist/tracing/otel.js +246 -0
- package/dist/tracing/types.d.ts +346 -0
- package/dist/tracing/types.js +38 -0
- package/dist/utils/index.d.ts +23 -0
- package/dist/utils/index.js +44 -0
- package/package.json +79 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextManager - Manages agent context windows
|
|
3
|
+
*
|
|
4
|
+
* Handles token tracking, compaction, summarization, and filtering
|
|
5
|
+
* to keep agent context within limits while preserving important information.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const contextManager = new ContextManager({
|
|
10
|
+
* provider,
|
|
11
|
+
* maxContextTokens: 200000,
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Check if action needed
|
|
15
|
+
* if (contextManager.needsSummarization()) {
|
|
16
|
+
* await contextManager.summarize(messages);
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Default budget allocation
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_BUDGET_ALLOCATION = {
|
|
24
|
+
system: 0.05, // 5% for system prompt
|
|
25
|
+
recentMessages: 0.4, // 40% for recent conversation
|
|
26
|
+
toolResults: 0.25, // 25% for tool outputs
|
|
27
|
+
history: 0.3, // 30% for older context
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Default context configuration
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_CONTEXT_CONFIG = {
|
|
33
|
+
maxContextTokens: 200000, // Claude's context window
|
|
34
|
+
budget: { ...DEFAULT_BUDGET_ALLOCATION },
|
|
35
|
+
verbosity: {
|
|
36
|
+
fullThreshold: 0.5, // Full output below 50%
|
|
37
|
+
normalThreshold: 0.8, // Normal output below 80%
|
|
38
|
+
abbreviatedThreshold: 0.95, // Abbreviated below 95%, minimal above
|
|
39
|
+
},
|
|
40
|
+
filtering: {
|
|
41
|
+
maxToolResultTokens: 80000,
|
|
42
|
+
maxFileLines: 500,
|
|
43
|
+
maxErrorLines: 50,
|
|
44
|
+
},
|
|
45
|
+
compaction: {
|
|
46
|
+
triggerInterval: 20,
|
|
47
|
+
triggerThreshold: 0.5,
|
|
48
|
+
preserveRecentTurns: 10,
|
|
49
|
+
minTokensToCompact: 1000,
|
|
50
|
+
},
|
|
51
|
+
summarization: {
|
|
52
|
+
warningThreshold: 0.8,
|
|
53
|
+
triggerThreshold: 0.9,
|
|
54
|
+
emergencyThreshold: 0.95,
|
|
55
|
+
preserveRecentMessages: 6,
|
|
56
|
+
emergencyPreserveMessages: 4,
|
|
57
|
+
summaryMaxTokens: 2000,
|
|
58
|
+
maxRounds: 3,
|
|
59
|
+
targetUtilization: 0.7,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* ContextManager tracks and manages context window usage
|
|
64
|
+
*/
|
|
65
|
+
export class ContextManager {
|
|
66
|
+
provider;
|
|
67
|
+
config;
|
|
68
|
+
onEvent;
|
|
69
|
+
cachedTokenCount = 0;
|
|
70
|
+
turnCount = 0;
|
|
71
|
+
compactionCount = 0;
|
|
72
|
+
summarizationCount = 0;
|
|
73
|
+
lastCompactionTurn = 0;
|
|
74
|
+
// Budget tracking - tokens used per category
|
|
75
|
+
categoryUsage = {
|
|
76
|
+
system: 0,
|
|
77
|
+
recentMessages: 0,
|
|
78
|
+
toolResults: 0,
|
|
79
|
+
history: 0,
|
|
80
|
+
};
|
|
81
|
+
constructor(options) {
|
|
82
|
+
this.provider = options.provider;
|
|
83
|
+
this.config = this.mergeConfig(options.config);
|
|
84
|
+
this.onEvent = options.onEvent;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Merge partial config with defaults
|
|
88
|
+
*/
|
|
89
|
+
mergeConfig(partial) {
|
|
90
|
+
if (!partial) {
|
|
91
|
+
return {
|
|
92
|
+
...DEFAULT_CONTEXT_CONFIG,
|
|
93
|
+
budget: { ...DEFAULT_CONTEXT_CONFIG.budget },
|
|
94
|
+
verbosity: { ...DEFAULT_CONTEXT_CONFIG.verbosity },
|
|
95
|
+
filtering: { ...DEFAULT_CONTEXT_CONFIG.filtering },
|
|
96
|
+
compaction: { ...DEFAULT_CONTEXT_CONFIG.compaction },
|
|
97
|
+
summarization: { ...DEFAULT_CONTEXT_CONFIG.summarization },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
maxContextTokens: partial.maxContextTokens ?? DEFAULT_CONTEXT_CONFIG.maxContextTokens,
|
|
102
|
+
budget: { ...DEFAULT_CONTEXT_CONFIG.budget, ...partial.budget },
|
|
103
|
+
verbosity: { ...DEFAULT_CONTEXT_CONFIG.verbosity, ...partial.verbosity },
|
|
104
|
+
filtering: { ...DEFAULT_CONTEXT_CONFIG.filtering, ...partial.filtering },
|
|
105
|
+
compaction: { ...DEFAULT_CONTEXT_CONFIG.compaction, ...partial.compaction },
|
|
106
|
+
summarization: { ...DEFAULT_CONTEXT_CONFIG.summarization, ...partial.summarization },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Emit a context event
|
|
111
|
+
*/
|
|
112
|
+
emit(event) {
|
|
113
|
+
this.onEvent?.(event);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Count tokens in messages using the provider
|
|
117
|
+
*/
|
|
118
|
+
async countTokens(messages) {
|
|
119
|
+
if (this.provider.countTokens) {
|
|
120
|
+
return this.provider.countTokens(messages);
|
|
121
|
+
}
|
|
122
|
+
// Fallback: rough estimate based on character count
|
|
123
|
+
let charCount = 0;
|
|
124
|
+
for (const msg of messages) {
|
|
125
|
+
if (typeof msg.content === 'string') {
|
|
126
|
+
charCount += msg.content.length;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
for (const block of msg.content) {
|
|
130
|
+
switch (block.type) {
|
|
131
|
+
case 'text':
|
|
132
|
+
charCount += block.text.length;
|
|
133
|
+
break;
|
|
134
|
+
case 'tool_use':
|
|
135
|
+
charCount += JSON.stringify(block.input).length;
|
|
136
|
+
break;
|
|
137
|
+
case 'tool_result':
|
|
138
|
+
charCount += block.content.length;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Math.ceil(charCount / 4);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Update token count for messages
|
|
148
|
+
*/
|
|
149
|
+
async updateTokenCount(messages) {
|
|
150
|
+
this.cachedTokenCount = await this.countTokens(messages);
|
|
151
|
+
return this.cachedTokenCount;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get current token count (cached)
|
|
155
|
+
*/
|
|
156
|
+
getTokenCount() {
|
|
157
|
+
return this.cachedTokenCount;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get context utilization (0.0 - 1.0)
|
|
161
|
+
*/
|
|
162
|
+
getUtilization() {
|
|
163
|
+
return this.cachedTokenCount / this.config.maxContextTokens;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get maximum context tokens
|
|
167
|
+
*/
|
|
168
|
+
getMaxTokens() {
|
|
169
|
+
return this.config.maxContextTokens;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Increment turn count (call after each assistant response)
|
|
173
|
+
*/
|
|
174
|
+
incrementTurn() {
|
|
175
|
+
this.turnCount++;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if compaction is needed
|
|
179
|
+
*/
|
|
180
|
+
needsCompaction() {
|
|
181
|
+
// Check turn-based trigger
|
|
182
|
+
const turnsSinceCompaction = this.turnCount - this.lastCompactionTurn;
|
|
183
|
+
if (turnsSinceCompaction >= this.config.compaction.triggerInterval) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
// Check utilization-based trigger
|
|
187
|
+
if (this.getUtilization() >= this.config.compaction.triggerThreshold) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if summarization is needed (more aggressive than compaction)
|
|
194
|
+
*/
|
|
195
|
+
needsSummarization() {
|
|
196
|
+
return this.getUtilization() >= this.config.summarization.triggerThreshold;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if content should be filtered before adding
|
|
200
|
+
*/
|
|
201
|
+
shouldFilter(tokenCount) {
|
|
202
|
+
return tokenCount > this.config.filtering.maxToolResultTokens;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get context statistics
|
|
206
|
+
*/
|
|
207
|
+
getStats(messageCount) {
|
|
208
|
+
return {
|
|
209
|
+
currentTokens: this.cachedTokenCount,
|
|
210
|
+
maxTokens: this.config.maxContextTokens,
|
|
211
|
+
utilization: this.getUtilization(),
|
|
212
|
+
messageCount,
|
|
213
|
+
turnCount: this.turnCount,
|
|
214
|
+
compactionCount: this.compactionCount,
|
|
215
|
+
summarizationCount: this.summarizationCount,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get the current configuration
|
|
220
|
+
*/
|
|
221
|
+
getConfig() {
|
|
222
|
+
return { ...this.config };
|
|
223
|
+
}
|
|
224
|
+
// ==========================================================================
|
|
225
|
+
// Budget System (Novel Technique #1)
|
|
226
|
+
// ==========================================================================
|
|
227
|
+
/**
|
|
228
|
+
* Get budget information for a specific category
|
|
229
|
+
*/
|
|
230
|
+
getCategoryBudget(category) {
|
|
231
|
+
const allocated = this.config.budget[category];
|
|
232
|
+
const allocatedTokens = Math.floor(this.config.maxContextTokens * allocated);
|
|
233
|
+
const used = this.categoryUsage[category];
|
|
234
|
+
const remaining = Math.max(0, allocatedTokens - used);
|
|
235
|
+
const utilization = allocatedTokens > 0 ? used / allocatedTokens : 0;
|
|
236
|
+
return {
|
|
237
|
+
allocated,
|
|
238
|
+
allocatedTokens,
|
|
239
|
+
used,
|
|
240
|
+
remaining,
|
|
241
|
+
utilization,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get budget information for all categories
|
|
246
|
+
*/
|
|
247
|
+
getAllBudgets() {
|
|
248
|
+
return {
|
|
249
|
+
system: this.getCategoryBudget('system'),
|
|
250
|
+
recentMessages: this.getCategoryBudget('recentMessages'),
|
|
251
|
+
toolResults: this.getCategoryBudget('toolResults'),
|
|
252
|
+
history: this.getCategoryBudget('history'),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Update token usage for a category
|
|
257
|
+
*/
|
|
258
|
+
updateCategoryUsage(category, tokens) {
|
|
259
|
+
this.categoryUsage[category] = tokens;
|
|
260
|
+
// Update total cached count
|
|
261
|
+
this.cachedTokenCount = Object.values(this.categoryUsage).reduce((a, b) => a + b, 0);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Add tokens to a category
|
|
265
|
+
*/
|
|
266
|
+
addToCategory(category, tokens) {
|
|
267
|
+
this.categoryUsage[category] += tokens;
|
|
268
|
+
this.cachedTokenCount += tokens;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check if a specific category needs compaction
|
|
272
|
+
*/
|
|
273
|
+
categoryNeedsCompaction(category) {
|
|
274
|
+
const budget = this.getCategoryBudget(category);
|
|
275
|
+
return budget.utilization >= this.config.compaction.triggerThreshold;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Compact only a specific category
|
|
279
|
+
*
|
|
280
|
+
* This is more targeted than full compaction - only affects messages
|
|
281
|
+
* in the specified category, leaving others untouched.
|
|
282
|
+
*/
|
|
283
|
+
async compactCategory(messages, category, saveToFile) {
|
|
284
|
+
const tokensBefore = await this.countTokens(messages);
|
|
285
|
+
const messagesBefore = messages.length;
|
|
286
|
+
const filesCreated = [];
|
|
287
|
+
let compactedIndex = 0;
|
|
288
|
+
const compactedMessages = [];
|
|
289
|
+
for (const msg of messages) {
|
|
290
|
+
// Determine if this message belongs to the target category
|
|
291
|
+
const msgCategory = this.categorizeMessage(msg);
|
|
292
|
+
if (msgCategory !== category) {
|
|
293
|
+
// Not our category, keep as-is
|
|
294
|
+
compactedMessages.push(msg);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Compact this message
|
|
298
|
+
if (typeof msg.content === 'string') {
|
|
299
|
+
const tokens = Math.ceil(msg.content.length / 4);
|
|
300
|
+
if (tokens >= this.config.compaction.minTokensToCompact) {
|
|
301
|
+
const filePath = await saveToFile(msg.content, compactedIndex++);
|
|
302
|
+
filesCreated.push(filePath);
|
|
303
|
+
compactedMessages.push({
|
|
304
|
+
...msg,
|
|
305
|
+
content: `[Content saved to ${filePath}]`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
compactedMessages.push(msg);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// Handle content blocks
|
|
314
|
+
const compactedBlocks = [];
|
|
315
|
+
for (const block of msg.content) {
|
|
316
|
+
if (block.type === 'tool_result' && category === 'toolResults') {
|
|
317
|
+
const tokens = Math.ceil(block.content.length / 4);
|
|
318
|
+
if (tokens >= this.config.compaction.minTokensToCompact) {
|
|
319
|
+
const filePath = await saveToFile(block.content, compactedIndex++);
|
|
320
|
+
filesCreated.push(filePath);
|
|
321
|
+
compactedBlocks.push({
|
|
322
|
+
...block,
|
|
323
|
+
content: `[Content saved to ${filePath}]`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
compactedBlocks.push(block);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
compactedBlocks.push(block);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
compactedMessages.push({ ...msg, content: compactedBlocks });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const tokensAfter = await this.countTokens(compactedMessages);
|
|
338
|
+
// Update category usage
|
|
339
|
+
const tokensSaved = tokensBefore - tokensAfter;
|
|
340
|
+
this.categoryUsage[category] = Math.max(0, this.categoryUsage[category] - tokensSaved);
|
|
341
|
+
this.cachedTokenCount = tokensAfter;
|
|
342
|
+
this.compactionCount++;
|
|
343
|
+
this.lastCompactionTurn = this.turnCount;
|
|
344
|
+
const result = {
|
|
345
|
+
messagesBefore,
|
|
346
|
+
messagesAfter: compactedMessages.length,
|
|
347
|
+
tokensBefore,
|
|
348
|
+
tokensAfter,
|
|
349
|
+
filesCreated,
|
|
350
|
+
};
|
|
351
|
+
this.emit({ type: 'context_compacted', result });
|
|
352
|
+
return { messages: compactedMessages, result };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Determine which category a message belongs to
|
|
356
|
+
*/
|
|
357
|
+
categorizeMessage(msg) {
|
|
358
|
+
if (msg.role === 'system') {
|
|
359
|
+
return 'system';
|
|
360
|
+
}
|
|
361
|
+
// Check if message contains tool results
|
|
362
|
+
if (typeof msg.content !== 'string') {
|
|
363
|
+
for (const block of msg.content) {
|
|
364
|
+
if (block.type === 'tool_result') {
|
|
365
|
+
return 'toolResults';
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// For now, treat all user/assistant messages as recentMessages
|
|
370
|
+
// The caller should track which are recent vs history
|
|
371
|
+
return 'recentMessages';
|
|
372
|
+
}
|
|
373
|
+
// ==========================================================================
|
|
374
|
+
// Pre-flight Checks (Novel Technique #2)
|
|
375
|
+
// ==========================================================================
|
|
376
|
+
/**
|
|
377
|
+
* Estimate tokens for a string content
|
|
378
|
+
*/
|
|
379
|
+
estimateTokens(content) {
|
|
380
|
+
return Math.ceil(content.length / 4);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check if content can be added to a category
|
|
384
|
+
*
|
|
385
|
+
* Returns a pre-flight result with recommendations for action.
|
|
386
|
+
*/
|
|
387
|
+
canAddContent(estimatedTokens, category) {
|
|
388
|
+
const budget = this.getCategoryBudget(category);
|
|
389
|
+
// Calculate what utilization would be after adding
|
|
390
|
+
const projectedCategoryUsage = budget.used + estimatedTokens;
|
|
391
|
+
const projectedCategoryUtilization = projectedCategoryUsage / budget.allocatedTokens;
|
|
392
|
+
const projectedTotalTokens = this.cachedTokenCount + estimatedTokens;
|
|
393
|
+
const projectedTotalUtilization = projectedTotalTokens / this.config.maxContextTokens;
|
|
394
|
+
// Case 1: Content exceeds total context capacity
|
|
395
|
+
if (projectedTotalUtilization > 1.0) {
|
|
396
|
+
return {
|
|
397
|
+
allowed: false,
|
|
398
|
+
requiresAction: true,
|
|
399
|
+
action: 'reject',
|
|
400
|
+
estimatedTokens,
|
|
401
|
+
budgetRemaining: budget.remaining,
|
|
402
|
+
recommendation: `Content (~${String(estimatedTokens)} tokens) exceeds available context space. Consider truncating or saving to file.`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
// Case 2: Would trigger emergency summarization
|
|
406
|
+
if (projectedTotalUtilization >= this.config.summarization.emergencyThreshold) {
|
|
407
|
+
return {
|
|
408
|
+
allowed: false,
|
|
409
|
+
requiresAction: true,
|
|
410
|
+
action: 'summarize',
|
|
411
|
+
estimatedTokens,
|
|
412
|
+
budgetRemaining: budget.remaining,
|
|
413
|
+
recommendation: `Adding content would trigger emergency summarization. Consider compacting first.`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
// Case 3: Category budget exceeded
|
|
417
|
+
if (projectedCategoryUtilization > 1.0) {
|
|
418
|
+
return {
|
|
419
|
+
allowed: false,
|
|
420
|
+
requiresAction: true,
|
|
421
|
+
action: 'compact',
|
|
422
|
+
category,
|
|
423
|
+
estimatedTokens,
|
|
424
|
+
budgetRemaining: budget.remaining,
|
|
425
|
+
recommendation: `Category '${category}' budget would be exceeded. Compact this category first.`,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// Case 4: Category approaching limit (>80% of category budget)
|
|
429
|
+
if (projectedCategoryUtilization > 0.8) {
|
|
430
|
+
return {
|
|
431
|
+
allowed: true,
|
|
432
|
+
requiresAction: false,
|
|
433
|
+
estimatedTokens,
|
|
434
|
+
budgetRemaining: budget.remaining,
|
|
435
|
+
recommendation: `Category '${category}' approaching limit (${String(Math.round(projectedCategoryUtilization * 100))}%).`,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// Case 5: All good
|
|
439
|
+
return {
|
|
440
|
+
allowed: true,
|
|
441
|
+
requiresAction: false,
|
|
442
|
+
estimatedTokens,
|
|
443
|
+
budgetRemaining: budget.remaining,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// ==========================================================================
|
|
447
|
+
// Graceful Degradation (Novel Technique #3)
|
|
448
|
+
// ==========================================================================
|
|
449
|
+
/**
|
|
450
|
+
* Get current verbosity level based on context utilization
|
|
451
|
+
*
|
|
452
|
+
* Tools should adapt their output based on this level.
|
|
453
|
+
*/
|
|
454
|
+
getVerbosityLevel() {
|
|
455
|
+
const utilization = this.getUtilization();
|
|
456
|
+
if (utilization < this.config.verbosity.fullThreshold) {
|
|
457
|
+
return 'full';
|
|
458
|
+
}
|
|
459
|
+
if (utilization < this.config.verbosity.normalThreshold) {
|
|
460
|
+
return 'normal';
|
|
461
|
+
}
|
|
462
|
+
if (utilization < this.config.verbosity.abbreviatedThreshold) {
|
|
463
|
+
return 'abbreviated';
|
|
464
|
+
}
|
|
465
|
+
return 'minimal';
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Check if we're in minimal mode (critical context pressure)
|
|
469
|
+
*/
|
|
470
|
+
isMinimalMode() {
|
|
471
|
+
return this.getVerbosityLevel() === 'minimal';
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Check if emergency summarization is needed
|
|
475
|
+
*/
|
|
476
|
+
needsEmergencySummarization() {
|
|
477
|
+
return this.getUtilization() >= this.config.summarization.emergencyThreshold;
|
|
478
|
+
}
|
|
479
|
+
// ==========================================================================
|
|
480
|
+
// Content Filtering
|
|
481
|
+
// ==========================================================================
|
|
482
|
+
/**
|
|
483
|
+
* Filter large content before adding to context
|
|
484
|
+
*
|
|
485
|
+
* Returns the filtered content and metadata about what was done.
|
|
486
|
+
* The caller is responsible for saving to file if needed.
|
|
487
|
+
*/
|
|
488
|
+
filterContent(content, type) {
|
|
489
|
+
const originalLength = content.length;
|
|
490
|
+
const lines = content.split('\n');
|
|
491
|
+
let maxLines;
|
|
492
|
+
switch (type) {
|
|
493
|
+
case 'file':
|
|
494
|
+
maxLines = this.config.filtering.maxFileLines;
|
|
495
|
+
break;
|
|
496
|
+
case 'error':
|
|
497
|
+
maxLines = this.config.filtering.maxErrorLines;
|
|
498
|
+
break;
|
|
499
|
+
default: {
|
|
500
|
+
// For tool results, check token count estimate
|
|
501
|
+
const estimatedTokens = Math.ceil(content.length / 4);
|
|
502
|
+
if (estimatedTokens <= this.config.filtering.maxToolResultTokens) {
|
|
503
|
+
return { content, filtered: false, originalLength };
|
|
504
|
+
}
|
|
505
|
+
// Truncate to roughly maxToolResultTokens
|
|
506
|
+
const maxChars = this.config.filtering.maxToolResultTokens * 4;
|
|
507
|
+
const truncated = content.slice(0, maxChars);
|
|
508
|
+
return {
|
|
509
|
+
content: truncated + '\n\n[Content truncated - see file for full output]',
|
|
510
|
+
filtered: true,
|
|
511
|
+
originalLength,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (lines.length <= maxLines) {
|
|
516
|
+
return { content, filtered: false, originalLength };
|
|
517
|
+
}
|
|
518
|
+
// For files and errors, show first half and last half
|
|
519
|
+
const halfLines = Math.floor(maxLines / 2);
|
|
520
|
+
const firstPart = lines.slice(0, halfLines);
|
|
521
|
+
const lastPart = lines.slice(-halfLines);
|
|
522
|
+
const omitted = lines.length - maxLines;
|
|
523
|
+
const filteredContent = [
|
|
524
|
+
...firstPart,
|
|
525
|
+
'',
|
|
526
|
+
`[... ${String(omitted)} lines omitted ...]`,
|
|
527
|
+
'',
|
|
528
|
+
...lastPart,
|
|
529
|
+
].join('\n');
|
|
530
|
+
return {
|
|
531
|
+
content: filteredContent,
|
|
532
|
+
filtered: true,
|
|
533
|
+
originalLength,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Compact old messages by replacing large content with file references
|
|
538
|
+
*
|
|
539
|
+
* This is a placeholder - actual implementation requires file system access
|
|
540
|
+
* which will be provided by the Agent or CLI layer.
|
|
541
|
+
*/
|
|
542
|
+
async compact(messages, saveToFile) {
|
|
543
|
+
const tokensBefore = await this.countTokens(messages);
|
|
544
|
+
const messagesBefore = messages.length;
|
|
545
|
+
const filesCreated = [];
|
|
546
|
+
// Preserve recent messages
|
|
547
|
+
const preserveCount = this.config.compaction.preserveRecentTurns * 2; // 2 messages per turn
|
|
548
|
+
const oldMessages = messages.slice(0, -preserveCount);
|
|
549
|
+
const recentMessages = messages.slice(-preserveCount);
|
|
550
|
+
const compactedOld = [];
|
|
551
|
+
let compactedIndex = 0;
|
|
552
|
+
for (const msg of oldMessages) {
|
|
553
|
+
if (typeof msg.content === 'string') {
|
|
554
|
+
// Check if string content is large enough to compact
|
|
555
|
+
const tokens = Math.ceil(msg.content.length / 4);
|
|
556
|
+
if (tokens >= this.config.compaction.minTokensToCompact) {
|
|
557
|
+
const filePath = await saveToFile(msg.content, compactedIndex++);
|
|
558
|
+
filesCreated.push(filePath);
|
|
559
|
+
compactedOld.push({
|
|
560
|
+
...msg,
|
|
561
|
+
content: `[Content saved to ${filePath}]`,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
compactedOld.push(msg);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
// Check content blocks for large tool results
|
|
570
|
+
const compactedBlocks = [];
|
|
571
|
+
for (const block of msg.content) {
|
|
572
|
+
if (block.type === 'tool_result') {
|
|
573
|
+
const tokens = Math.ceil(block.content.length / 4);
|
|
574
|
+
if (tokens >= this.config.compaction.minTokensToCompact) {
|
|
575
|
+
const filePath = await saveToFile(block.content, compactedIndex++);
|
|
576
|
+
filesCreated.push(filePath);
|
|
577
|
+
compactedBlocks.push({
|
|
578
|
+
...block,
|
|
579
|
+
content: `[Content saved to ${filePath}]`,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
compactedBlocks.push(block);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
compactedBlocks.push(block);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
compactedOld.push({ ...msg, content: compactedBlocks });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const compactedMessages = [...compactedOld, ...recentMessages];
|
|
594
|
+
const tokensAfter = await this.countTokens(compactedMessages);
|
|
595
|
+
this.compactionCount++;
|
|
596
|
+
this.lastCompactionTurn = this.turnCount;
|
|
597
|
+
this.cachedTokenCount = tokensAfter;
|
|
598
|
+
const result = {
|
|
599
|
+
messagesBefore,
|
|
600
|
+
messagesAfter: compactedMessages.length,
|
|
601
|
+
tokensBefore,
|
|
602
|
+
tokensAfter,
|
|
603
|
+
filesCreated,
|
|
604
|
+
};
|
|
605
|
+
this.emit({ type: 'context_compacted', result });
|
|
606
|
+
return { messages: compactedMessages, result };
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Summarize conversation history
|
|
610
|
+
*
|
|
611
|
+
* This is a placeholder - actual implementation requires LLM call
|
|
612
|
+
* which will be orchestrated by the Agent layer.
|
|
613
|
+
*/
|
|
614
|
+
async summarize(messages, generateSummary) {
|
|
615
|
+
const originalTokens = await this.countTokens(messages);
|
|
616
|
+
// Find system message (preserve it)
|
|
617
|
+
const systemMessage = messages.find((m) => m.role === 'system');
|
|
618
|
+
// Preserve recent messages
|
|
619
|
+
const preserveCount = this.config.summarization.preserveRecentMessages;
|
|
620
|
+
const toSummarize = messages.filter((m) => m.role !== 'system').slice(0, -preserveCount);
|
|
621
|
+
const recentMessages = messages.filter((m) => m.role !== 'system').slice(-preserveCount);
|
|
622
|
+
// Generate summary
|
|
623
|
+
const summary = await generateSummary(toSummarize);
|
|
624
|
+
// Build new message list
|
|
625
|
+
const summarizedMessages = [];
|
|
626
|
+
if (systemMessage) {
|
|
627
|
+
summarizedMessages.push(systemMessage);
|
|
628
|
+
}
|
|
629
|
+
// Add summary as a user message
|
|
630
|
+
summarizedMessages.push({
|
|
631
|
+
role: 'user',
|
|
632
|
+
content: `[Previous conversation summary]\n\n${summary}\n\n[End of summary - conversation continues below]`,
|
|
633
|
+
});
|
|
634
|
+
// Add assistant acknowledgment
|
|
635
|
+
summarizedMessages.push({
|
|
636
|
+
role: 'assistant',
|
|
637
|
+
content: 'I understand. I have the context from our previous conversation. How can I continue helping you?',
|
|
638
|
+
});
|
|
639
|
+
// Add recent messages
|
|
640
|
+
summarizedMessages.push(...recentMessages);
|
|
641
|
+
const summaryTokens = await this.countTokens(summarizedMessages);
|
|
642
|
+
this.summarizationCount++;
|
|
643
|
+
this.cachedTokenCount = summaryTokens;
|
|
644
|
+
const result = {
|
|
645
|
+
originalTokens,
|
|
646
|
+
summaryTokens,
|
|
647
|
+
messagesPreserved: preserveCount,
|
|
648
|
+
summary,
|
|
649
|
+
rounds: 1,
|
|
650
|
+
emergency: false,
|
|
651
|
+
};
|
|
652
|
+
this.emit({ type: 'context_summarized', result });
|
|
653
|
+
return { messages: summarizedMessages, result };
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Summarize with support for multiple rounds and emergency mode
|
|
657
|
+
*
|
|
658
|
+
* Will perform multiple summarization rounds if needed to reach target utilization.
|
|
659
|
+
* Throws ContextOverflowError if unable to reduce context sufficiently.
|
|
660
|
+
*/
|
|
661
|
+
async summarizeWithRetry(messages, generateSummary, options) {
|
|
662
|
+
const emergency = options?.emergency ?? this.needsEmergencySummarization();
|
|
663
|
+
const maxRounds = options?.maxRounds ?? this.config.summarization.maxRounds;
|
|
664
|
+
const targetUtilization = options?.targetUtilization ?? this.config.summarization.targetUtilization;
|
|
665
|
+
let currentMessages = messages;
|
|
666
|
+
let rounds = 0;
|
|
667
|
+
let lastResult;
|
|
668
|
+
while (rounds < maxRounds) {
|
|
669
|
+
rounds++;
|
|
670
|
+
// Determine preserve count based on emergency mode and round
|
|
671
|
+
let preserveCount;
|
|
672
|
+
if (emergency || rounds > 1) {
|
|
673
|
+
// More aggressive: fewer preserved messages each round
|
|
674
|
+
preserveCount = Math.max(2, this.config.summarization.emergencyPreserveMessages - (rounds - 1) * 2);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
preserveCount = this.config.summarization.preserveRecentMessages;
|
|
678
|
+
}
|
|
679
|
+
// Find system message
|
|
680
|
+
const systemMessage = currentMessages.find((m) => m.role === 'system');
|
|
681
|
+
// Separate messages
|
|
682
|
+
const nonSystemMessages = currentMessages.filter((m) => m.role !== 'system');
|
|
683
|
+
const toSummarize = nonSystemMessages.slice(0, -preserveCount);
|
|
684
|
+
const recentMessages = nonSystemMessages.slice(-preserveCount);
|
|
685
|
+
// Generate summary
|
|
686
|
+
const summary = await generateSummary(toSummarize);
|
|
687
|
+
// Build new message list
|
|
688
|
+
const summarizedMessages = [];
|
|
689
|
+
if (systemMessage) {
|
|
690
|
+
summarizedMessages.push(systemMessage);
|
|
691
|
+
}
|
|
692
|
+
summarizedMessages.push({
|
|
693
|
+
role: 'user',
|
|
694
|
+
content: `[Previous conversation summary${rounds > 1 ? ` (round ${String(rounds)})` : ''}]\n\n${summary}\n\n[End of summary]`,
|
|
695
|
+
});
|
|
696
|
+
summarizedMessages.push({
|
|
697
|
+
role: 'assistant',
|
|
698
|
+
content: 'I understand. I have the context from our previous conversation.',
|
|
699
|
+
});
|
|
700
|
+
summarizedMessages.push(...recentMessages);
|
|
701
|
+
const summaryTokens = await this.countTokens(summarizedMessages);
|
|
702
|
+
this.cachedTokenCount = summaryTokens;
|
|
703
|
+
lastResult = {
|
|
704
|
+
originalTokens: await this.countTokens(messages),
|
|
705
|
+
summaryTokens,
|
|
706
|
+
messagesPreserved: preserveCount,
|
|
707
|
+
summary,
|
|
708
|
+
rounds,
|
|
709
|
+
emergency,
|
|
710
|
+
};
|
|
711
|
+
currentMessages = summarizedMessages;
|
|
712
|
+
// Check if we've reached target
|
|
713
|
+
if (this.getUtilization() <= targetUtilization) {
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Ensure we have a result (should always be true if maxRounds > 0)
|
|
718
|
+
if (!lastResult) {
|
|
719
|
+
// This shouldn't happen, but handle gracefully
|
|
720
|
+
lastResult = {
|
|
721
|
+
originalTokens: await this.countTokens(messages),
|
|
722
|
+
summaryTokens: this.cachedTokenCount,
|
|
723
|
+
messagesPreserved: 0,
|
|
724
|
+
summary: '',
|
|
725
|
+
rounds: 0,
|
|
726
|
+
emergency,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
this.summarizationCount++;
|
|
730
|
+
this.emit({ type: 'context_summarized', result: lastResult });
|
|
731
|
+
return { messages: currentMessages, result: lastResult };
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Check context and emit warning if needed
|
|
735
|
+
*/
|
|
736
|
+
checkAndWarn() {
|
|
737
|
+
const utilization = this.getUtilization();
|
|
738
|
+
if (utilization >= this.config.summarization.triggerThreshold) {
|
|
739
|
+
this.emit({
|
|
740
|
+
type: 'context_warning',
|
|
741
|
+
utilization,
|
|
742
|
+
threshold: this.config.summarization.triggerThreshold,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
else if (utilization >= this.config.compaction.triggerThreshold) {
|
|
746
|
+
this.emit({
|
|
747
|
+
type: 'context_warning',
|
|
748
|
+
utilization,
|
|
749
|
+
threshold: this.config.compaction.triggerThreshold,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Reset context manager state (for new conversations)
|
|
755
|
+
*/
|
|
756
|
+
reset() {
|
|
757
|
+
this.cachedTokenCount = 0;
|
|
758
|
+
this.turnCount = 0;
|
|
759
|
+
this.compactionCount = 0;
|
|
760
|
+
this.summarizationCount = 0;
|
|
761
|
+
this.lastCompactionTurn = 0;
|
|
762
|
+
// Reset category usage
|
|
763
|
+
this.categoryUsage = {
|
|
764
|
+
system: 0,
|
|
765
|
+
recentMessages: 0,
|
|
766
|
+
toolResults: 0,
|
|
767
|
+
history: 0,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|