@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 +12 -0
- package/dist/agent.js +30 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/windowing.d.ts +64 -0
- package/dist/context/windowing.js +428 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
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()) {
|
package/dist/context/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/context/index.js
CHANGED
|
@@ -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';
|