@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.
Files changed (160) hide show
  1. package/README.md +1277 -0
  2. package/dist/agent.d.ts +1272 -0
  3. package/dist/agent.js +1912 -0
  4. package/dist/anchors/builtin.d.ts +24 -0
  5. package/dist/anchors/builtin.js +61 -0
  6. package/dist/anchors/index.d.ts +6 -0
  7. package/dist/anchors/index.js +5 -0
  8. package/dist/anchors/manager.d.ts +115 -0
  9. package/dist/anchors/manager.js +412 -0
  10. package/dist/anchors/types.d.ts +168 -0
  11. package/dist/anchors/types.js +10 -0
  12. package/dist/context/index.d.ts +12 -0
  13. package/dist/context/index.js +10 -0
  14. package/dist/context/manager.d.ts +224 -0
  15. package/dist/context/manager.js +770 -0
  16. package/dist/context/types.d.ts +377 -0
  17. package/dist/context/types.js +7 -0
  18. package/dist/costs/index.d.ts +8 -0
  19. package/dist/costs/index.js +7 -0
  20. package/dist/costs/tracker.d.ts +121 -0
  21. package/dist/costs/tracker.js +295 -0
  22. package/dist/costs/types.d.ts +157 -0
  23. package/dist/costs/types.js +8 -0
  24. package/dist/errors.d.ts +178 -0
  25. package/dist/errors.js +249 -0
  26. package/dist/guardrails/builtin.d.ts +27 -0
  27. package/dist/guardrails/builtin.js +223 -0
  28. package/dist/guardrails/index.d.ts +6 -0
  29. package/dist/guardrails/index.js +5 -0
  30. package/dist/guardrails/manager.d.ts +117 -0
  31. package/dist/guardrails/manager.js +288 -0
  32. package/dist/guardrails/types.d.ts +159 -0
  33. package/dist/guardrails/types.js +7 -0
  34. package/dist/hooks/index.d.ts +31 -0
  35. package/dist/hooks/index.js +29 -0
  36. package/dist/hooks/manager.d.ts +147 -0
  37. package/dist/hooks/manager.js +600 -0
  38. package/dist/hooks/types.d.ts +368 -0
  39. package/dist/hooks/types.js +12 -0
  40. package/dist/index.d.ts +45 -0
  41. package/dist/index.js +73 -0
  42. package/dist/mcp/client.d.ts +93 -0
  43. package/dist/mcp/client.js +287 -0
  44. package/dist/mcp/errors.d.ts +60 -0
  45. package/dist/mcp/errors.js +78 -0
  46. package/dist/mcp/index.d.ts +43 -0
  47. package/dist/mcp/index.js +45 -0
  48. package/dist/mcp/manager.d.ts +120 -0
  49. package/dist/mcp/manager.js +276 -0
  50. package/dist/mcp/tools.d.ts +54 -0
  51. package/dist/mcp/tools.js +99 -0
  52. package/dist/mcp/types.d.ts +150 -0
  53. package/dist/mcp/types.js +40 -0
  54. package/dist/memory/index.d.ts +8 -0
  55. package/dist/memory/index.js +7 -0
  56. package/dist/memory/loader.d.ts +114 -0
  57. package/dist/memory/loader.js +463 -0
  58. package/dist/memory/types.d.ts +182 -0
  59. package/dist/memory/types.js +8 -0
  60. package/dist/messages/index.d.ts +82 -0
  61. package/dist/messages/index.js +155 -0
  62. package/dist/permissions/index.d.ts +5 -0
  63. package/dist/permissions/index.js +4 -0
  64. package/dist/permissions/manager.d.ts +125 -0
  65. package/dist/permissions/manager.js +379 -0
  66. package/dist/permissions/types.d.ts +162 -0
  67. package/dist/permissions/types.js +7 -0
  68. package/dist/providers/claude.d.ts +90 -0
  69. package/dist/providers/claude.js +348 -0
  70. package/dist/providers/index.d.ts +8 -0
  71. package/dist/providers/index.js +11 -0
  72. package/dist/providers/mock.d.ts +133 -0
  73. package/dist/providers/mock.js +204 -0
  74. package/dist/providers/types.d.ts +168 -0
  75. package/dist/providers/types.js +4 -0
  76. package/dist/rate-limit/index.d.ts +45 -0
  77. package/dist/rate-limit/index.js +47 -0
  78. package/dist/rate-limit/limiter.d.ts +104 -0
  79. package/dist/rate-limit/limiter.js +326 -0
  80. package/dist/rate-limit/provider-wrapper.d.ts +112 -0
  81. package/dist/rate-limit/provider-wrapper.js +201 -0
  82. package/dist/rate-limit/retry.d.ts +108 -0
  83. package/dist/rate-limit/retry.js +287 -0
  84. package/dist/rate-limit/types.d.ts +181 -0
  85. package/dist/rate-limit/types.js +22 -0
  86. package/dist/rehearsal/file-analyzer.d.ts +22 -0
  87. package/dist/rehearsal/file-analyzer.js +351 -0
  88. package/dist/rehearsal/git-analyzer.d.ts +22 -0
  89. package/dist/rehearsal/git-analyzer.js +472 -0
  90. package/dist/rehearsal/index.d.ts +35 -0
  91. package/dist/rehearsal/index.js +36 -0
  92. package/dist/rehearsal/manager.d.ts +100 -0
  93. package/dist/rehearsal/manager.js +290 -0
  94. package/dist/rehearsal/types.d.ts +235 -0
  95. package/dist/rehearsal/types.js +8 -0
  96. package/dist/skills/index.d.ts +160 -0
  97. package/dist/skills/index.js +282 -0
  98. package/dist/state/agent-state.d.ts +41 -0
  99. package/dist/state/agent-state.js +88 -0
  100. package/dist/state/checkpointer.d.ts +110 -0
  101. package/dist/state/checkpointer.js +362 -0
  102. package/dist/state/errors.d.ts +66 -0
  103. package/dist/state/errors.js +88 -0
  104. package/dist/state/index.d.ts +35 -0
  105. package/dist/state/index.js +37 -0
  106. package/dist/state/serializer.d.ts +55 -0
  107. package/dist/state/serializer.js +172 -0
  108. package/dist/state/types.d.ts +312 -0
  109. package/dist/state/types.js +14 -0
  110. package/dist/tools/builtin/bash-output.d.ts +61 -0
  111. package/dist/tools/builtin/bash-output.js +90 -0
  112. package/dist/tools/builtin/bash.d.ts +150 -0
  113. package/dist/tools/builtin/bash.js +354 -0
  114. package/dist/tools/builtin/edit.d.ts +50 -0
  115. package/dist/tools/builtin/edit.js +215 -0
  116. package/dist/tools/builtin/glob.d.ts +62 -0
  117. package/dist/tools/builtin/glob.js +244 -0
  118. package/dist/tools/builtin/grep.d.ts +74 -0
  119. package/dist/tools/builtin/grep.js +363 -0
  120. package/dist/tools/builtin/index.d.ts +44 -0
  121. package/dist/tools/builtin/index.js +69 -0
  122. package/dist/tools/builtin/kill-shell.d.ts +44 -0
  123. package/dist/tools/builtin/kill-shell.js +80 -0
  124. package/dist/tools/builtin/read-file.d.ts +57 -0
  125. package/dist/tools/builtin/read-file.js +184 -0
  126. package/dist/tools/builtin/shell-manager.d.ts +176 -0
  127. package/dist/tools/builtin/shell-manager.js +337 -0
  128. package/dist/tools/builtin/task.d.ts +202 -0
  129. package/dist/tools/builtin/task.js +350 -0
  130. package/dist/tools/builtin/todo.d.ts +207 -0
  131. package/dist/tools/builtin/todo.js +453 -0
  132. package/dist/tools/builtin/utils.d.ts +27 -0
  133. package/dist/tools/builtin/utils.js +70 -0
  134. package/dist/tools/builtin/web-fetch.d.ts +96 -0
  135. package/dist/tools/builtin/web-fetch.js +290 -0
  136. package/dist/tools/builtin/write-file.d.ts +54 -0
  137. package/dist/tools/builtin/write-file.js +147 -0
  138. package/dist/tools/define.d.ts +60 -0
  139. package/dist/tools/define.js +65 -0
  140. package/dist/tools/index.d.ts +10 -0
  141. package/dist/tools/index.js +37 -0
  142. package/dist/tools/registry.d.ts +79 -0
  143. package/dist/tools/registry.js +151 -0
  144. package/dist/tools/types.d.ts +59 -0
  145. package/dist/tools/types.js +4 -0
  146. package/dist/tracing/hooks.d.ts +58 -0
  147. package/dist/tracing/hooks.js +377 -0
  148. package/dist/tracing/index.d.ts +51 -0
  149. package/dist/tracing/index.js +55 -0
  150. package/dist/tracing/logging.d.ts +78 -0
  151. package/dist/tracing/logging.js +310 -0
  152. package/dist/tracing/manager.d.ts +160 -0
  153. package/dist/tracing/manager.js +468 -0
  154. package/dist/tracing/otel.d.ts +102 -0
  155. package/dist/tracing/otel.js +246 -0
  156. package/dist/tracing/types.d.ts +346 -0
  157. package/dist/tracing/types.js +38 -0
  158. package/dist/utils/index.d.ts +23 -0
  159. package/dist/utils/index.js +44 -0
  160. 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
+ }