@compilr-dev/agents 0.3.18 → 0.3.19

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.
@@ -1308,6 +1318,15 @@ export declare class Agent {
1308
1318
  tokensSaved: number;
1309
1319
  activeStamps: number;
1310
1320
  } | undefined;
1321
+ /**
1322
+ * Get dead message pruning statistics (errors pruned, permissions pruned, tokens saved).
1323
+ */
1324
+ getDeadMessagePruneStats(): {
1325
+ prunedCount: number;
1326
+ tokensSaved: number;
1327
+ errorsPruned: number;
1328
+ permissionsPruned: number;
1329
+ } | undefined;
1311
1330
  /**
1312
1331
  * Get current verbosity level based on context pressure
1313
1332
  */
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
  /**
@@ -830,6 +840,12 @@ export class Agent {
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
+ }
@@ -23,3 +23,5 @@ export type { DelegationConfig, StoredResult, DelegationEvent } from './delegati
23
23
  export { compactToolResult } from './result-compactor.js';
24
24
  export { ObservationMasker, DEFAULT_MASK_CONFIG, extractInputSummary, buildMaskText, isMasked, } from './observation-masker.js';
25
25
  export type { 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';
@@ -19,3 +19,5 @@ export { DEFAULT_DELEGATION_CONFIG } from './delegation-types.js';
19
19
  export { compactToolResult } from './result-compactor.js';
20
20
  // Observation Masking (Phase 1 Token Optimization)
21
21
  export { ObservationMasker, DEFAULT_MASK_CONFIG, 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';
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, 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, 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
@@ -51,7 +51,9 @@ DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DEL
51
51
  // Compact tool result formatting (Phase 2 Token Optimization)
52
52
  compactToolResult,
53
53
  // Observation masking (Phase 1 Token Optimization)
54
- ObservationMasker, DEFAULT_MASK_CONFIG, extractInputSummary, buildMaskText, isMasked, } from './context/index.js';
54
+ ObservationMasker, DEFAULT_MASK_CONFIG, 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.19",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",