@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 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
+ }
@@ -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';
@@ -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 in-place in the messages array.
55
- * Modifies ToolResultBlock.content directly.
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 in-place in the messages array.
47
- * Modifies ToolResultBlock.content directly.
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 (msg.role !== 'user' || typeof msg.content === 'string')
59
+ if (typeof msg.content === 'string')
54
60
  continue;
55
- for (const block of msg.content) {
56
- if (block.type !== 'tool_result')
57
- continue;
58
- if (isMasked(block.content))
59
- continue;
60
- const stamp = this.stamps.get(block.toolUseId);
61
- if (!stamp)
62
- continue;
63
- if (this.config.neverMask.includes(stamp.toolName))
64
- continue;
65
- const age = currentTurn - stamp.turn;
66
- const threshold = this.config.alwaysMaskEarly.includes(stamp.toolName)
67
- ? 1
68
- : this.config.maskAfterTurns;
69
- if (age < threshold)
70
- continue;
71
- if (stamp.contentLength < this.config.minCharsToMask)
72
- continue;
73
- // Build mask and calculate savings
74
- const maskText = buildMaskText(stamp);
75
- const savedChars = stamp.contentLength - maskText.length;
76
- const savedTokens = Math.max(0, Math.ceil(savedChars / 4));
77
- // Mask in-place
78
- block.content = maskText;
79
- tokensSaved += savedTokens;
80
- maskedCount++;
81
- // Clean up stamp
82
- this.stamps.delete(block.toolUseId);
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, } from './context/index.js';
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",