@compilr-dev/agents 0.3.18 → 0.3.20
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 +21 -1
- package/dist/agent.js +29 -1
- package/dist/context/dead-message-pruner.d.ts +65 -0
- package/dist/context/dead-message-pruner.js +174 -0
- package/dist/context/index.d.ts +4 -2
- package/dist/context/index.js +4 -2
- package/dist/context/observation-masker.d.ts +26 -2
- package/dist/context/observation-masker.js +97 -35
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- 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 { PruneConfig } from './context/dead-message-pruner.js';
|
|
19
20
|
import { AnchorManager } from './anchors/manager.js';
|
|
20
21
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
21
22
|
import { type RetryConfig } from './utils/index.js';
|
|
@@ -290,6 +291,11 @@ export interface AgentConfig {
|
|
|
290
291
|
* Enabled by default when contextManager is provided. Set to `false` to disable.
|
|
291
292
|
*/
|
|
292
293
|
compactToolResults?: boolean;
|
|
294
|
+
/**
|
|
295
|
+
* Dead message pruning configuration. Prunes superseded errors and permission exchanges.
|
|
296
|
+
* Enabled by default when contextManager is provided. Set to `false` to disable.
|
|
297
|
+
*/
|
|
298
|
+
deadMessagePruning?: Partial<PruneConfig> | false;
|
|
293
299
|
/**
|
|
294
300
|
* Event handler for monitoring agent execution
|
|
295
301
|
*/
|
|
@@ -898,6 +904,10 @@ export declare class Agent {
|
|
|
898
904
|
* Whether to use compact text format for tool results in LLM messages
|
|
899
905
|
*/
|
|
900
906
|
private readonly compactToolResults;
|
|
907
|
+
/**
|
|
908
|
+
* Dead message pruner for removing superseded errors and permission exchanges
|
|
909
|
+
*/
|
|
910
|
+
private readonly deadMessagePruner?;
|
|
901
911
|
constructor(config: AgentConfig);
|
|
902
912
|
/**
|
|
903
913
|
* Create an agent with project memory loaded from files.
|
|
@@ -1301,12 +1311,22 @@ export declare class Agent {
|
|
|
1301
1311
|
*/
|
|
1302
1312
|
getContextStats(): ContextStats | undefined;
|
|
1303
1313
|
/**
|
|
1304
|
-
* Get observation masking statistics (tokens saved, observations masked).
|
|
1314
|
+
* Get observation masking statistics (tokens saved, observations masked, inputs compacted).
|
|
1305
1315
|
*/
|
|
1306
1316
|
getObservationMaskStats(): {
|
|
1307
1317
|
maskedCount: number;
|
|
1308
1318
|
tokensSaved: number;
|
|
1309
1319
|
activeStamps: number;
|
|
1320
|
+
inputsCompacted: number;
|
|
1321
|
+
} | undefined;
|
|
1322
|
+
/**
|
|
1323
|
+
* Get dead message pruning statistics (errors pruned, permissions pruned, tokens saved).
|
|
1324
|
+
*/
|
|
1325
|
+
getDeadMessagePruneStats(): {
|
|
1326
|
+
prunedCount: number;
|
|
1327
|
+
tokensSaved: number;
|
|
1328
|
+
errorsPruned: number;
|
|
1329
|
+
permissionsPruned: number;
|
|
1310
1330
|
} | undefined;
|
|
1311
1331
|
/**
|
|
1312
1332
|
* Get current verbosity level based on context pressure
|
package/dist/agent.js
CHANGED
|
@@ -12,6 +12,7 @@ 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
14
|
import { compactToolResult } from './context/result-compactor.js';
|
|
15
|
+
import { DeadMessagePruner } from './context/dead-message-pruner.js';
|
|
15
16
|
import { createRecallResultTool } from './tools/builtin/recall-result.js';
|
|
16
17
|
import { AnchorManager } from './anchors/manager.js';
|
|
17
18
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
@@ -107,6 +108,10 @@ export class Agent {
|
|
|
107
108
|
* Whether to use compact text format for tool results in LLM messages
|
|
108
109
|
*/
|
|
109
110
|
compactToolResults;
|
|
111
|
+
/**
|
|
112
|
+
* Dead message pruner for removing superseded errors and permission exchanges
|
|
113
|
+
*/
|
|
114
|
+
deadMessagePruner;
|
|
110
115
|
constructor(config) {
|
|
111
116
|
this.provider = config.provider;
|
|
112
117
|
this.systemPrompt = config.systemPrompt ?? '';
|
|
@@ -128,6 +133,10 @@ export class Agent {
|
|
|
128
133
|
}
|
|
129
134
|
// Compact tool results: enabled by default when contextManager is provided
|
|
130
135
|
this.compactToolResults = config.compactToolResults ?? this.contextManager !== undefined;
|
|
136
|
+
// Dead message pruning: enabled by default when contextManager is provided, unless explicitly false
|
|
137
|
+
if (config.deadMessagePruning !== false && this.contextManager) {
|
|
138
|
+
this.deadMessagePruner = new DeadMessagePruner(config.deadMessagePruning === undefined ? undefined : config.deadMessagePruning);
|
|
139
|
+
}
|
|
131
140
|
this.onEvent = config.onEvent;
|
|
132
141
|
this.onIterationLimitReached = config.onIterationLimitReached;
|
|
133
142
|
// State management
|
|
@@ -769,6 +778,7 @@ export class Agent {
|
|
|
769
778
|
this.conversationHistory = [];
|
|
770
779
|
this.contextManager?.reset();
|
|
771
780
|
this.observationMasker?.reset();
|
|
781
|
+
this.deadMessagePruner?.reset();
|
|
772
782
|
return this;
|
|
773
783
|
}
|
|
774
784
|
/**
|
|
@@ -825,11 +835,17 @@ export class Agent {
|
|
|
825
835
|
return this.contextManager?.getStats(this.conversationHistory.length);
|
|
826
836
|
}
|
|
827
837
|
/**
|
|
828
|
-
* Get observation masking statistics (tokens saved, observations masked).
|
|
838
|
+
* Get observation masking statistics (tokens saved, observations masked, inputs compacted).
|
|
829
839
|
*/
|
|
830
840
|
getObservationMaskStats() {
|
|
831
841
|
return this.observationMasker?.getStats();
|
|
832
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* Get dead message pruning statistics (errors pruned, permissions pruned, tokens saved).
|
|
845
|
+
*/
|
|
846
|
+
getDeadMessagePruneStats() {
|
|
847
|
+
return this.deadMessagePruner?.getStats();
|
|
848
|
+
}
|
|
833
849
|
/**
|
|
834
850
|
* Get current verbosity level based on context pressure
|
|
835
851
|
*/
|
|
@@ -2103,6 +2119,10 @@ export class Agent {
|
|
|
2103
2119
|
const block = toolResultMsg.content[0];
|
|
2104
2120
|
this.observationMasker.stamp(toolUse.id, toolUse.name, toolUse.input, block.content.length, this.contextManager?.getTurnCount() ?? 0);
|
|
2105
2121
|
}
|
|
2122
|
+
// Stamp for dead message pruning
|
|
2123
|
+
if (this.deadMessagePruner) {
|
|
2124
|
+
this.deadMessagePruner.stamp(toolUse.id, toolUse.name, toolUse.input, !result.success, this.contextManager?.getTurnCount() ?? 0);
|
|
2125
|
+
}
|
|
2106
2126
|
}
|
|
2107
2127
|
}
|
|
2108
2128
|
else {
|
|
@@ -2142,6 +2162,10 @@ export class Agent {
|
|
|
2142
2162
|
const block = toolResultMsg.content[0];
|
|
2143
2163
|
this.observationMasker.stamp(toolUse.id, toolUse.name, toolUse.input, block.content.length, this.contextManager?.getTurnCount() ?? 0);
|
|
2144
2164
|
}
|
|
2165
|
+
// Stamp for dead message pruning
|
|
2166
|
+
if (this.deadMessagePruner) {
|
|
2167
|
+
this.deadMessagePruner.stamp(toolUse.id, toolUse.name, toolUse.input, !result.success, this.contextManager?.getTurnCount() ?? 0);
|
|
2168
|
+
}
|
|
2145
2169
|
if (skipped) {
|
|
2146
2170
|
continue;
|
|
2147
2171
|
}
|
|
@@ -2264,6 +2288,10 @@ export class Agent {
|
|
|
2264
2288
|
if (this.observationMasker) {
|
|
2265
2289
|
this.observationMasker.maskHistory(messages, this.contextManager.getTurnCount());
|
|
2266
2290
|
}
|
|
2291
|
+
// Dead message pruning: prune superseded errors and permission exchanges
|
|
2292
|
+
if (this.deadMessagePruner) {
|
|
2293
|
+
this.deadMessagePruner.prune(messages, this.contextManager.getTurnCount());
|
|
2294
|
+
}
|
|
2267
2295
|
await this.contextManager.updateTokenCount(messages);
|
|
2268
2296
|
}
|
|
2269
2297
|
// Update internal state tracking
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead Message Pruner — Phase 4 of Advanced Token Optimization
|
|
3
|
+
*
|
|
4
|
+
* Prunes provably obsolete messages in conversation history:
|
|
5
|
+
* 1. Superseded errors — tool error followed by same tool succeeding on same input
|
|
6
|
+
* 2. Permission exchanges — ask_user/ask_user_simple calls (purely procedural)
|
|
7
|
+
*
|
|
8
|
+
* Uses in-place content replacement (not message removal) to preserve
|
|
9
|
+
* the tool_use/tool_result pairing required by the Anthropic API.
|
|
10
|
+
*/
|
|
11
|
+
import type { Message } from '../providers/types.js';
|
|
12
|
+
export interface PruneConfig {
|
|
13
|
+
/** Enable superseded error pruning (default: true) */
|
|
14
|
+
supersededErrors: boolean;
|
|
15
|
+
/** Enable permission exchange pruning (default: true) */
|
|
16
|
+
permissionExchanges: boolean;
|
|
17
|
+
/** Tool names considered permission exchanges */
|
|
18
|
+
permissionTools: string[];
|
|
19
|
+
/** Never prune messages newer than this many turns (default: 4) */
|
|
20
|
+
protectedTurns: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const DEFAULT_PRUNE_CONFIG: PruneConfig;
|
|
23
|
+
export interface PruneResult {
|
|
24
|
+
prunedCount: number;
|
|
25
|
+
tokensSaved: number;
|
|
26
|
+
}
|
|
27
|
+
export interface PruneStats {
|
|
28
|
+
prunedCount: number;
|
|
29
|
+
tokensSaved: number;
|
|
30
|
+
errorsPruned: number;
|
|
31
|
+
permissionsPruned: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if a tool result content string has been pruned.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isPruned(content: string): boolean;
|
|
37
|
+
export declare class DeadMessagePruner {
|
|
38
|
+
private readonly config;
|
|
39
|
+
private readonly stamps;
|
|
40
|
+
private stats;
|
|
41
|
+
constructor(config?: Partial<PruneConfig>);
|
|
42
|
+
/**
|
|
43
|
+
* Register a tool result for pruning analysis.
|
|
44
|
+
* Called at the same point as ObservationMasker.stamp().
|
|
45
|
+
*/
|
|
46
|
+
stamp(toolUseId: string, toolName: string, input: Record<string, unknown>, isError: boolean, turn: number): void;
|
|
47
|
+
/**
|
|
48
|
+
* Prune dead messages in-place. Replaces content of dead tool_result
|
|
49
|
+
* blocks with short placeholders and clears corresponding tool_use input.
|
|
50
|
+
*/
|
|
51
|
+
prune(messages: Message[], currentTurn: number): PruneResult;
|
|
52
|
+
getStats(): PruneStats;
|
|
53
|
+
/**
|
|
54
|
+
* Reset all state (stamps and stats). Used when clearing history.
|
|
55
|
+
*/
|
|
56
|
+
reset(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Get current configuration (for testing/inspection).
|
|
59
|
+
*/
|
|
60
|
+
getConfig(): PruneConfig;
|
|
61
|
+
/**
|
|
62
|
+
* Identify tool_use IDs that should be pruned.
|
|
63
|
+
*/
|
|
64
|
+
private identifyDeadMessages;
|
|
65
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead Message Pruner — Phase 4 of Advanced Token Optimization
|
|
3
|
+
*
|
|
4
|
+
* Prunes provably obsolete messages in conversation history:
|
|
5
|
+
* 1. Superseded errors — tool error followed by same tool succeeding on same input
|
|
6
|
+
* 2. Permission exchanges — ask_user/ask_user_simple calls (purely procedural)
|
|
7
|
+
*
|
|
8
|
+
* Uses in-place content replacement (not message removal) to preserve
|
|
9
|
+
* the tool_use/tool_result pairing required by the Anthropic API.
|
|
10
|
+
*/
|
|
11
|
+
import { extractInputSummary } from './observation-masker.js';
|
|
12
|
+
import { isMasked } from './observation-masker.js';
|
|
13
|
+
export const DEFAULT_PRUNE_CONFIG = {
|
|
14
|
+
supersededErrors: true,
|
|
15
|
+
permissionExchanges: true,
|
|
16
|
+
permissionTools: ['ask_user', 'ask_user_simple'],
|
|
17
|
+
protectedTurns: 4,
|
|
18
|
+
};
|
|
19
|
+
// ============================================================
|
|
20
|
+
// Pruned content detection
|
|
21
|
+
// ============================================================
|
|
22
|
+
const PRUNED_ERROR = '[pruned:error superseded]';
|
|
23
|
+
const PRUNED_PERMISSION = '[pruned:permission]';
|
|
24
|
+
/**
|
|
25
|
+
* Check if a tool result content string has been pruned.
|
|
26
|
+
*/
|
|
27
|
+
export function isPruned(content) {
|
|
28
|
+
return content.startsWith('[pruned:');
|
|
29
|
+
}
|
|
30
|
+
// ============================================================
|
|
31
|
+
// DeadMessagePruner
|
|
32
|
+
// ============================================================
|
|
33
|
+
export class DeadMessagePruner {
|
|
34
|
+
config;
|
|
35
|
+
stamps = new Map();
|
|
36
|
+
stats = {
|
|
37
|
+
prunedCount: 0,
|
|
38
|
+
tokensSaved: 0,
|
|
39
|
+
errorsPruned: 0,
|
|
40
|
+
permissionsPruned: 0,
|
|
41
|
+
};
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.config = { ...DEFAULT_PRUNE_CONFIG, ...config };
|
|
44
|
+
}
|
|
45
|
+
// ----------------------------------------------------------
|
|
46
|
+
// Stamping — called when tool results are added to history
|
|
47
|
+
// ----------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Register a tool result for pruning analysis.
|
|
50
|
+
* Called at the same point as ObservationMasker.stamp().
|
|
51
|
+
*/
|
|
52
|
+
stamp(toolUseId, toolName, input, isError, turn) {
|
|
53
|
+
this.stamps.set(toolUseId, {
|
|
54
|
+
turn,
|
|
55
|
+
toolName,
|
|
56
|
+
inputKey: extractInputSummary(toolName, input),
|
|
57
|
+
isError,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// ----------------------------------------------------------
|
|
61
|
+
// Pruning — called after ObservationMasker.maskHistory()
|
|
62
|
+
// ----------------------------------------------------------
|
|
63
|
+
/**
|
|
64
|
+
* Prune dead messages in-place. Replaces content of dead tool_result
|
|
65
|
+
* blocks with short placeholders and clears corresponding tool_use input.
|
|
66
|
+
*/
|
|
67
|
+
prune(messages, currentTurn) {
|
|
68
|
+
// 1. Build set of tool_use IDs to prune
|
|
69
|
+
const toPrune = this.identifyDeadMessages(currentTurn);
|
|
70
|
+
if (toPrune.size === 0)
|
|
71
|
+
return { prunedCount: 0, tokensSaved: 0 };
|
|
72
|
+
// 2. Walk messages and replace content
|
|
73
|
+
let prunedCount = 0;
|
|
74
|
+
let tokensSaved = 0;
|
|
75
|
+
let errorsPruned = 0;
|
|
76
|
+
let permissionsPruned = 0;
|
|
77
|
+
for (const msg of messages) {
|
|
78
|
+
if (typeof msg.content === 'string')
|
|
79
|
+
continue;
|
|
80
|
+
for (const block of msg.content) {
|
|
81
|
+
if (block.type === 'tool_result' && toPrune.has(block.toolUseId)) {
|
|
82
|
+
// Skip if already masked by Phase 1 or already pruned
|
|
83
|
+
if (isMasked(block.content) || isPruned(block.content)) {
|
|
84
|
+
toPrune.delete(block.toolUseId); // don't clear tool_use input either
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const stamp = this.stamps.get(block.toolUseId);
|
|
88
|
+
if (!stamp)
|
|
89
|
+
continue;
|
|
90
|
+
const pruneLabel = stamp.isError ? PRUNED_ERROR : PRUNED_PERMISSION;
|
|
91
|
+
const savedChars = block.content.length - pruneLabel.length;
|
|
92
|
+
const savedTokens = Math.max(0, Math.ceil(savedChars / 4));
|
|
93
|
+
block.content = pruneLabel;
|
|
94
|
+
tokensSaved += savedTokens;
|
|
95
|
+
prunedCount++;
|
|
96
|
+
if (stamp.isError) {
|
|
97
|
+
errorsPruned++;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
permissionsPruned++;
|
|
101
|
+
}
|
|
102
|
+
// Clean up stamp
|
|
103
|
+
this.stamps.delete(block.toolUseId);
|
|
104
|
+
}
|
|
105
|
+
// Clear tool_use input for pruned blocks
|
|
106
|
+
if (block.type === 'tool_use' && toPrune.has(block.id)) {
|
|
107
|
+
const inputStr = JSON.stringify(block.input);
|
|
108
|
+
if (inputStr.length > 2) {
|
|
109
|
+
// Estimate savings from clearing input (subtract '{}')
|
|
110
|
+
tokensSaved += Math.max(0, Math.ceil((inputStr.length - 2) / 4));
|
|
111
|
+
}
|
|
112
|
+
block.input = {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.stats.prunedCount += prunedCount;
|
|
117
|
+
this.stats.tokensSaved += tokensSaved;
|
|
118
|
+
this.stats.errorsPruned += errorsPruned;
|
|
119
|
+
this.stats.permissionsPruned += permissionsPruned;
|
|
120
|
+
return { prunedCount, tokensSaved };
|
|
121
|
+
}
|
|
122
|
+
// ----------------------------------------------------------
|
|
123
|
+
// Stats
|
|
124
|
+
// ----------------------------------------------------------
|
|
125
|
+
getStats() {
|
|
126
|
+
return { ...this.stats };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Reset all state (stamps and stats). Used when clearing history.
|
|
130
|
+
*/
|
|
131
|
+
reset() {
|
|
132
|
+
this.stamps.clear();
|
|
133
|
+
this.stats = { prunedCount: 0, tokensSaved: 0, errorsPruned: 0, permissionsPruned: 0 };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get current configuration (for testing/inspection).
|
|
137
|
+
*/
|
|
138
|
+
getConfig() {
|
|
139
|
+
return { ...this.config };
|
|
140
|
+
}
|
|
141
|
+
// ----------------------------------------------------------
|
|
142
|
+
// Internal
|
|
143
|
+
// ----------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* Identify tool_use IDs that should be pruned.
|
|
146
|
+
*/
|
|
147
|
+
identifyDeadMessages(currentTurn) {
|
|
148
|
+
const toPrune = new Set();
|
|
149
|
+
// Build set of successful {toolName:inputKey} pairs
|
|
150
|
+
const successKeys = new Set();
|
|
151
|
+
for (const stamp of this.stamps.values()) {
|
|
152
|
+
if (!stamp.isError) {
|
|
153
|
+
successKeys.add(`${stamp.toolName}:${stamp.inputKey}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const [toolUseId, stamp] of this.stamps) {
|
|
157
|
+
const age = currentTurn - stamp.turn;
|
|
158
|
+
if (age < this.config.protectedTurns)
|
|
159
|
+
continue;
|
|
160
|
+
// Rule 1: Superseded errors
|
|
161
|
+
if (this.config.supersededErrors && stamp.isError) {
|
|
162
|
+
const key = `${stamp.toolName}:${stamp.inputKey}`;
|
|
163
|
+
if (successKeys.has(key)) {
|
|
164
|
+
toPrune.add(toolUseId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Rule 2: Permission exchanges
|
|
168
|
+
if (this.config.permissionExchanges && this.config.permissionTools.includes(stamp.toolName)) {
|
|
169
|
+
toPrune.add(toolUseId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return toPrune;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/dist/context/index.d.ts
CHANGED
|
@@ -21,5 +21,7 @@ export type { ToolResultDelegatorOptions } from './tool-result-delegator.js';
|
|
|
21
21
|
export { DEFAULT_DELEGATION_CONFIG } from './delegation-types.js';
|
|
22
22
|
export type { DelegationConfig, StoredResult, DelegationEvent } from './delegation-types.js';
|
|
23
23
|
export { compactToolResult } from './result-compactor.js';
|
|
24
|
-
export { ObservationMasker, DEFAULT_MASK_CONFIG, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
|
|
25
|
-
export type { ObservationMaskConfig, MaskResult, ObservationMaskStats, } from './observation-masker.js';
|
|
24
|
+
export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
|
|
25
|
+
export type { InputCompactionRule, ObservationMaskConfig, MaskResult, ObservationMaskStats, } from './observation-masker.js';
|
|
26
|
+
export { DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned } from './dead-message-pruner.js';
|
|
27
|
+
export type { PruneConfig, PruneResult, PruneStats } from './dead-message-pruner.js';
|
package/dist/context/index.js
CHANGED
|
@@ -17,5 +17,7 @@ export { ToolResultDelegator, DELEGATION_SYSTEM_PROMPT } from './tool-result-del
|
|
|
17
17
|
export { DEFAULT_DELEGATION_CONFIG } from './delegation-types.js';
|
|
18
18
|
// Compact Tool Result Formatting (Phase 2 Token Optimization)
|
|
19
19
|
export { compactToolResult } from './result-compactor.js';
|
|
20
|
-
// Observation Masking (Phase 1 Token Optimization)
|
|
21
|
-
export { ObservationMasker, DEFAULT_MASK_CONFIG, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
|
|
20
|
+
// Observation Masking (Phase 1 Token Optimization) + Tool Input Compaction (Phase 1b)
|
|
21
|
+
export { ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
|
|
22
|
+
// Dead Message Pruning (Phase 4 Token Optimization)
|
|
23
|
+
export { DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned } from './dead-message-pruner.js';
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* The agent can re-read from the environment if needed (files, git, etc.).
|
|
10
10
|
*/
|
|
11
11
|
import type { Message } from '../providers/types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Defines which input fields to keep when compacting a tool_use input.
|
|
14
|
+
* All other fields are removed.
|
|
15
|
+
*/
|
|
16
|
+
export interface InputCompactionRule {
|
|
17
|
+
/** Fields to preserve in the compacted input */
|
|
18
|
+
keepFields: string[];
|
|
19
|
+
}
|
|
12
20
|
export interface ObservationMaskConfig {
|
|
13
21
|
/** Turns after which tool results are masked (default: 6) */
|
|
14
22
|
maskAfterTurns: number;
|
|
@@ -18,7 +26,15 @@ export interface ObservationMaskConfig {
|
|
|
18
26
|
neverMask: string[];
|
|
19
27
|
/** Tool names to mask after just 1 turn (large reads, bash output) */
|
|
20
28
|
alwaysMaskEarly: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Tool input compaction rules (Phase 1b). Keys are tool names.
|
|
31
|
+
* After the turn threshold, large input fields are removed, keeping only the fields listed.
|
|
32
|
+
* Set to false to disable input compaction entirely.
|
|
33
|
+
* Default: compact edit (keep filePath) and write_file (keep path, mode).
|
|
34
|
+
*/
|
|
35
|
+
inputCompaction: Map<string, InputCompactionRule> | false;
|
|
21
36
|
}
|
|
37
|
+
export declare const DEFAULT_INPUT_COMPACTION: Map<string, InputCompactionRule>;
|
|
22
38
|
export declare const DEFAULT_MASK_CONFIG: ObservationMaskConfig;
|
|
23
39
|
interface TurnStamp {
|
|
24
40
|
turn: number;
|
|
@@ -39,6 +55,8 @@ export interface ObservationMaskStats {
|
|
|
39
55
|
tokensSaved: number;
|
|
40
56
|
/** Active stamps (pending masking) */
|
|
41
57
|
activeStamps: number;
|
|
58
|
+
/** Total tool_use inputs compacted this session (Phase 1b) */
|
|
59
|
+
inputsCompacted: number;
|
|
42
60
|
}
|
|
43
61
|
export declare class ObservationMasker {
|
|
44
62
|
private readonly stamps;
|
|
@@ -51,8 +69,9 @@ export declare class ObservationMasker {
|
|
|
51
69
|
*/
|
|
52
70
|
stamp(toolUseId: string, toolName: string, input: Record<string, unknown>, contentLength: number, turn: number): void;
|
|
53
71
|
/**
|
|
54
|
-
* Mask old tool results
|
|
55
|
-
*
|
|
72
|
+
* Mask old tool results and compact old tool_use inputs in-place.
|
|
73
|
+
* - tool_result: replaces content with compact mask text (Phase 1)
|
|
74
|
+
* - tool_use input: strips large fields, keeping only identifying fields (Phase 1b)
|
|
56
75
|
*/
|
|
57
76
|
maskHistory(messages: Message[], currentTurn: number): MaskResult;
|
|
58
77
|
getStats(): ObservationMaskStats;
|
|
@@ -64,6 +83,11 @@ export declare class ObservationMasker {
|
|
|
64
83
|
* Get current configuration (for testing/inspection).
|
|
65
84
|
*/
|
|
66
85
|
getConfig(): ObservationMaskConfig;
|
|
86
|
+
/**
|
|
87
|
+
* Compact a tool_use input in-place by stripping large fields.
|
|
88
|
+
* Returns estimated tokens saved (0 if no compaction performed).
|
|
89
|
+
*/
|
|
90
|
+
private compactInput;
|
|
67
91
|
}
|
|
68
92
|
/**
|
|
69
93
|
* Extract a short summary from tool input for the mask text.
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
* Strategy: In-place masking of conversationHistory after N turns.
|
|
9
9
|
* The agent can re-read from the environment if needed (files, git, etc.).
|
|
10
10
|
*/
|
|
11
|
+
export const DEFAULT_INPUT_COMPACTION = new Map([
|
|
12
|
+
['edit', { keepFields: ['filePath'] }],
|
|
13
|
+
['write_file', { keepFields: ['path', 'mode'] }],
|
|
14
|
+
]);
|
|
11
15
|
export const DEFAULT_MASK_CONFIG = {
|
|
12
16
|
maskAfterTurns: 6,
|
|
13
17
|
minCharsToMask: 400,
|
|
14
18
|
neverMask: ['recall_full_result', 'recall_work'],
|
|
15
19
|
alwaysMaskEarly: ['read_file', 'bash', 'bash_output', 'grep', 'glob'],
|
|
20
|
+
inputCompaction: new Map(DEFAULT_INPUT_COMPACTION),
|
|
16
21
|
};
|
|
17
22
|
// ============================================================
|
|
18
23
|
// ObservationMasker
|
|
@@ -20,7 +25,7 @@ export const DEFAULT_MASK_CONFIG = {
|
|
|
20
25
|
export class ObservationMasker {
|
|
21
26
|
stamps = new Map();
|
|
22
27
|
config;
|
|
23
|
-
stats = { maskedCount: 0, tokensSaved: 0 };
|
|
28
|
+
stats = { maskedCount: 0, tokensSaved: 0, inputsCompacted: 0 };
|
|
24
29
|
constructor(config) {
|
|
25
30
|
this.config = { ...DEFAULT_MASK_CONFIG, ...config };
|
|
26
31
|
}
|
|
@@ -43,43 +48,59 @@ export class ObservationMasker {
|
|
|
43
48
|
// Masking — called after incrementTurn()
|
|
44
49
|
// ----------------------------------------------------------
|
|
45
50
|
/**
|
|
46
|
-
* Mask old tool results
|
|
47
|
-
*
|
|
51
|
+
* Mask old tool results and compact old tool_use inputs in-place.
|
|
52
|
+
* - tool_result: replaces content with compact mask text (Phase 1)
|
|
53
|
+
* - tool_use input: strips large fields, keeping only identifying fields (Phase 1b)
|
|
48
54
|
*/
|
|
49
55
|
maskHistory(messages, currentTurn) {
|
|
50
56
|
let tokensSaved = 0;
|
|
51
57
|
let maskedCount = 0;
|
|
52
58
|
for (const msg of messages) {
|
|
53
|
-
if (
|
|
59
|
+
if (typeof msg.content === 'string')
|
|
54
60
|
continue;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
61
|
+
// Phase 1: Mask old tool_result content in user messages
|
|
62
|
+
if (msg.role === 'user') {
|
|
63
|
+
for (const block of msg.content) {
|
|
64
|
+
if (block.type !== 'tool_result')
|
|
65
|
+
continue;
|
|
66
|
+
if (isMasked(block.content))
|
|
67
|
+
continue;
|
|
68
|
+
const stamp = this.stamps.get(block.toolUseId);
|
|
69
|
+
if (!stamp)
|
|
70
|
+
continue;
|
|
71
|
+
if (this.config.neverMask.includes(stamp.toolName))
|
|
72
|
+
continue;
|
|
73
|
+
const age = currentTurn - stamp.turn;
|
|
74
|
+
const threshold = this.config.alwaysMaskEarly.includes(stamp.toolName)
|
|
75
|
+
? 1
|
|
76
|
+
: this.config.maskAfterTurns;
|
|
77
|
+
if (age < threshold)
|
|
78
|
+
continue;
|
|
79
|
+
if (stamp.contentLength < this.config.minCharsToMask)
|
|
80
|
+
continue;
|
|
81
|
+
// Build mask and calculate savings
|
|
82
|
+
const maskText = buildMaskText(stamp);
|
|
83
|
+
const savedChars = stamp.contentLength - maskText.length;
|
|
84
|
+
const savedTokens = Math.max(0, Math.ceil(savedChars / 4));
|
|
85
|
+
// Mask in-place
|
|
86
|
+
block.content = maskText;
|
|
87
|
+
tokensSaved += savedTokens;
|
|
88
|
+
maskedCount++;
|
|
89
|
+
// Clean up stamp
|
|
90
|
+
this.stamps.delete(block.toolUseId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Phase 1b: Compact old tool_use inputs in assistant messages
|
|
94
|
+
if (msg.role === 'assistant') {
|
|
95
|
+
for (const block of msg.content) {
|
|
96
|
+
if (block.type !== 'tool_use')
|
|
97
|
+
continue;
|
|
98
|
+
const saved = this.compactInput(block, currentTurn);
|
|
99
|
+
if (saved > 0) {
|
|
100
|
+
tokensSaved += saved;
|
|
101
|
+
this.stats.inputsCompacted++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
83
104
|
}
|
|
84
105
|
}
|
|
85
106
|
this.stats.maskedCount += maskedCount;
|
|
@@ -100,7 +121,7 @@ export class ObservationMasker {
|
|
|
100
121
|
*/
|
|
101
122
|
reset() {
|
|
102
123
|
this.stamps.clear();
|
|
103
|
-
this.stats = { maskedCount: 0, tokensSaved: 0 };
|
|
124
|
+
this.stats = { maskedCount: 0, tokensSaved: 0, inputsCompacted: 0 };
|
|
104
125
|
}
|
|
105
126
|
/**
|
|
106
127
|
* Get current configuration (for testing/inspection).
|
|
@@ -108,6 +129,47 @@ export class ObservationMasker {
|
|
|
108
129
|
getConfig() {
|
|
109
130
|
return { ...this.config };
|
|
110
131
|
}
|
|
132
|
+
// ----------------------------------------------------------
|
|
133
|
+
// Phase 1b: Tool input compaction
|
|
134
|
+
// ----------------------------------------------------------
|
|
135
|
+
/**
|
|
136
|
+
* Compact a tool_use input in-place by stripping large fields.
|
|
137
|
+
* Returns estimated tokens saved (0 if no compaction performed).
|
|
138
|
+
*/
|
|
139
|
+
compactInput(block, currentTurn) {
|
|
140
|
+
if (this.config.inputCompaction === false)
|
|
141
|
+
return 0;
|
|
142
|
+
const rule = this.config.inputCompaction.get(block.name);
|
|
143
|
+
if (!rule)
|
|
144
|
+
return 0;
|
|
145
|
+
const stamp = this.stamps.get(block.id);
|
|
146
|
+
if (!stamp)
|
|
147
|
+
return 0;
|
|
148
|
+
const age = currentTurn - stamp.turn;
|
|
149
|
+
const threshold = this.config.alwaysMaskEarly.includes(stamp.toolName)
|
|
150
|
+
? 1
|
|
151
|
+
: this.config.maskAfterTurns;
|
|
152
|
+
if (age < threshold)
|
|
153
|
+
return 0;
|
|
154
|
+
// Check if there are any fields to remove
|
|
155
|
+
const fieldKeys = Object.keys(block.input);
|
|
156
|
+
const removableKeys = fieldKeys.filter((k) => !rule.keepFields.includes(k));
|
|
157
|
+
if (removableKeys.length === 0)
|
|
158
|
+
return 0;
|
|
159
|
+
// Estimate tokens before compaction
|
|
160
|
+
const beforeChars = JSON.stringify(block.input).length;
|
|
161
|
+
// Build compacted input with only kept fields
|
|
162
|
+
const compacted = {};
|
|
163
|
+
for (const field of rule.keepFields) {
|
|
164
|
+
if (field in block.input) {
|
|
165
|
+
compacted[field] = block.input[field];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
block.input = compacted;
|
|
169
|
+
// Estimate tokens saved
|
|
170
|
+
const afterChars = JSON.stringify(compacted).length;
|
|
171
|
+
return Math.max(0, Math.ceil((beforeChars - afterChars) / 4));
|
|
172
|
+
}
|
|
111
173
|
}
|
|
112
174
|
// ============================================================
|
|
113
175
|
// Pure functions (exported for testing)
|
|
@@ -116,9 +178,9 @@ export class ObservationMasker {
|
|
|
116
178
|
* Extract a short summary from tool input for the mask text.
|
|
117
179
|
*/
|
|
118
180
|
export function extractInputSummary(toolName, input) {
|
|
119
|
-
// File operations — use path
|
|
181
|
+
// File operations — use path (supports path, file_path, and filePath conventions)
|
|
120
182
|
if (toolName === 'read_file' || toolName === 'edit' || toolName === 'write_file') {
|
|
121
|
-
const path = input.path ?? input.file_path;
|
|
183
|
+
const path = input.path ?? input.file_path ?? input.filePath;
|
|
122
184
|
if (typeof path === 'string')
|
|
123
185
|
return path;
|
|
124
186
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -39,8 +39,8 @@ export type { ToolPairingValidation } from './messages/index.js';
|
|
|
39
39
|
export { generateId, sleep, retry, truncate, withRetryGenerator, calculateBackoffDelay, DEFAULT_RETRY_CONFIG, countTokens, countMessageTokens, } from './utils/index.js';
|
|
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
|
-
export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG, compactToolResult, ObservationMasker, DEFAULT_MASK_CONFIG, extractInputSummary, buildMaskText, isMasked, } 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, ObservationMaskConfig, MaskResult, ObservationMaskStats, } from './context/index.js';
|
|
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';
|
|
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/dist/index.js
CHANGED
|
@@ -50,8 +50,10 @@ export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTr
|
|
|
50
50
|
DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG,
|
|
51
51
|
// Compact tool result formatting (Phase 2 Token Optimization)
|
|
52
52
|
compactToolResult,
|
|
53
|
-
// Observation masking (Phase 1 Token Optimization)
|
|
54
|
-
ObservationMasker, DEFAULT_MASK_CONFIG, extractInputSummary, buildMaskText, isMasked,
|
|
53
|
+
// Observation masking (Phase 1 Token Optimization) + Tool Input Compaction (Phase 1b)
|
|
54
|
+
ObservationMasker, DEFAULT_MASK_CONFIG, DEFAULT_INPUT_COMPACTION, extractInputSummary, buildMaskText, isMasked,
|
|
55
|
+
// Dead message pruning (Phase 4 Token Optimization)
|
|
56
|
+
DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned, } from './context/index.js';
|
|
55
57
|
// Skills system
|
|
56
58
|
export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
|
|
57
59
|
// State management
|