@compilr-dev/agents 0.3.28 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.d.ts CHANGED
@@ -16,6 +16,7 @@ import { PermissionManager } from './permissions/manager.js';
16
16
  import { ContextManager } from './context/manager.js';
17
17
  import { FileAccessTracker } from './context/file-tracker.js';
18
18
  import type { ObservationMaskConfig } from './context/observation-masker.js';
19
+ import type { WindowingConfig } from './context/windowing.js';
19
20
  import type { PruneConfig } from './context/dead-message-pruner.js';
20
21
  import { AnchorManager } from './anchors/manager.js';
21
22
  import { GuardrailManager } from './guardrails/manager.js';
@@ -306,6 +307,13 @@ export interface AgentConfig {
306
307
  * Enabled by default when contextManager is provided. Set to `false` to disable.
307
308
  */
308
309
  observationMask?: Partial<ObservationMaskConfig> | false;
310
+ /**
311
+ * Smart windowing configuration. Programmatically compacts old messages when
312
+ * history exceeds a token budget. Three zones: recent (intact), middle
313
+ * (truncated), old (event log). Zero LLM calls.
314
+ * Enabled by default when contextManager is provided. Set to `false` to disable.
315
+ */
316
+ windowing?: Partial<WindowingConfig> | false;
309
317
  /**
310
318
  * Use compact text format for tool results in LLM messages.
311
319
  * Strips JSON wrappers and metadata, reducing token usage.
@@ -924,6 +932,10 @@ export declare class Agent {
924
932
  * Observation masker for reducing token usage by masking old tool results
925
933
  */
926
934
  private readonly observationMasker?;
935
+ /**
936
+ * Smart windowing config for programmatic context compaction
937
+ */
938
+ private readonly windowingConfig?;
927
939
  /**
928
940
  * Whether to use compact text format for tool results in LLM messages
929
941
  */
package/dist/agent.js CHANGED
@@ -11,6 +11,7 @@ import { FileAccessTracker } from './context/file-tracker.js';
11
11
  import { createFileTrackingHook } from './context/file-tracking-hook.js';
12
12
  import { ToolResultDelegator, DELEGATION_SYSTEM_PROMPT } from './context/tool-result-delegator.js';
13
13
  import { ObservationMasker } from './context/observation-masker.js';
14
+ import { applyWindowing, DEFAULT_WINDOWING_CONFIG } from './context/windowing.js';
14
15
  import { compactToolResult } from './context/result-compactor.js';
15
16
  import { DeadMessagePruner } from './context/dead-message-pruner.js';
16
17
  import { createRecallResultTool } from './tools/builtin/recall-result.js';
@@ -105,6 +106,10 @@ export class Agent {
105
106
  * Observation masker for reducing token usage by masking old tool results
106
107
  */
107
108
  observationMasker;
109
+ /**
110
+ * Smart windowing config for programmatic context compaction
111
+ */
112
+ windowingConfig;
108
113
  /**
109
114
  * Whether to use compact text format for tool results in LLM messages
110
115
  */
@@ -134,6 +139,13 @@ export class Agent {
134
139
  }
135
140
  // Compact tool results: enabled by default when contextManager is provided
136
141
  this.compactToolResults = config.compactToolResults ?? this.contextManager !== undefined;
142
+ // Smart windowing: enabled by default when contextManager is provided, unless explicitly false
143
+ if (config.windowing !== false && this.contextManager) {
144
+ this.windowingConfig = {
145
+ ...DEFAULT_WINDOWING_CONFIG,
146
+ ...(typeof config.windowing === 'object' ? config.windowing : {}),
147
+ };
148
+ }
137
149
  // Dead message pruning: enabled by default when contextManager is provided, unless explicitly false
138
150
  if (config.deadMessagePruning !== false && this.contextManager) {
139
151
  this.deadMessagePruner = new DeadMessagePruner(config.deadMessagePruning === undefined ? undefined : config.deadMessagePruning);
@@ -1575,6 +1587,24 @@ export class Agent {
1575
1587
  const systemTokens = this.contextManager.estimateTokens(this.systemPrompt);
1576
1588
  this.contextManager.updateCategoryUsage('system', systemTokens);
1577
1589
  }
1590
+ // Smart windowing: programmatic compaction when history exceeds budget
1591
+ // Runs BEFORE LLM-based compaction (lighter, zero LLM calls)
1592
+ if (this.windowingConfig) {
1593
+ const windowResult = applyWindowing(messages, this.windowingConfig);
1594
+ if (windowResult.applied) {
1595
+ messages = windowResult.messages;
1596
+ // Update history to match windowed state
1597
+ const historyMsgs = messages.filter((m) => m.role !== 'system');
1598
+ // Keep conversationHistory in sync (drop system msg)
1599
+ this.conversationHistory = historyMsgs.slice(0, -newMessages.length);
1600
+ await this.contextManager.updateTokenCount(messages);
1601
+ emit({
1602
+ type: 'context_compacted',
1603
+ tokensBefore: windowResult.tokensBefore,
1604
+ tokensAfter: windowResult.tokensAfter,
1605
+ });
1606
+ }
1607
+ }
1578
1608
  // Check if we need to manage context before starting
1579
1609
  // Order: emergency (95%) → compaction (50%/20 turns) → warning (90%)
1580
1610
  if (this.contextManager.needsEmergencySummarization()) {
@@ -25,3 +25,5 @@ export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extra
25
25
  export type { InputCompactionRule, ObservationMaskConfig, MaskResult, ObservationMaskStats, } from './observation-masker.js';
26
26
  export { DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned } from './dead-message-pruner.js';
27
27
  export type { PruneConfig, PruneResult, PruneStats } from './dead-message-pruner.js';
28
+ export { applyWindowing, groupIntoTurnPairs, identifyZones, scoreTurnImportance, softCompact, compactToEventLog, DEFAULT_WINDOWING_CONFIG, } from './windowing.js';
29
+ export type { WindowingConfig, WindowingResult, TurnPair, ImportanceLevel } from './windowing.js';
@@ -21,3 +21,5 @@ export { compactToolResult } from './result-compactor.js';
21
21
  export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
22
22
  // Dead Message Pruning (Phase 4 Token Optimization)
23
23
  export { DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned } from './dead-message-pruner.js';
24
+ // Smart Windowing (Programmatic Context Compaction)
25
+ export { applyWindowing, groupIntoTurnPairs, identifyZones, scoreTurnImportance, softCompact, compactToEventLog, DEFAULT_WINDOWING_CONFIG, } from './windowing.js';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Smart Windowing — Programmatic context compaction
3
+ *
4
+ * Three-zone architecture:
5
+ * Zone 1 (recent): Intact — last N tokens of conversation
6
+ * Zone 2 (middle): Soft-compacted — importance-aware truncation
7
+ * Zone 3 (old): Event log — one line per turn
8
+ *
9
+ * Trigger: total history tokens > targetHistoryTokens
10
+ * Method: Purely programmatic (zero LLM calls)
11
+ *
12
+ * @see /workspace/project-docs/00-requirements/compilr-dev-agents/smart-windowing-spec.md
13
+ */
14
+ import type { Message } from '../providers/types.js';
15
+ export interface WindowingConfig {
16
+ /** Target maximum tokens for conversation history. Default: 60000 */
17
+ targetHistoryTokens: number;
18
+ /** Tokens reserved for recent window (never compacted). Default: 15000 */
19
+ recentWindowTokens: number;
20
+ /** Whether windowing is enabled. Default: true */
21
+ enabled: boolean;
22
+ }
23
+ export declare const DEFAULT_WINDOWING_CONFIG: WindowingConfig;
24
+ export type ImportanceLevel = 'high' | 'medium' | 'low';
25
+ export interface TurnPair {
26
+ /** User messages (usually 1, may include tool_result follow-ups) */
27
+ userMessages: Message[];
28
+ /** Assistant messages (1+ including tool loops) */
29
+ assistantMessages: Message[];
30
+ /** Total tokens for this turn pair */
31
+ tokenCount: number;
32
+ /** Importance score */
33
+ importance: ImportanceLevel;
34
+ }
35
+ interface Zones {
36
+ recent: TurnPair[];
37
+ middle: TurnPair[];
38
+ old: TurnPair[];
39
+ }
40
+ export interface WindowingResult {
41
+ /** Whether windowing was applied */
42
+ applied: boolean;
43
+ /** Messages after windowing */
44
+ messages: Message[];
45
+ /** Tokens before windowing */
46
+ tokensBefore: number;
47
+ /** Tokens after windowing */
48
+ tokensAfter: number;
49
+ /** Zone sizes */
50
+ zones: {
51
+ recent: number;
52
+ middle: number;
53
+ old: number;
54
+ };
55
+ /** Importance distribution */
56
+ importanceCounts: Record<ImportanceLevel, number>;
57
+ }
58
+ export declare function groupIntoTurnPairs(messages: Message[]): TurnPair[];
59
+ export declare function identifyZones(turnPairs: TurnPair[], config: WindowingConfig): Zones;
60
+ export declare function scoreTurnImportance(pair: TurnPair, allPairs: TurnPair[], index: number): ImportanceLevel;
61
+ export declare function softCompact(turnPairs: TurnPair[]): Message[];
62
+ export declare function compactToEventLog(turnPairs: TurnPair[]): Message[];
63
+ export declare function applyWindowing(messages: Message[], config?: WindowingConfig): WindowingResult;
64
+ export {};
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Smart Windowing — Programmatic context compaction
3
+ *
4
+ * Three-zone architecture:
5
+ * Zone 1 (recent): Intact — last N tokens of conversation
6
+ * Zone 2 (middle): Soft-compacted — importance-aware truncation
7
+ * Zone 3 (old): Event log — one line per turn
8
+ *
9
+ * Trigger: total history tokens > targetHistoryTokens
10
+ * Method: Purely programmatic (zero LLM calls)
11
+ *
12
+ * @see /workspace/project-docs/00-requirements/compilr-dev-agents/smart-windowing-spec.md
13
+ */
14
+ import { countMessageTokens } from '../utils/tokenizer.js';
15
+ import { repairToolPairing } from '../messages/index.js';
16
+ export const DEFAULT_WINDOWING_CONFIG = {
17
+ targetHistoryTokens: 60_000,
18
+ recentWindowTokens: 15_000,
19
+ enabled: true,
20
+ };
21
+ // =============================================================================
22
+ // Importance Scoring — Keyword Patterns
23
+ // =============================================================================
24
+ const HIGH_IMPORTANCE_USER = /\b(let's use|don't use|go with|switch to|the approach is|instead of|not that|actually|wrong|no,|stop using|prefer|must use|never|always|requirement is|constraint is|decided|agreed|architecture|stack|database|framework|pattern|schema|API|endpoint|migration|deployment)\b/i;
25
+ const HIGH_IMPORTANCE_AGENT = /\b(I'll use|understood|switching to|noted.*won't|will not use|as you requested|per your instruction|confirmed|decision)\b/i;
26
+ const LOW_IMPORTANCE_USER = /^(ok|sure|got it|understood|will do|sounds good|perfect|great|yes|yeah|yep|right|correct|exactly|thanks|thank you|good|nice|cool)[\s.!]*$/i;
27
+ const LOW_IMPORTANCE_AGENT = /^(I've |I have |Done[.!]|Here's what|The changes|Updated |Created |Modified |Deleted |Successfully )/i;
28
+ const LOW_IMPORTANCE_TOOLS = new Set([
29
+ 'get_tool_info',
30
+ 'list_tools',
31
+ 'load_capability',
32
+ 'suggest',
33
+ 'todo_read',
34
+ 'todo_write',
35
+ ]);
36
+ // =============================================================================
37
+ // Text Extraction Helpers
38
+ // =============================================================================
39
+ function extractText(messages) {
40
+ const parts = [];
41
+ for (const msg of messages) {
42
+ if (typeof msg.content === 'string') {
43
+ parts.push(msg.content);
44
+ }
45
+ else {
46
+ for (const block of msg.content) {
47
+ if (block.type === 'text') {
48
+ parts.push(block.text);
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return parts.join(' ');
54
+ }
55
+ function extractToolUseBlocks(messages) {
56
+ const blocks = [];
57
+ for (const msg of messages) {
58
+ if (typeof msg.content !== 'string') {
59
+ for (const block of msg.content) {
60
+ if (block.type === 'tool_use') {
61
+ blocks.push(block);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ return blocks;
67
+ }
68
+ function hasErrorToolResult(messages) {
69
+ for (const msg of messages) {
70
+ if (typeof msg.content !== 'string') {
71
+ for (const block of msg.content) {
72
+ if (block.type === 'tool_result' && block.isError)
73
+ return true;
74
+ }
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+ // =============================================================================
80
+ // Step 1: Group Messages into Turn Pairs
81
+ // =============================================================================
82
+ export function groupIntoTurnPairs(messages) {
83
+ const pairs = [];
84
+ let currentUser = [];
85
+ let currentAssistant = [];
86
+ for (const msg of messages) {
87
+ if (msg.role === 'user') {
88
+ // Close previous turn pair if we have assistant messages
89
+ if (currentAssistant.length > 0) {
90
+ pairs.push({
91
+ userMessages: currentUser,
92
+ assistantMessages: currentAssistant,
93
+ tokenCount: countMessageTokens([...currentUser, ...currentAssistant]),
94
+ importance: 'medium',
95
+ });
96
+ currentUser = [];
97
+ currentAssistant = [];
98
+ }
99
+ currentUser.push(msg);
100
+ }
101
+ else if (msg.role === 'assistant') {
102
+ currentAssistant.push(msg);
103
+ }
104
+ // Skip system messages — handled separately
105
+ }
106
+ // Close final pair
107
+ if (currentUser.length > 0 || currentAssistant.length > 0) {
108
+ pairs.push({
109
+ userMessages: currentUser,
110
+ assistantMessages: currentAssistant,
111
+ tokenCount: countMessageTokens([...currentUser, ...currentAssistant]),
112
+ importance: 'medium',
113
+ });
114
+ }
115
+ return pairs;
116
+ }
117
+ // =============================================================================
118
+ // Step 2: Identify Zones (Token-Based)
119
+ // =============================================================================
120
+ export function identifyZones(turnPairs, config) {
121
+ // Zone 1: work backwards, accumulating tokens up to recentWindowTokens
122
+ let recentTokens = 0;
123
+ let zone1Start = turnPairs.length;
124
+ for (let i = turnPairs.length - 1; i >= 0; i--) {
125
+ if (recentTokens + turnPairs[i].tokenCount > config.recentWindowTokens) {
126
+ break;
127
+ }
128
+ recentTokens += turnPairs[i].tokenCount;
129
+ zone1Start = i;
130
+ }
131
+ const recent = turnPairs.slice(zone1Start);
132
+ const older = turnPairs.slice(0, zone1Start);
133
+ if (older.length === 0) {
134
+ return { recent, middle: [], old: [] };
135
+ }
136
+ // Zone 2/3 split: older half = Zone 3, newer half = Zone 2
137
+ const zone3End = Math.floor(older.length / 2);
138
+ return {
139
+ old: older.slice(0, zone3End),
140
+ middle: older.slice(zone3End),
141
+ recent,
142
+ };
143
+ }
144
+ // =============================================================================
145
+ // Step 3: Importance Scoring
146
+ // =============================================================================
147
+ export function scoreTurnImportance(pair, allPairs, index) {
148
+ const userText = extractText(pair.userMessages);
149
+ const assistantText = extractText(pair.assistantMessages);
150
+ const toolCalls = extractToolUseBlocks(pair.assistantMessages);
151
+ // Low: error-then-retry pattern (superseded by successful retry)
152
+ if (isErrorRetryPattern(allPairs, index))
153
+ return 'low';
154
+ // Low: turn consists entirely of low-importance tools and short messages
155
+ const allToolsLow = toolCalls.length > 0 && toolCalls.every((t) => LOW_IMPORTANCE_TOOLS.has(t.name));
156
+ if (allToolsLow && userText.length < 50 && assistantText.length < 200)
157
+ return 'low';
158
+ // Low: pure acknowledgment from user + short agent response
159
+ if (LOW_IMPORTANCE_USER.test(userText.trim()) && userText.length < 100)
160
+ return 'low';
161
+ // High: user made a decision or correction
162
+ if (HIGH_IMPORTANCE_USER.test(userText))
163
+ return 'high';
164
+ // High: agent confirmed a decision
165
+ if (HIGH_IMPORTANCE_AGENT.test(assistantText))
166
+ return 'high';
167
+ // Low: agent recap without user decision
168
+ if (LOW_IMPORTANCE_AGENT.test(assistantText.trim()) && userText.length < 50)
169
+ return 'low';
170
+ // Medium: everything else
171
+ return 'medium';
172
+ }
173
+ function isErrorRetryPattern(allPairs, index) {
174
+ if (index + 1 >= allPairs.length)
175
+ return false;
176
+ const current = allPairs[index];
177
+ const next = allPairs[index + 1];
178
+ // Current turn has a failed tool call
179
+ if (!hasErrorToolResult([...current.userMessages, ...current.assistantMessages]))
180
+ return false;
181
+ // Next turn retries a tool with the same name
182
+ const currentToolNames = new Set(extractToolUseBlocks(current.assistantMessages).map((t) => t.name));
183
+ const nextToolNames = extractToolUseBlocks(next.assistantMessages).map((t) => t.name);
184
+ return nextToolNames.some((name) => currentToolNames.has(name));
185
+ }
186
+ // =============================================================================
187
+ // Step 4: Zone 2 — Soft Compaction
188
+ // =============================================================================
189
+ const TRUNCATION_LIMITS = {
190
+ high: { userChars: Infinity, assistantChars: Infinity },
191
+ medium: { userChars: 300, assistantChars: 500 },
192
+ low: { userChars: 100, assistantChars: 200 },
193
+ };
194
+ export function softCompact(turnPairs) {
195
+ const result = [];
196
+ for (const pair of turnPairs) {
197
+ const limits = TRUNCATION_LIMITS[pair.importance];
198
+ for (const msg of pair.userMessages) {
199
+ result.push(compactMessage(msg, limits.userChars));
200
+ }
201
+ for (const msg of pair.assistantMessages) {
202
+ result.push(compactMessage(msg, limits.assistantChars));
203
+ }
204
+ }
205
+ return result;
206
+ }
207
+ function compactMessage(msg, maxChars) {
208
+ if (typeof msg.content === 'string') {
209
+ if (msg.content.length <= maxChars)
210
+ return msg;
211
+ const omitted = msg.content.length - maxChars;
212
+ return {
213
+ ...msg,
214
+ content: `${msg.content.slice(0, maxChars)}... [+${String(omitted)} chars]`,
215
+ };
216
+ }
217
+ // Multi-block message: process each block
218
+ const compactedBlocks = [];
219
+ for (const block of msg.content) {
220
+ switch (block.type) {
221
+ case 'text': {
222
+ if (block.text.length <= maxChars) {
223
+ compactedBlocks.push(block);
224
+ }
225
+ else {
226
+ const omitted = block.text.length - maxChars;
227
+ compactedBlocks.push({
228
+ ...block,
229
+ text: `${block.text.slice(0, maxChars)}... [+${String(omitted)} chars]`,
230
+ });
231
+ }
232
+ break;
233
+ }
234
+ case 'tool_use': {
235
+ // Keep name + compact input summary
236
+ const inputStr = JSON.stringify(block.input);
237
+ compactedBlocks.push({
238
+ ...block,
239
+ input: inputStr.length > 100
240
+ ? { _summary: `${block.name}(${inputStr.slice(0, 80)}...)` }
241
+ : block.input,
242
+ });
243
+ break;
244
+ }
245
+ case 'tool_result': {
246
+ // Already masked by observation masker — keep as-is if short
247
+ if (block.content.length <= 200) {
248
+ compactedBlocks.push(block);
249
+ }
250
+ else {
251
+ compactedBlocks.push({
252
+ ...block,
253
+ content: `${block.content.slice(0, 150)}... [+${String(block.content.length - 150)} chars]`,
254
+ });
255
+ }
256
+ break;
257
+ }
258
+ default:
259
+ // thinking blocks etc. — drop in compacted zone
260
+ break;
261
+ }
262
+ }
263
+ return { ...msg, content: compactedBlocks };
264
+ }
265
+ // =============================================================================
266
+ // Step 5: Zone 3 — Event Log
267
+ // =============================================================================
268
+ export function compactToEventLog(turnPairs) {
269
+ if (turnPairs.length === 0)
270
+ return [];
271
+ const lines = [];
272
+ for (let i = 0; i < turnPairs.length; i++) {
273
+ lines.push(generateEventLine(i + 1, turnPairs[i]));
274
+ }
275
+ // Cap event log at ~3K tokens (~12K chars)
276
+ const MAX_EVENT_LOG_CHARS = 12_000;
277
+ let eventLog = lines.join('\n');
278
+ if (eventLog.length > MAX_EVENT_LOG_CHARS) {
279
+ // Drop oldest lines until under cap
280
+ while (lines.length > 1 && eventLog.length > MAX_EVENT_LOG_CHARS) {
281
+ lines.shift();
282
+ eventLog = `[+${String(turnPairs.length - lines.length)} earlier turns omitted]\n${lines.join('\n')}`;
283
+ }
284
+ }
285
+ return [
286
+ {
287
+ role: 'user',
288
+ content: `[Conversation history — ${String(turnPairs.length)} earlier turns compacted]\n\n${eventLog}\n\n[End of compacted history. Recent conversation follows.]`,
289
+ },
290
+ {
291
+ role: 'assistant',
292
+ content: 'Understood. I have the context from our earlier conversation.',
293
+ },
294
+ ];
295
+ }
296
+ function generateEventLine(turnIndex, pair) {
297
+ const userText = extractText(pair.userMessages).replace(/\n/g, ' ').trim().slice(0, 80);
298
+ const toolCalls = extractToolUseBlocks(pair.assistantMessages);
299
+ const isHighImportance = pair.importance === 'high';
300
+ const marker = isHighImportance ? '\u2605 ' : '';
301
+ if (toolCalls.length === 0) {
302
+ // Conversation-only turn
303
+ const agentText = extractText(pair.assistantMessages).replace(/\n/g, ' ').trim().slice(0, 80);
304
+ return `[Turn ${String(turnIndex)}] ${marker}User: "${userText}" \u2192 "${agentText}"`;
305
+ }
306
+ // Summarize tool usage
307
+ const toolSummary = summarizeTools(toolCalls);
308
+ return `[Turn ${String(turnIndex)}] ${marker}User: "${userText}" \u2192 ${toolSummary}`;
309
+ }
310
+ function summarizeTools(toolCalls) {
311
+ const reads = toolCalls.filter((t) => [
312
+ 'read_file',
313
+ 'grep',
314
+ 'glob',
315
+ 'git_diff',
316
+ 'git_log',
317
+ 'git_status',
318
+ 'project_document_get',
319
+ 'find_definition',
320
+ 'find_references',
321
+ 'web_fetch',
322
+ 'detect_project',
323
+ 'get_file_structure',
324
+ ].includes(t.name));
325
+ const writes = toolCalls.filter((t) => ['write_file', 'edit', 'git_commit', 'project_document_add', 'project_document_patch'].includes(t.name));
326
+ const runs = toolCalls.filter((t) => ['bash', 'run_tests', 'run_lint', 'run_build', 'run_format', 'task'].includes(t.name));
327
+ const other = toolCalls.filter((t) => !reads.includes(t) && !writes.includes(t) && !runs.includes(t));
328
+ const parts = [];
329
+ if (reads.length > 0)
330
+ parts.push(`read ${String(reads.length)}`);
331
+ if (writes.length > 0) {
332
+ const files = writes.map((t) => extractFileName(t.input)).filter(Boolean);
333
+ parts.push(files.length > 0 ? `edited ${files.join(', ')}` : `edited ${String(writes.length)}`);
334
+ }
335
+ if (runs.length > 0) {
336
+ const names = [...new Set(runs.map((t) => t.name))];
337
+ parts.push(names.join(', '));
338
+ }
339
+ if (other.length > 0)
340
+ parts.push(`${String(other.length)} other`);
341
+ return parts.join(', ') || `${String(toolCalls.length)} tools`;
342
+ }
343
+ function extractFileName(input) {
344
+ const path = (input.path ?? input.filePath ?? input.file ?? '');
345
+ if (!path)
346
+ return '';
347
+ const parts = path.split('/');
348
+ return parts[parts.length - 1];
349
+ }
350
+ // =============================================================================
351
+ // Main: Apply Windowing
352
+ // =============================================================================
353
+ export function applyWindowing(messages, config = DEFAULT_WINDOWING_CONFIG) {
354
+ // Separate system messages (never compacted)
355
+ const systemMsgs = messages.filter((m) => m.role === 'system');
356
+ const historyMsgs = messages.filter((m) => m.role !== 'system');
357
+ const tokensBefore = countMessageTokens(historyMsgs);
358
+ // Check if windowing is needed
359
+ if (!config.enabled || tokensBefore <= config.targetHistoryTokens) {
360
+ return {
361
+ applied: false,
362
+ messages,
363
+ tokensBefore,
364
+ tokensAfter: tokensBefore,
365
+ zones: { recent: historyMsgs.length, middle: 0, old: 0 },
366
+ importanceCounts: { high: 0, medium: 0, low: 0 },
367
+ };
368
+ }
369
+ // Step 1: Group into turn pairs
370
+ const turnPairs = groupIntoTurnPairs(historyMsgs);
371
+ // Step 2: Identify zones
372
+ const zones = identifyZones(turnPairs, config);
373
+ // Step 3: Score importance for Zone 2 and 3
374
+ const allOlder = [...zones.old, ...zones.middle];
375
+ for (let i = 0; i < allOlder.length; i++) {
376
+ allOlder[i].importance = scoreTurnImportance(allOlder[i], allOlder, i);
377
+ }
378
+ // Count importance distribution
379
+ const importanceCounts = { high: 0, medium: 0, low: 0 };
380
+ for (const pair of allOlder) {
381
+ importanceCounts[pair.importance]++;
382
+ }
383
+ // Step 4: Compact Zone 3 → event log
384
+ const zone3Messages = compactToEventLog(zones.old);
385
+ // Step 5: Compact Zone 2 → soft truncation
386
+ const zone2Messages = softCompact(zones.middle);
387
+ // Step 6: Flatten Zone 1 (recent — intact)
388
+ const zone1Messages = [];
389
+ for (const pair of zones.recent) {
390
+ zone1Messages.push(...pair.userMessages, ...pair.assistantMessages);
391
+ }
392
+ // Step 7: Inject compaction notice between compacted and recent zones
393
+ const compactionNotice = zones.old.length > 0 || zones.middle.length > 0
394
+ ? [
395
+ {
396
+ role: 'user',
397
+ content: `[Context note: ${String(zones.old.length)} turns were compacted to event log, ${String(zones.middle.length)} turns were soft-compacted. Use recall_work to access full history if needed.]`,
398
+ },
399
+ {
400
+ role: 'assistant',
401
+ content: "Understood. I'll work with the available context and use recall_work if I need details from earlier.",
402
+ },
403
+ ]
404
+ : [];
405
+ // Step 8: Reassemble
406
+ let compactedHistory = [
407
+ ...zone3Messages,
408
+ ...zone2Messages,
409
+ ...compactionNotice,
410
+ ...zone1Messages,
411
+ ];
412
+ // Step 8: Repair tool pairing (orphaned tool_use/tool_result)
413
+ compactedHistory = repairToolPairing(compactedHistory);
414
+ const finalMessages = [...systemMsgs, ...compactedHistory];
415
+ const tokensAfter = countMessageTokens(compactedHistory);
416
+ return {
417
+ applied: true,
418
+ messages: finalMessages,
419
+ tokensBefore,
420
+ tokensAfter,
421
+ zones: {
422
+ recent: zones.recent.length,
423
+ middle: zones.middle.length,
424
+ old: zones.old.length,
425
+ },
426
+ importanceCounts,
427
+ };
428
+ }
package/dist/index.d.ts CHANGED
@@ -40,7 +40,7 @@ export { generateId, sleep, retry, truncate, withRetryGenerator, calculateBackof
40
40
  export type { RetryConfig as LLMRetryConfig, WithRetryOptions } from './utils/index.js';
41
41
  export { AgentError, ProviderError, ToolError, ToolTimeoutError, ToolLoopError, ValidationError, MaxIterationsError, AbortError, ContextOverflowError, isAgentError, isProviderError, isToolError, isToolTimeoutError, isToolLoopError, isContextOverflowError, wrapError, } from './errors.js';
42
42
  export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG, compactToolResult, ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned, } from './context/index.js';
43
- export type { ContextManagerOptions, ContextCategory, BudgetAllocation, CategoryBudgetInfo, PreflightResult, VerbosityLevel, VerbosityConfig, ContextConfig, FilteringConfig, CompactionConfig, SummarizationConfig, CompactionResult, SummarizationResult, FilteringResult, ContextEvent, ContextEventHandler, ContextStats, FileAccessType, FileAccess, FileAccessTrackerOptions, FormatHintsOptions, FileAccessStats, RestorationHintMessage, DelegatedResultStoreStats, ToolResultDelegatorOptions, DelegationConfig, StoredResult, DelegationEvent, InputCompactionRule, ObservationMaskConfig, MaskResult, ObservationMaskStats, PruneConfig, PruneResult, PruneStats, } from './context/index.js';
43
+ export type { ContextManagerOptions, ContextCategory, BudgetAllocation, CategoryBudgetInfo, PreflightResult, VerbosityLevel, VerbosityConfig, ContextConfig, FilteringConfig, CompactionConfig, SummarizationConfig, CompactionResult, SummarizationResult, FilteringResult, ContextEvent, ContextEventHandler, ContextStats, FileAccessType, FileAccess, FileAccessTrackerOptions, FormatHintsOptions, FileAccessStats, RestorationHintMessage, DelegatedResultStoreStats, ToolResultDelegatorOptions, DelegationConfig, StoredResult, DelegationEvent, InputCompactionRule, ObservationMaskConfig, MaskResult, ObservationMaskStats, PruneConfig, PruneResult, PruneStats, WindowingConfig, WindowingResult, ImportanceLevel, } from './context/index.js';
44
44
  export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
45
45
  export type { Skill, SkillInvocationResult, SkillInvokeOptions } from './skills/index.js';
46
46
  export { JsonSerializer, CompactJsonSerializer, defaultSerializer, MemoryCheckpointer, FileCheckpointer, StateError, StateErrorCode, CURRENT_STATE_VERSION, } from './state/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.3.28",
3
+ "version": "0.4.0",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",