@compilr-dev/agents 0.3.17 → 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 +47 -0
- package/dist/agent.js +71 -2
- 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 +5 -0
- package/dist/context/index.js +6 -0
- package/dist/context/observation-masker.d.ts +80 -0
- package/dist/context/observation-masker.js +192 -0
- package/dist/context/result-compactor.d.ts +15 -0
- package/dist/context/result-compactor.js +165 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -1
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type { DelegationConfig } from './context/delegation-types.js';
|
|
|
15
15
|
import { PermissionManager } from './permissions/manager.js';
|
|
16
16
|
import { ContextManager } from './context/manager.js';
|
|
17
17
|
import { FileAccessTracker } from './context/file-tracker.js';
|
|
18
|
+
import type { ObservationMaskConfig } from './context/observation-masker.js';
|
|
19
|
+
import type { PruneConfig } from './context/dead-message-pruner.js';
|
|
18
20
|
import { AnchorManager } from './anchors/manager.js';
|
|
19
21
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
20
22
|
import { type RetryConfig } from './utils/index.js';
|
|
@@ -278,6 +280,22 @@ export interface AgentConfig {
|
|
|
278
280
|
* Requires contextManager to be set. Default: true when contextManager is provided.
|
|
279
281
|
*/
|
|
280
282
|
autoContextManagement?: boolean;
|
|
283
|
+
/**
|
|
284
|
+
* Observation masking configuration. Masks old tool results in history to reduce tokens.
|
|
285
|
+
* Enabled by default when contextManager is provided. Set to `false` to disable.
|
|
286
|
+
*/
|
|
287
|
+
observationMask?: Partial<ObservationMaskConfig> | false;
|
|
288
|
+
/**
|
|
289
|
+
* Use compact text format for tool results in LLM messages.
|
|
290
|
+
* Strips JSON wrappers and metadata, reducing token usage.
|
|
291
|
+
* Enabled by default when contextManager is provided. Set to `false` to disable.
|
|
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;
|
|
281
299
|
/**
|
|
282
300
|
* Event handler for monitoring agent execution
|
|
283
301
|
*/
|
|
@@ -878,6 +896,18 @@ export declare class Agent {
|
|
|
878
896
|
* File restoration options for post-compaction content injection
|
|
879
897
|
*/
|
|
880
898
|
private readonly fileRestorationConfig?;
|
|
899
|
+
/**
|
|
900
|
+
* Observation masker for reducing token usage by masking old tool results
|
|
901
|
+
*/
|
|
902
|
+
private readonly observationMasker?;
|
|
903
|
+
/**
|
|
904
|
+
* Whether to use compact text format for tool results in LLM messages
|
|
905
|
+
*/
|
|
906
|
+
private readonly compactToolResults;
|
|
907
|
+
/**
|
|
908
|
+
* Dead message pruner for removing superseded errors and permission exchanges
|
|
909
|
+
*/
|
|
910
|
+
private readonly deadMessagePruner?;
|
|
881
911
|
constructor(config: AgentConfig);
|
|
882
912
|
/**
|
|
883
913
|
* Create an agent with project memory loaded from files.
|
|
@@ -1280,6 +1310,23 @@ export declare class Agent {
|
|
|
1280
1310
|
* Get context statistics
|
|
1281
1311
|
*/
|
|
1282
1312
|
getContextStats(): ContextStats | undefined;
|
|
1313
|
+
/**
|
|
1314
|
+
* Get observation masking statistics (tokens saved, observations masked).
|
|
1315
|
+
*/
|
|
1316
|
+
getObservationMaskStats(): {
|
|
1317
|
+
maskedCount: number;
|
|
1318
|
+
tokensSaved: number;
|
|
1319
|
+
activeStamps: number;
|
|
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;
|
|
1283
1330
|
/**
|
|
1284
1331
|
* Get current verbosity level based on context pressure
|
|
1285
1332
|
*/
|
package/dist/agent.js
CHANGED
|
@@ -10,6 +10,9 @@ import { ContextManager } from './context/manager.js';
|
|
|
10
10
|
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
|
+
import { ObservationMasker } from './context/observation-masker.js';
|
|
14
|
+
import { compactToolResult } from './context/result-compactor.js';
|
|
15
|
+
import { DeadMessagePruner } from './context/dead-message-pruner.js';
|
|
13
16
|
import { createRecallResultTool } from './tools/builtin/recall-result.js';
|
|
14
17
|
import { AnchorManager } from './anchors/manager.js';
|
|
15
18
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
@@ -97,6 +100,18 @@ export class Agent {
|
|
|
97
100
|
* File restoration options for post-compaction content injection
|
|
98
101
|
*/
|
|
99
102
|
fileRestorationConfig;
|
|
103
|
+
/**
|
|
104
|
+
* Observation masker for reducing token usage by masking old tool results
|
|
105
|
+
*/
|
|
106
|
+
observationMasker;
|
|
107
|
+
/**
|
|
108
|
+
* Whether to use compact text format for tool results in LLM messages
|
|
109
|
+
*/
|
|
110
|
+
compactToolResults;
|
|
111
|
+
/**
|
|
112
|
+
* Dead message pruner for removing superseded errors and permission exchanges
|
|
113
|
+
*/
|
|
114
|
+
deadMessagePruner;
|
|
100
115
|
constructor(config) {
|
|
101
116
|
this.provider = config.provider;
|
|
102
117
|
this.systemPrompt = config.systemPrompt ?? '';
|
|
@@ -112,6 +127,16 @@ export class Agent {
|
|
|
112
127
|
this.contextManager = config.contextManager;
|
|
113
128
|
this.autoContextManagement =
|
|
114
129
|
config.autoContextManagement ?? config.contextManager !== undefined;
|
|
130
|
+
// Observation masking: enabled by default when contextManager is provided, unless explicitly false
|
|
131
|
+
if (config.observationMask !== false && this.contextManager) {
|
|
132
|
+
this.observationMasker = new ObservationMasker(config.observationMask === undefined ? undefined : config.observationMask);
|
|
133
|
+
}
|
|
134
|
+
// Compact tool results: enabled by default when contextManager is provided
|
|
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
|
+
}
|
|
115
140
|
this.onEvent = config.onEvent;
|
|
116
141
|
this.onIterationLimitReached = config.onIterationLimitReached;
|
|
117
142
|
// State management
|
|
@@ -752,6 +777,8 @@ export class Agent {
|
|
|
752
777
|
clearHistory() {
|
|
753
778
|
this.conversationHistory = [];
|
|
754
779
|
this.contextManager?.reset();
|
|
780
|
+
this.observationMasker?.reset();
|
|
781
|
+
this.deadMessagePruner?.reset();
|
|
755
782
|
return this;
|
|
756
783
|
}
|
|
757
784
|
/**
|
|
@@ -807,6 +834,18 @@ export class Agent {
|
|
|
807
834
|
getContextStats() {
|
|
808
835
|
return this.contextManager?.getStats(this.conversationHistory.length);
|
|
809
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Get observation masking statistics (tokens saved, observations masked).
|
|
839
|
+
*/
|
|
840
|
+
getObservationMaskStats() {
|
|
841
|
+
return this.observationMasker?.getStats();
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Get dead message pruning statistics (errors pruned, permissions pruned, tokens saved).
|
|
845
|
+
*/
|
|
846
|
+
getDeadMessagePruneStats() {
|
|
847
|
+
return this.deadMessagePruner?.getStats();
|
|
848
|
+
}
|
|
810
849
|
/**
|
|
811
850
|
* Get current verbosity level based on context pressure
|
|
812
851
|
*/
|
|
@@ -1915,7 +1954,9 @@ export class Agent {
|
|
|
1915
1954
|
type: 'tool_result',
|
|
1916
1955
|
toolUseId: toolUse.id,
|
|
1917
1956
|
content: result.success
|
|
1918
|
-
?
|
|
1957
|
+
? this.compactToolResults
|
|
1958
|
+
? compactToolResult(toolUse.name, result.result, toolUse.input)
|
|
1959
|
+
: JSON.stringify(result.result)
|
|
1919
1960
|
: `Error: ${result.error ?? 'Unknown error'}`,
|
|
1920
1961
|
isError: !result.success,
|
|
1921
1962
|
},
|
|
@@ -1998,7 +2039,9 @@ export class Agent {
|
|
|
1998
2039
|
emit({ type: 'tool_end', name: toolUse.name, result, toolUseId: toolUse.id });
|
|
1999
2040
|
// Build tool result content
|
|
2000
2041
|
let toolResultContent = result.success
|
|
2001
|
-
?
|
|
2042
|
+
? this.compactToolResults
|
|
2043
|
+
? compactToolResult(toolUse.name, result.result, toolUse.input)
|
|
2044
|
+
: JSON.stringify(result.result)
|
|
2002
2045
|
: `Error: ${result.error ?? 'Unknown error'}`;
|
|
2003
2046
|
// Context management (only for sequential - parallel handles this after)
|
|
2004
2047
|
if (!inParallelGroup && this.contextManager && this.autoContextManagement) {
|
|
@@ -2071,6 +2114,15 @@ export class Agent {
|
|
|
2071
2114
|
iterationToolCalls.push(toolCallEntry);
|
|
2072
2115
|
messages.push(toolResultMsg);
|
|
2073
2116
|
newMessages.push(toolResultMsg);
|
|
2117
|
+
// Stamp for observation masking
|
|
2118
|
+
if (this.observationMasker) {
|
|
2119
|
+
const block = toolResultMsg.content[0];
|
|
2120
|
+
this.observationMasker.stamp(toolUse.id, toolUse.name, toolUse.input, block.content.length, this.contextManager?.getTurnCount() ?? 0);
|
|
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
|
+
}
|
|
2074
2126
|
}
|
|
2075
2127
|
}
|
|
2076
2128
|
else {
|
|
@@ -2105,6 +2157,15 @@ export class Agent {
|
|
|
2105
2157
|
iterationToolCalls.push(toolCallEntry);
|
|
2106
2158
|
messages.push(toolResultMsg);
|
|
2107
2159
|
newMessages.push(toolResultMsg);
|
|
2160
|
+
// Stamp for observation masking
|
|
2161
|
+
if (this.observationMasker) {
|
|
2162
|
+
const block = toolResultMsg.content[0];
|
|
2163
|
+
this.observationMasker.stamp(toolUse.id, toolUse.name, toolUse.input, block.content.length, this.contextManager?.getTurnCount() ?? 0);
|
|
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
|
+
}
|
|
2108
2169
|
if (skipped) {
|
|
2109
2170
|
continue;
|
|
2110
2171
|
}
|
|
@@ -2223,6 +2284,14 @@ export class Agent {
|
|
|
2223
2284
|
// Context management: increment turn count and update token count
|
|
2224
2285
|
if (this.contextManager) {
|
|
2225
2286
|
this.contextManager.incrementTurn();
|
|
2287
|
+
// Observation masking: mask old tool results in-place before token update
|
|
2288
|
+
if (this.observationMasker) {
|
|
2289
|
+
this.observationMasker.maskHistory(messages, this.contextManager.getTurnCount());
|
|
2290
|
+
}
|
|
2291
|
+
// Dead message pruning: prune superseded errors and permission exchanges
|
|
2292
|
+
if (this.deadMessagePruner) {
|
|
2293
|
+
this.deadMessagePruner.prune(messages, this.contextManager.getTurnCount());
|
|
2294
|
+
}
|
|
2226
2295
|
await this.contextManager.updateTokenCount(messages);
|
|
2227
2296
|
}
|
|
2228
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
|
@@ -20,3 +20,8 @@ export { ToolResultDelegator, DELEGATION_SYSTEM_PROMPT } from './tool-result-del
|
|
|
20
20
|
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
|
+
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';
|
|
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
|
@@ -15,3 +15,9 @@ export { createFileTrackingHook, TRACKED_TOOLS } from './file-tracking-hook.js';
|
|
|
15
15
|
export { DelegatedResultStore } from './delegated-result-store.js';
|
|
16
16
|
export { ToolResultDelegator, DELEGATION_SYSTEM_PROMPT } from './tool-result-delegator.js';
|
|
17
17
|
export { DEFAULT_DELEGATION_CONFIG } from './delegation-types.js';
|
|
18
|
+
// Compact Tool Result Formatting (Phase 2 Token Optimization)
|
|
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';
|
|
22
|
+
// Dead Message Pruning (Phase 4 Token Optimization)
|
|
23
|
+
export { DeadMessagePruner, DEFAULT_PRUNE_CONFIG, isPruned } from './dead-message-pruner.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observation Masker — Phase 1 of Advanced Token Optimization
|
|
3
|
+
*
|
|
4
|
+
* Masks old tool results (observations) in conversation history to reduce token usage.
|
|
5
|
+
* Based on JetBrains Research (Dec 2025): observation masking outperforms LLM summarization,
|
|
6
|
+
* achieving 52% cost reduction with +2.6% higher solve rates.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: In-place masking of conversationHistory after N turns.
|
|
9
|
+
* The agent can re-read from the environment if needed (files, git, etc.).
|
|
10
|
+
*/
|
|
11
|
+
import type { Message } from '../providers/types.js';
|
|
12
|
+
export interface ObservationMaskConfig {
|
|
13
|
+
/** Turns after which tool results are masked (default: 6) */
|
|
14
|
+
maskAfterTurns: number;
|
|
15
|
+
/** Minimum content length (chars) to mask — skip tiny results (default: 400 ≈ 100 tokens) */
|
|
16
|
+
minCharsToMask: number;
|
|
17
|
+
/** Tool names to NEVER mask (e.g., recall tools, state queries) */
|
|
18
|
+
neverMask: string[];
|
|
19
|
+
/** Tool names to mask after just 1 turn (large reads, bash output) */
|
|
20
|
+
alwaysMaskEarly: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare const DEFAULT_MASK_CONFIG: ObservationMaskConfig;
|
|
23
|
+
interface TurnStamp {
|
|
24
|
+
turn: number;
|
|
25
|
+
toolName: string;
|
|
26
|
+
/** Short summary of input for mask text (e.g., file path, command) */
|
|
27
|
+
inputSummary: string;
|
|
28
|
+
/** Original content length in chars */
|
|
29
|
+
contentLength: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MaskResult {
|
|
32
|
+
maskedCount: number;
|
|
33
|
+
tokensSaved: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ObservationMaskStats {
|
|
36
|
+
/** Total observations masked this session */
|
|
37
|
+
maskedCount: number;
|
|
38
|
+
/** Estimated tokens saved this session */
|
|
39
|
+
tokensSaved: number;
|
|
40
|
+
/** Active stamps (pending masking) */
|
|
41
|
+
activeStamps: number;
|
|
42
|
+
}
|
|
43
|
+
export declare class ObservationMasker {
|
|
44
|
+
private readonly stamps;
|
|
45
|
+
private readonly config;
|
|
46
|
+
private stats;
|
|
47
|
+
constructor(config?: Partial<ObservationMaskConfig>);
|
|
48
|
+
/**
|
|
49
|
+
* Register a tool result with its turn number and input context.
|
|
50
|
+
* Called immediately after a tool result is added to the messages array.
|
|
51
|
+
*/
|
|
52
|
+
stamp(toolUseId: string, toolName: string, input: Record<string, unknown>, contentLength: number, turn: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Mask old tool results in-place in the messages array.
|
|
55
|
+
* Modifies ToolResultBlock.content directly.
|
|
56
|
+
*/
|
|
57
|
+
maskHistory(messages: Message[], currentTurn: number): MaskResult;
|
|
58
|
+
getStats(): ObservationMaskStats;
|
|
59
|
+
/**
|
|
60
|
+
* Reset all state (stamps and stats). Used when clearing history.
|
|
61
|
+
*/
|
|
62
|
+
reset(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Get current configuration (for testing/inspection).
|
|
65
|
+
*/
|
|
66
|
+
getConfig(): ObservationMaskConfig;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Extract a short summary from tool input for the mask text.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractInputSummary(toolName: string, input: Record<string, unknown>): string;
|
|
72
|
+
/**
|
|
73
|
+
* Build the compact mask text that replaces the original content.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildMaskText(stamp: TurnStamp): string;
|
|
76
|
+
/**
|
|
77
|
+
* Check if a tool result content string is already masked.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isMasked(content: string): boolean;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observation Masker — Phase 1 of Advanced Token Optimization
|
|
3
|
+
*
|
|
4
|
+
* Masks old tool results (observations) in conversation history to reduce token usage.
|
|
5
|
+
* Based on JetBrains Research (Dec 2025): observation masking outperforms LLM summarization,
|
|
6
|
+
* achieving 52% cost reduction with +2.6% higher solve rates.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: In-place masking of conversationHistory after N turns.
|
|
9
|
+
* The agent can re-read from the environment if needed (files, git, etc.).
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MASK_CONFIG = {
|
|
12
|
+
maskAfterTurns: 6,
|
|
13
|
+
minCharsToMask: 400,
|
|
14
|
+
neverMask: ['recall_full_result', 'recall_work'],
|
|
15
|
+
alwaysMaskEarly: ['read_file', 'bash', 'bash_output', 'grep', 'glob'],
|
|
16
|
+
};
|
|
17
|
+
// ============================================================
|
|
18
|
+
// ObservationMasker
|
|
19
|
+
// ============================================================
|
|
20
|
+
export class ObservationMasker {
|
|
21
|
+
stamps = new Map();
|
|
22
|
+
config;
|
|
23
|
+
stats = { maskedCount: 0, tokensSaved: 0 };
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = { ...DEFAULT_MASK_CONFIG, ...config };
|
|
26
|
+
}
|
|
27
|
+
// ----------------------------------------------------------
|
|
28
|
+
// Stamping — called when tool results are added to history
|
|
29
|
+
// ----------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Register a tool result with its turn number and input context.
|
|
32
|
+
* Called immediately after a tool result is added to the messages array.
|
|
33
|
+
*/
|
|
34
|
+
stamp(toolUseId, toolName, input, contentLength, turn) {
|
|
35
|
+
this.stamps.set(toolUseId, {
|
|
36
|
+
turn,
|
|
37
|
+
toolName,
|
|
38
|
+
inputSummary: extractInputSummary(toolName, input),
|
|
39
|
+
contentLength,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// ----------------------------------------------------------
|
|
43
|
+
// Masking — called after incrementTurn()
|
|
44
|
+
// ----------------------------------------------------------
|
|
45
|
+
/**
|
|
46
|
+
* Mask old tool results in-place in the messages array.
|
|
47
|
+
* Modifies ToolResultBlock.content directly.
|
|
48
|
+
*/
|
|
49
|
+
maskHistory(messages, currentTurn) {
|
|
50
|
+
let tokensSaved = 0;
|
|
51
|
+
let maskedCount = 0;
|
|
52
|
+
for (const msg of messages) {
|
|
53
|
+
if (msg.role !== 'user' || typeof msg.content === 'string')
|
|
54
|
+
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);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.stats.maskedCount += maskedCount;
|
|
86
|
+
this.stats.tokensSaved += tokensSaved;
|
|
87
|
+
return { maskedCount, tokensSaved };
|
|
88
|
+
}
|
|
89
|
+
// ----------------------------------------------------------
|
|
90
|
+
// Stats
|
|
91
|
+
// ----------------------------------------------------------
|
|
92
|
+
getStats() {
|
|
93
|
+
return {
|
|
94
|
+
...this.stats,
|
|
95
|
+
activeStamps: this.stamps.size,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Reset all state (stamps and stats). Used when clearing history.
|
|
100
|
+
*/
|
|
101
|
+
reset() {
|
|
102
|
+
this.stamps.clear();
|
|
103
|
+
this.stats = { maskedCount: 0, tokensSaved: 0 };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get current configuration (for testing/inspection).
|
|
107
|
+
*/
|
|
108
|
+
getConfig() {
|
|
109
|
+
return { ...this.config };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ============================================================
|
|
113
|
+
// Pure functions (exported for testing)
|
|
114
|
+
// ============================================================
|
|
115
|
+
/**
|
|
116
|
+
* Extract a short summary from tool input for the mask text.
|
|
117
|
+
*/
|
|
118
|
+
export function extractInputSummary(toolName, input) {
|
|
119
|
+
// File operations — use path
|
|
120
|
+
if (toolName === 'read_file' || toolName === 'edit' || toolName === 'write_file') {
|
|
121
|
+
const path = input.path ?? input.file_path;
|
|
122
|
+
if (typeof path === 'string')
|
|
123
|
+
return path;
|
|
124
|
+
}
|
|
125
|
+
// Shell commands — use command (truncated)
|
|
126
|
+
if (toolName === 'bash' || toolName === 'bash_output') {
|
|
127
|
+
const cmd = input.command;
|
|
128
|
+
if (typeof cmd === 'string') {
|
|
129
|
+
return cmd.length > 40 ? cmd.slice(0, 40) + '...' : cmd;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Search — use pattern
|
|
133
|
+
if (toolName === 'grep') {
|
|
134
|
+
const pattern = input.pattern;
|
|
135
|
+
if (typeof pattern === 'string')
|
|
136
|
+
return `"${pattern}"`;
|
|
137
|
+
}
|
|
138
|
+
// Glob — use pattern
|
|
139
|
+
if (toolName === 'glob') {
|
|
140
|
+
const pattern = input.pattern;
|
|
141
|
+
if (typeof pattern === 'string')
|
|
142
|
+
return pattern;
|
|
143
|
+
}
|
|
144
|
+
// Git tools — use subcommand or operation
|
|
145
|
+
if (toolName.startsWith('git_')) {
|
|
146
|
+
return toolName.slice(4); // "git_diff" → "diff"
|
|
147
|
+
}
|
|
148
|
+
// Web fetch — use URL
|
|
149
|
+
if (toolName === 'web_fetch') {
|
|
150
|
+
const url = input.url;
|
|
151
|
+
if (typeof url === 'string') {
|
|
152
|
+
return url.length > 60 ? url.slice(0, 60) + '...' : url;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Default — just the tool name
|
|
156
|
+
return toolName;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Build the compact mask text that replaces the original content.
|
|
160
|
+
*/
|
|
161
|
+
export function buildMaskText(stamp) {
|
|
162
|
+
const { toolName, inputSummary, turn, contentLength } = stamp;
|
|
163
|
+
const lines = String(Math.ceil(contentLength / 80));
|
|
164
|
+
const t = String(turn);
|
|
165
|
+
if (toolName === 'read_file') {
|
|
166
|
+
return `[file:${inputSummary} ~${lines}L read@turn:${t}]`;
|
|
167
|
+
}
|
|
168
|
+
if (toolName === 'bash' || toolName === 'bash_output') {
|
|
169
|
+
return `[cmd:${inputSummary} ~${lines}L@turn:${t}]`;
|
|
170
|
+
}
|
|
171
|
+
if (toolName === 'grep') {
|
|
172
|
+
return `[search:${inputSummary}@turn:${t}]`;
|
|
173
|
+
}
|
|
174
|
+
if (toolName === 'glob') {
|
|
175
|
+
return `[glob:${inputSummary}@turn:${t}]`;
|
|
176
|
+
}
|
|
177
|
+
if (toolName.startsWith('git_')) {
|
|
178
|
+
return `[git:${inputSummary}@turn:${t}]`;
|
|
179
|
+
}
|
|
180
|
+
if (toolName === 'edit' || toolName === 'write_file') {
|
|
181
|
+
return `[write:${inputSummary}@turn:${t}]`;
|
|
182
|
+
}
|
|
183
|
+
// Generic
|
|
184
|
+
const tokens = String(Math.ceil(contentLength / 4));
|
|
185
|
+
return `[tool:${toolName} ${tokens}tok@turn:${t}]`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if a tool result content string is already masked.
|
|
189
|
+
*/
|
|
190
|
+
export function isMasked(content) {
|
|
191
|
+
return content.startsWith('[') && content.endsWith(']') && content.includes('@turn:');
|
|
192
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact Tool Result Formatting — Phase 2 of Advanced Token Optimization
|
|
3
|
+
*
|
|
4
|
+
* Replaces JSON.stringify for tool results in the LLM messages array.
|
|
5
|
+
* Per-tool formatters strip JSON overhead (escaped newlines, metadata fields)
|
|
6
|
+
* and produce a text format the LLM can parse just as well.
|
|
7
|
+
*
|
|
8
|
+
* Only affects what the LLM reads in its message history.
|
|
9
|
+
* CLI formatters, events, and hooks still receive the raw ToolExecutionResult.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Format a tool result as compact text for LLM context.
|
|
13
|
+
* Falls back to JSON.stringify for unknown tools or non-object results.
|
|
14
|
+
*/
|
|
15
|
+
export declare function compactToolResult(toolName: string, result: unknown, input?: Record<string, unknown>): string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact Tool Result Formatting — Phase 2 of Advanced Token Optimization
|
|
3
|
+
*
|
|
4
|
+
* Replaces JSON.stringify for tool results in the LLM messages array.
|
|
5
|
+
* Per-tool formatters strip JSON overhead (escaped newlines, metadata fields)
|
|
6
|
+
* and produce a text format the LLM can parse just as well.
|
|
7
|
+
*
|
|
8
|
+
* Only affects what the LLM reads in its message history.
|
|
9
|
+
* CLI formatters, events, and hooks still receive the raw ToolExecutionResult.
|
|
10
|
+
*/
|
|
11
|
+
// ============================================================
|
|
12
|
+
// Helpers
|
|
13
|
+
// ============================================================
|
|
14
|
+
/** Safely extract a string field from an unknown record. */
|
|
15
|
+
function str(value, fallback = '') {
|
|
16
|
+
return typeof value === 'string' ? value : fallback;
|
|
17
|
+
}
|
|
18
|
+
/** Safely extract a number field from an unknown record. */
|
|
19
|
+
function num(value, fallback = 0) {
|
|
20
|
+
return typeof value === 'number' ? value : fallback;
|
|
21
|
+
}
|
|
22
|
+
// ============================================================
|
|
23
|
+
// Public API
|
|
24
|
+
// ============================================================
|
|
25
|
+
/**
|
|
26
|
+
* Format a tool result as compact text for LLM context.
|
|
27
|
+
* Falls back to JSON.stringify for unknown tools or non-object results.
|
|
28
|
+
*/
|
|
29
|
+
export function compactToolResult(toolName, result, input) {
|
|
30
|
+
// String results — already compact
|
|
31
|
+
if (typeof result === 'string')
|
|
32
|
+
return result;
|
|
33
|
+
if (result === null || result === undefined)
|
|
34
|
+
return '';
|
|
35
|
+
// Per-tool formatters
|
|
36
|
+
const formatter = TOOL_FORMATTERS.get(toolName);
|
|
37
|
+
if (formatter)
|
|
38
|
+
return formatter(result, input);
|
|
39
|
+
// Fallback: JSON.stringify (current behavior)
|
|
40
|
+
return JSON.stringify(result);
|
|
41
|
+
}
|
|
42
|
+
const TOOL_FORMATTERS = new Map([
|
|
43
|
+
['read_file', formatReadFile],
|
|
44
|
+
['bash', formatBash],
|
|
45
|
+
['bash_output', formatBash],
|
|
46
|
+
['grep', formatGrep],
|
|
47
|
+
['glob', formatGlob],
|
|
48
|
+
['edit', formatEdit],
|
|
49
|
+
['write_file', formatWriteFile],
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* read_file → [path 200L] + content
|
|
53
|
+
*/
|
|
54
|
+
function formatReadFile(r) {
|
|
55
|
+
const path = str(r.path);
|
|
56
|
+
const content = str(r.content);
|
|
57
|
+
const totalLines = typeof r.totalLines === 'number' ? r.totalLines : undefined;
|
|
58
|
+
const linesReturned = typeof r.linesReturned === 'number' ? r.linesReturned : undefined;
|
|
59
|
+
const truncated = r.truncated === true;
|
|
60
|
+
// Header: [path NL] or [path M/NL] if truncated
|
|
61
|
+
let header;
|
|
62
|
+
if (truncated && linesReturned !== undefined && totalLines !== undefined) {
|
|
63
|
+
header = `[${path} ${String(linesReturned)}/${String(totalLines)}L]`;
|
|
64
|
+
}
|
|
65
|
+
else if (totalLines !== undefined) {
|
|
66
|
+
header = `[${path} ${String(totalLines)}L]`;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
header = `[${path}]`;
|
|
70
|
+
}
|
|
71
|
+
return `${header}\n${content}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* bash → $ command\nstdout\n[stderr]\nstderr
|
|
75
|
+
* Non-zero exit: [exit:N] $ command\n...
|
|
76
|
+
* Background: shell_id message
|
|
77
|
+
*/
|
|
78
|
+
function formatBash(r, input) {
|
|
79
|
+
// Background execution has different shape
|
|
80
|
+
if (r.shell_id) {
|
|
81
|
+
return `[bg:${str(r.shell_id)}] ${str(r.message)}`;
|
|
82
|
+
}
|
|
83
|
+
const stdout = str(r.stdout);
|
|
84
|
+
const stderr = str(r.stderr);
|
|
85
|
+
const exitCode = num(r.exitCode);
|
|
86
|
+
const command = input ? str(input.command) : '';
|
|
87
|
+
const parts = [];
|
|
88
|
+
// Command line
|
|
89
|
+
if (command) {
|
|
90
|
+
const prefix = exitCode !== 0 ? `[exit:${String(exitCode)}] ` : '';
|
|
91
|
+
parts.push(`${prefix}$ ${command}`);
|
|
92
|
+
}
|
|
93
|
+
else if (exitCode !== 0) {
|
|
94
|
+
parts.push(`[exit:${String(exitCode)}]`);
|
|
95
|
+
}
|
|
96
|
+
// stdout
|
|
97
|
+
if (stdout) {
|
|
98
|
+
parts.push(stdout);
|
|
99
|
+
}
|
|
100
|
+
// stderr (only if non-empty)
|
|
101
|
+
if (stderr) {
|
|
102
|
+
parts.push('[stderr]');
|
|
103
|
+
parts.push(stderr);
|
|
104
|
+
}
|
|
105
|
+
return parts.join('\n');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* grep → N matches in M files:\n...matches
|
|
109
|
+
* Files-only mode → N files matching:\n...files
|
|
110
|
+
*/
|
|
111
|
+
function formatGrep(r) {
|
|
112
|
+
const count = num(r.count);
|
|
113
|
+
// Files-only mode (has `files` array)
|
|
114
|
+
if (Array.isArray(r.files)) {
|
|
115
|
+
const files = r.files;
|
|
116
|
+
if (files.length === 0)
|
|
117
|
+
return '0 files matching';
|
|
118
|
+
return `${String(count)} files matching:\n${files.join('\n')}`;
|
|
119
|
+
}
|
|
120
|
+
// Matches mode
|
|
121
|
+
const matches = Array.isArray(r.matches) ? r.matches : undefined;
|
|
122
|
+
const filesSearched = typeof r.filesSearched === 'number' ? r.filesSearched : undefined;
|
|
123
|
+
if (!matches || matches.length === 0) {
|
|
124
|
+
return filesSearched ? `0 matches in ${String(filesSearched)} files` : '0 matches';
|
|
125
|
+
}
|
|
126
|
+
const summary = filesSearched
|
|
127
|
+
? `${String(count)} matches in ${String(filesSearched)} files:`
|
|
128
|
+
: `${String(count)} matches:`;
|
|
129
|
+
return `${summary}\n${matches.join('\n')}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* glob → N files:\n...files
|
|
133
|
+
*/
|
|
134
|
+
function formatGlob(r) {
|
|
135
|
+
const files = Array.isArray(r.files) ? r.files : undefined;
|
|
136
|
+
const count = num(r.count);
|
|
137
|
+
if (!files || files.length === 0)
|
|
138
|
+
return '0 files';
|
|
139
|
+
return `${String(count)} files:\n${files.join('\n')}`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* edit → OK path (N replacements)
|
|
143
|
+
*/
|
|
144
|
+
function formatEdit(r) {
|
|
145
|
+
const filePath = str(r.filePath);
|
|
146
|
+
const replacements = num(r.replacements);
|
|
147
|
+
const created = r.created === true;
|
|
148
|
+
if (created) {
|
|
149
|
+
return `OK created ${filePath}`;
|
|
150
|
+
}
|
|
151
|
+
return `OK ${filePath} (${String(replacements)} replacement${replacements !== 1 ? 's' : ''})`;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* write_file → OK wrote path (NB)
|
|
155
|
+
*/
|
|
156
|
+
function formatWriteFile(r) {
|
|
157
|
+
const path = str(r.path);
|
|
158
|
+
const bytesWritten = typeof r.bytesWritten === 'number' ? r.bytesWritten : undefined;
|
|
159
|
+
const mode = str(r.mode);
|
|
160
|
+
const action = mode === 'appended' ? 'appended' : 'wrote';
|
|
161
|
+
if (bytesWritten !== undefined) {
|
|
162
|
+
return `OK ${action} ${path} (${String(bytesWritten)}B)`;
|
|
163
|
+
}
|
|
164
|
+
return `OK ${action} ${path}`;
|
|
165
|
+
}
|
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, } 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, } 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
|
@@ -47,7 +47,13 @@ export { AgentError, ProviderError, ToolError, ToolTimeoutError, ToolLoopError,
|
|
|
47
47
|
// Context management
|
|
48
48
|
export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS,
|
|
49
49
|
// Tool result delegation
|
|
50
|
-
DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG,
|
|
50
|
+
DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG,
|
|
51
|
+
// Compact tool result formatting (Phase 2 Token Optimization)
|
|
52
|
+
compactToolResult,
|
|
53
|
+
// Observation masking (Phase 1 Token Optimization)
|
|
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';
|
|
51
57
|
// Skills system
|
|
52
58
|
export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
|
|
53
59
|
// State management
|