@compilr-dev/agents 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +188 -1
- package/dist/agent.js +284 -14
- package/dist/context/file-tracker.d.ts +156 -0
- package/dist/context/file-tracker.js +358 -0
- package/dist/context/file-tracking-hook.d.ts +29 -0
- package/dist/context/file-tracking-hook.js +103 -0
- package/dist/context/index.d.ts +5 -1
- package/dist/context/index.js +3 -0
- package/dist/context/manager.d.ts +69 -1
- package/dist/context/manager.js +304 -0
- package/dist/context/types.d.ts +95 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.js +11 -3
- package/dist/messages/index.d.ts +13 -0
- package/dist/messages/index.js +51 -0
- package/dist/permissions/manager.js +6 -1
- package/dist/providers/gemini.d.ts +91 -0
- package/dist/providers/gemini.js +138 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +7 -3
- package/dist/providers/mock.js +8 -0
- package/dist/providers/ollama.d.ts +87 -0
- package/dist/providers/ollama.js +133 -0
- package/dist/providers/openai-compatible.d.ts +182 -0
- package/dist/providers/openai-compatible.js +357 -0
- package/dist/providers/openai.d.ts +93 -0
- package/dist/providers/openai.js +133 -0
- package/dist/skills/index.js +691 -0
- package/dist/tools/builtin/glob.d.ts +11 -0
- package/dist/tools/builtin/glob.js +44 -2
- package/dist/tools/builtin/grep.d.ts +11 -1
- package/dist/tools/builtin/grep.js +38 -2
- package/dist/tools/builtin/index.d.ts +6 -1
- package/dist/tools/builtin/index.js +7 -0
- package/dist/tools/builtin/suggest.d.ts +57 -0
- package/dist/tools/builtin/suggest.js +99 -0
- package/dist/tools/builtin/task.js +13 -8
- package/dist/tools/builtin/tool-names.d.ts +44 -0
- package/dist/tools/builtin/tool-names.js +51 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +5 -1
- package/dist/tools/registry.d.ts +4 -0
- package/dist/tools/registry.js +9 -0
- package/package.json +2 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Access Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks file accesses during agent sessions to provide context restoration
|
|
5
|
+
* hints after compaction. Inspired by Claude Code's post-compaction file
|
|
6
|
+
* reference display.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Track read, referenced, and modified files
|
|
10
|
+
* - Deduplication (read supersedes reference)
|
|
11
|
+
* - Max entries with LRU eviction
|
|
12
|
+
* - Verbosity-aware formatting
|
|
13
|
+
*
|
|
14
|
+
* @see /workspace/project-docs/00-requirements/compilr-dev-agents/context-restoration-hints.md
|
|
15
|
+
*/
|
|
16
|
+
import path from 'path';
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// FileAccessTracker Class
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Tracks file accesses during agent sessions for context restoration hints.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const tracker = new FileAccessTracker();
|
|
26
|
+
*
|
|
27
|
+
* // Track file accesses
|
|
28
|
+
* tracker.trackRead('/path/to/file.ts', 597);
|
|
29
|
+
* tracker.trackReference('/path/to/other.ts');
|
|
30
|
+
* tracker.trackModification('/path/to/file.ts', 'Added function');
|
|
31
|
+
*
|
|
32
|
+
* // Get restoration hints after compaction
|
|
33
|
+
* const hints = tracker.formatRestorationHints();
|
|
34
|
+
* console.log(hints);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class FileAccessTracker {
|
|
38
|
+
accesses = new Map();
|
|
39
|
+
maxEntries;
|
|
40
|
+
deduplicateReferences;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.maxEntries = options.maxEntries ?? 100;
|
|
43
|
+
this.deduplicateReferences = options.deduplicateReferences ?? true;
|
|
44
|
+
}
|
|
45
|
+
// ==========================================================================
|
|
46
|
+
// Track Methods
|
|
47
|
+
// ==========================================================================
|
|
48
|
+
/**
|
|
49
|
+
* Track a file that was fully read
|
|
50
|
+
*/
|
|
51
|
+
trackRead(filePath, lineCount, summary) {
|
|
52
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
53
|
+
// Read supersedes reference - remove any existing reference
|
|
54
|
+
const existing = this.accesses.get(normalizedPath);
|
|
55
|
+
if (existing && existing.type === 'referenced') {
|
|
56
|
+
this.accesses.delete(normalizedPath);
|
|
57
|
+
}
|
|
58
|
+
// Update or create read entry
|
|
59
|
+
this.accesses.set(normalizedPath, {
|
|
60
|
+
path: normalizedPath,
|
|
61
|
+
type: 'read',
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
lineCount,
|
|
64
|
+
summary,
|
|
65
|
+
});
|
|
66
|
+
this.enforceMaxEntries();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Track a file that was referenced (e.g., appeared in grep/glob results)
|
|
70
|
+
*/
|
|
71
|
+
trackReference(filePath) {
|
|
72
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
73
|
+
// Don't downgrade read/modified to reference
|
|
74
|
+
const existing = this.accesses.get(normalizedPath);
|
|
75
|
+
if (existing && (existing.type === 'read' || existing.type === 'modified')) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Deduplicate references
|
|
79
|
+
if (this.deduplicateReferences && existing && existing.type === 'referenced') {
|
|
80
|
+
// Update timestamp only
|
|
81
|
+
existing.timestamp = Date.now();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.accesses.set(normalizedPath, {
|
|
85
|
+
path: normalizedPath,
|
|
86
|
+
type: 'referenced',
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
this.enforceMaxEntries();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Track a file that was modified (written or edited)
|
|
93
|
+
*/
|
|
94
|
+
trackModification(filePath, summary) {
|
|
95
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
96
|
+
this.accesses.set(normalizedPath, {
|
|
97
|
+
path: normalizedPath,
|
|
98
|
+
type: 'modified',
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
summary,
|
|
101
|
+
});
|
|
102
|
+
this.enforceMaxEntries();
|
|
103
|
+
}
|
|
104
|
+
// ==========================================================================
|
|
105
|
+
// Query Methods
|
|
106
|
+
// ==========================================================================
|
|
107
|
+
/**
|
|
108
|
+
* Get all file accesses, optionally filtered
|
|
109
|
+
*/
|
|
110
|
+
getAccesses(options) {
|
|
111
|
+
let accesses = Array.from(this.accesses.values());
|
|
112
|
+
if (options?.type) {
|
|
113
|
+
accesses = accesses.filter((a) => a.type === options.type);
|
|
114
|
+
}
|
|
115
|
+
if (options?.since !== undefined) {
|
|
116
|
+
const since = options.since;
|
|
117
|
+
accesses = accesses.filter((a) => a.timestamp >= since);
|
|
118
|
+
}
|
|
119
|
+
// Sort by timestamp descending (most recent first)
|
|
120
|
+
return accesses.sort((a, b) => b.timestamp - a.timestamp);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get most recent file accesses
|
|
124
|
+
*/
|
|
125
|
+
getRecentAccesses(limit = 10) {
|
|
126
|
+
return this.getAccesses().slice(0, limit);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if a file has been accessed
|
|
130
|
+
*/
|
|
131
|
+
hasAccessed(filePath) {
|
|
132
|
+
return this.accesses.has(this.normalizePath(filePath));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get access record for a specific file
|
|
136
|
+
*/
|
|
137
|
+
getAccess(filePath) {
|
|
138
|
+
return this.accesses.get(this.normalizePath(filePath));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get statistics about file accesses
|
|
142
|
+
*/
|
|
143
|
+
getStats() {
|
|
144
|
+
const stats = { read: 0, referenced: 0, modified: 0, total: 0 };
|
|
145
|
+
for (const access of this.accesses.values()) {
|
|
146
|
+
stats[access.type]++;
|
|
147
|
+
stats.total++;
|
|
148
|
+
}
|
|
149
|
+
return stats;
|
|
150
|
+
}
|
|
151
|
+
// ==========================================================================
|
|
152
|
+
// Format Methods
|
|
153
|
+
// ==========================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Format restoration hints for injection after compaction
|
|
156
|
+
*/
|
|
157
|
+
formatRestorationHints(options = {}) {
|
|
158
|
+
const { includeLineCount = true, includeSummary = false, includeTimestamp = false, groupByType = true, maxFiles = 20, verbosityLevel = 'normal', } = options;
|
|
159
|
+
// Adjust based on verbosity
|
|
160
|
+
const effectiveMaxFiles = this.getEffectiveMaxFiles(maxFiles, verbosityLevel);
|
|
161
|
+
if (this.accesses.size === 0) {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
const allAccesses = this.getAccesses();
|
|
165
|
+
// Apply limit
|
|
166
|
+
const limitedAccesses = allAccesses.slice(0, effectiveMaxFiles);
|
|
167
|
+
const hasMore = allAccesses.length > effectiveMaxFiles;
|
|
168
|
+
// Use compact format for tight context
|
|
169
|
+
if (verbosityLevel === 'minimal' || verbosityLevel === 'abbreviated') {
|
|
170
|
+
return this.formatCompact(limitedAccesses, hasMore);
|
|
171
|
+
}
|
|
172
|
+
// Full format with grouping
|
|
173
|
+
if (groupByType) {
|
|
174
|
+
return this.formatGrouped(limitedAccesses, hasMore, {
|
|
175
|
+
includeLineCount,
|
|
176
|
+
includeSummary,
|
|
177
|
+
includeTimestamp,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Flat format (not grouped)
|
|
181
|
+
return this.formatFlat(limitedAccesses, hasMore, {
|
|
182
|
+
includeLineCount,
|
|
183
|
+
includeSummary,
|
|
184
|
+
includeTimestamp,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// ==========================================================================
|
|
188
|
+
// Lifecycle Methods
|
|
189
|
+
// ==========================================================================
|
|
190
|
+
/**
|
|
191
|
+
* Clear all tracked accesses
|
|
192
|
+
*/
|
|
193
|
+
clear() {
|
|
194
|
+
this.accesses.clear();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the number of tracked files
|
|
198
|
+
*/
|
|
199
|
+
get size() {
|
|
200
|
+
return this.accesses.size;
|
|
201
|
+
}
|
|
202
|
+
// ==========================================================================
|
|
203
|
+
// Private Helpers
|
|
204
|
+
// ==========================================================================
|
|
205
|
+
normalizePath(filePath) {
|
|
206
|
+
// Resolve to absolute path
|
|
207
|
+
return path.resolve(filePath);
|
|
208
|
+
}
|
|
209
|
+
enforceMaxEntries() {
|
|
210
|
+
if (this.accesses.size <= this.maxEntries) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Evict oldest references first, then oldest reads
|
|
214
|
+
const sorted = this.getAccesses().reverse(); // Oldest first
|
|
215
|
+
// Separate by type for eviction priority
|
|
216
|
+
const references = sorted.filter((a) => a.type === 'referenced');
|
|
217
|
+
const reads = sorted.filter((a) => a.type === 'read');
|
|
218
|
+
const modified = sorted.filter((a) => a.type === 'modified');
|
|
219
|
+
// Build eviction order: references first, then reads, modified last
|
|
220
|
+
const evictionOrder = [...references, ...reads, ...modified];
|
|
221
|
+
// Evict until under limit
|
|
222
|
+
while (this.accesses.size > this.maxEntries && evictionOrder.length > 0) {
|
|
223
|
+
const toEvict = evictionOrder.shift();
|
|
224
|
+
if (toEvict) {
|
|
225
|
+
this.accesses.delete(toEvict.path);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
getEffectiveMaxFiles(maxFiles, verbosityLevel) {
|
|
230
|
+
switch (verbosityLevel) {
|
|
231
|
+
case 'minimal':
|
|
232
|
+
return Math.min(maxFiles, 3);
|
|
233
|
+
case 'abbreviated':
|
|
234
|
+
return Math.min(maxFiles, 5);
|
|
235
|
+
case 'normal':
|
|
236
|
+
return Math.min(maxFiles, 10);
|
|
237
|
+
case 'full':
|
|
238
|
+
default:
|
|
239
|
+
return maxFiles;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
formatCompact(accesses, hasMore) {
|
|
243
|
+
const byType = this.groupByType(accesses);
|
|
244
|
+
const parts = [];
|
|
245
|
+
if (byType.read.length > 0) {
|
|
246
|
+
const names = byType.read.map((a) => this.getDisplayName(a.path)).join(', ');
|
|
247
|
+
parts.push(`${names} (read)`);
|
|
248
|
+
}
|
|
249
|
+
if (byType.modified.length > 0) {
|
|
250
|
+
const names = byType.modified.map((a) => this.getDisplayName(a.path)).join(', ');
|
|
251
|
+
parts.push(`${names} (modified)`);
|
|
252
|
+
}
|
|
253
|
+
if (byType.referenced.length > 0) {
|
|
254
|
+
const count = byType.referenced.length;
|
|
255
|
+
if (count <= 2) {
|
|
256
|
+
const names = byType.referenced.map((a) => this.getDisplayName(a.path)).join(', ');
|
|
257
|
+
parts.push(`${names} (referenced)`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const first = this.getDisplayName(byType.referenced[0].path);
|
|
261
|
+
parts.push(`${first} +${String(count - 1)} more (referenced)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const moreNote = hasMore ? ' +more' : '';
|
|
265
|
+
return `[Previously accessed: ${parts.join('; ')}${moreNote}]`;
|
|
266
|
+
}
|
|
267
|
+
formatGrouped(accesses, hasMore, opts) {
|
|
268
|
+
const byType = this.groupByType(accesses);
|
|
269
|
+
const lines = ['[Context compacted. Previously accessed files:]', ''];
|
|
270
|
+
if (byType.read.length > 0) {
|
|
271
|
+
const count = byType.read.length;
|
|
272
|
+
lines.push(`Read (${String(count)} file${count > 1 ? 's' : ''}):`);
|
|
273
|
+
for (const access of byType.read) {
|
|
274
|
+
lines.push(this.formatAccessLine(access, opts));
|
|
275
|
+
}
|
|
276
|
+
lines.push('');
|
|
277
|
+
}
|
|
278
|
+
if (byType.modified.length > 0) {
|
|
279
|
+
const count = byType.modified.length;
|
|
280
|
+
lines.push(`Modified (${String(count)} file${count > 1 ? 's' : ''}):`);
|
|
281
|
+
for (const access of byType.modified) {
|
|
282
|
+
lines.push(this.formatAccessLine(access, opts));
|
|
283
|
+
}
|
|
284
|
+
lines.push('');
|
|
285
|
+
}
|
|
286
|
+
if (byType.referenced.length > 0) {
|
|
287
|
+
const count = byType.referenced.length;
|
|
288
|
+
lines.push(`Referenced (${String(count)} file${count > 1 ? 's' : ''}):`);
|
|
289
|
+
for (const access of byType.referenced) {
|
|
290
|
+
lines.push(this.formatAccessLine(access, opts));
|
|
291
|
+
}
|
|
292
|
+
lines.push('');
|
|
293
|
+
}
|
|
294
|
+
if (hasMore) {
|
|
295
|
+
lines.push('(Additional files truncated)');
|
|
296
|
+
lines.push('');
|
|
297
|
+
}
|
|
298
|
+
lines.push('[Use read_file to re-access content if needed.]');
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
|
301
|
+
formatFlat(accesses, hasMore, opts) {
|
|
302
|
+
const lines = ['[Context compacted. Previously accessed files:]', ''];
|
|
303
|
+
for (const access of accesses) {
|
|
304
|
+
const typeLabel = access.type === 'read' ? 'R' : access.type === 'modified' ? 'M' : 'ref';
|
|
305
|
+
lines.push(` [${typeLabel}] ${this.formatAccessLine(access, opts).trim()}`);
|
|
306
|
+
}
|
|
307
|
+
if (hasMore) {
|
|
308
|
+
lines.push('');
|
|
309
|
+
lines.push('(Additional files truncated)');
|
|
310
|
+
}
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push('[Use read_file to re-access content if needed.]');
|
|
313
|
+
return lines.join('\n');
|
|
314
|
+
}
|
|
315
|
+
formatAccessLine(access, opts) {
|
|
316
|
+
const parts = [` • ${this.getDisplayPath(access.path)}`];
|
|
317
|
+
if (opts.includeLineCount && access.lineCount !== undefined) {
|
|
318
|
+
parts.push(`(${String(access.lineCount)} lines)`);
|
|
319
|
+
}
|
|
320
|
+
if (opts.includeSummary && access.summary) {
|
|
321
|
+
parts.push(`- ${access.summary}`);
|
|
322
|
+
}
|
|
323
|
+
if (opts.includeTimestamp) {
|
|
324
|
+
const date = new Date(access.timestamp);
|
|
325
|
+
parts.push(`@ ${date.toISOString().slice(11, 19)}`);
|
|
326
|
+
}
|
|
327
|
+
return parts.join(' ');
|
|
328
|
+
}
|
|
329
|
+
groupByType(accesses) {
|
|
330
|
+
const result = {
|
|
331
|
+
read: [],
|
|
332
|
+
referenced: [],
|
|
333
|
+
modified: [],
|
|
334
|
+
};
|
|
335
|
+
for (const access of accesses) {
|
|
336
|
+
result[access.type].push(access);
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
getDisplayName(filePath) {
|
|
341
|
+
return path.basename(filePath);
|
|
342
|
+
}
|
|
343
|
+
getDisplayPath(filePath) {
|
|
344
|
+
// Try to make path shorter for display
|
|
345
|
+
const cwd = process.cwd();
|
|
346
|
+
if (filePath.startsWith(cwd)) {
|
|
347
|
+
return filePath.slice(cwd.length + 1); // Remove cwd + leading slash
|
|
348
|
+
}
|
|
349
|
+
// Truncate long absolute paths
|
|
350
|
+
if (filePath.length > 60) {
|
|
351
|
+
const parts = filePath.split(path.sep);
|
|
352
|
+
if (parts.length > 4) {
|
|
353
|
+
return `...${path.sep}${parts.slice(-3).join(path.sep)}`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return filePath;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Tracking Hook
|
|
3
|
+
*
|
|
4
|
+
* An afterTool hook that automatically tracks file accesses based on tool calls.
|
|
5
|
+
* This approach is cleaner than modifying each tool directly.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const tracker = new FileAccessTracker();
|
|
10
|
+
* const agent = new Agent({
|
|
11
|
+
* hooks: {
|
|
12
|
+
* afterTool: [createFileTrackingHook(tracker)]
|
|
13
|
+
* }
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import type { AfterToolHook } from '../hooks/types.js';
|
|
18
|
+
import type { FileAccessTracker } from './file-tracker.js';
|
|
19
|
+
/**
|
|
20
|
+
* Create an afterTool hook that tracks file accesses
|
|
21
|
+
*
|
|
22
|
+
* @param tracker - FileAccessTracker instance to record accesses
|
|
23
|
+
* @returns An afterTool hook function
|
|
24
|
+
*/
|
|
25
|
+
export declare function createFileTrackingHook(tracker: FileAccessTracker): AfterToolHook;
|
|
26
|
+
/**
|
|
27
|
+
* Tool names that this hook tracks
|
|
28
|
+
*/
|
|
29
|
+
export declare const TRACKED_TOOLS: readonly ["read_file", "write_file", "edit", "grep", "glob"];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Tracking Hook
|
|
3
|
+
*
|
|
4
|
+
* An afterTool hook that automatically tracks file accesses based on tool calls.
|
|
5
|
+
* This approach is cleaner than modifying each tool directly.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const tracker = new FileAccessTracker();
|
|
10
|
+
* const agent = new Agent({
|
|
11
|
+
* hooks: {
|
|
12
|
+
* afterTool: [createFileTrackingHook(tracker)]
|
|
13
|
+
* }
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { TOOL_NAMES } from '../tools/builtin/tool-names.js';
|
|
18
|
+
/**
|
|
19
|
+
* Create an afterTool hook that tracks file accesses
|
|
20
|
+
*
|
|
21
|
+
* @param tracker - FileAccessTracker instance to record accesses
|
|
22
|
+
* @returns An afterTool hook function
|
|
23
|
+
*/
|
|
24
|
+
export function createFileTrackingHook(tracker) {
|
|
25
|
+
return (context) => {
|
|
26
|
+
const { toolName, input, result } = context;
|
|
27
|
+
// Only track successful operations
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
switch (toolName) {
|
|
32
|
+
case TOOL_NAMES.READ_FILE: {
|
|
33
|
+
const readInput = input;
|
|
34
|
+
const readResult = result;
|
|
35
|
+
const lineCount = readResult.result?.lineCount ?? 0;
|
|
36
|
+
tracker.trackRead(readInput.file_path, lineCount);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case TOOL_NAMES.WRITE_FILE: {
|
|
40
|
+
const writeInput = input;
|
|
41
|
+
tracker.trackModification(writeInput.file_path, 'File written');
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case TOOL_NAMES.EDIT: {
|
|
45
|
+
const editInput = input;
|
|
46
|
+
tracker.trackModification(editInput.file_path, 'File edited');
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case TOOL_NAMES.GREP: {
|
|
50
|
+
const grepInput = input;
|
|
51
|
+
const grepResult = result;
|
|
52
|
+
// Track the search path if specified
|
|
53
|
+
if (grepInput.path) {
|
|
54
|
+
tracker.trackReference(grepInput.path);
|
|
55
|
+
}
|
|
56
|
+
// Track all files that matched
|
|
57
|
+
if (grepResult.result?.matches) {
|
|
58
|
+
const seenFiles = new Set();
|
|
59
|
+
for (const match of grepResult.result.matches) {
|
|
60
|
+
if (!seenFiles.has(match.file)) {
|
|
61
|
+
seenFiles.add(match.file);
|
|
62
|
+
tracker.trackReference(match.file);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (grepResult.result?.files) {
|
|
67
|
+
for (const file of grepResult.result.files) {
|
|
68
|
+
tracker.trackReference(file);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case TOOL_NAMES.GLOB: {
|
|
74
|
+
const globInput = input;
|
|
75
|
+
const globResult = result;
|
|
76
|
+
// Track the search path if specified
|
|
77
|
+
if (globInput.path) {
|
|
78
|
+
tracker.trackReference(globInput.path);
|
|
79
|
+
}
|
|
80
|
+
// Track all matched files
|
|
81
|
+
if (globResult.result?.files) {
|
|
82
|
+
for (const file of globResult.result.files) {
|
|
83
|
+
tracker.trackReference(file);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
// Note: bash tool could potentially be tracked for file operations,
|
|
89
|
+
// but it's complex to parse shell commands. Skip for now.
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Tool names that this hook tracks
|
|
96
|
+
*/
|
|
97
|
+
export const TRACKED_TOOLS = [
|
|
98
|
+
TOOL_NAMES.READ_FILE,
|
|
99
|
+
TOOL_NAMES.WRITE_FILE,
|
|
100
|
+
TOOL_NAMES.EDIT,
|
|
101
|
+
TOOL_NAMES.GREP,
|
|
102
|
+
TOOL_NAMES.GLOB,
|
|
103
|
+
];
|
package/dist/context/index.d.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* - Filtering (truncate large content)
|
|
7
7
|
* - Compaction (save to files, replace with references)
|
|
8
8
|
* - Summarization (compress history when near limit)
|
|
9
|
+
* - File access tracking (for context restoration hints)
|
|
9
10
|
*/
|
|
10
11
|
export { ContextManager, DEFAULT_CONTEXT_CONFIG } from './manager.js';
|
|
11
12
|
export type { ContextManagerOptions } from './manager.js';
|
|
12
|
-
export
|
|
13
|
+
export { FileAccessTracker } from './file-tracker.js';
|
|
14
|
+
export type { FileAccessType, FileAccess, FileAccessTrackerOptions, FormatHintsOptions, FileAccessStats, } from './file-tracker.js';
|
|
15
|
+
export { createFileTrackingHook, TRACKED_TOOLS } from './file-tracking-hook.js';
|
|
16
|
+
export type { ContextCategory, BudgetAllocation, CategoryBudgetInfo, PreflightResult, VerbosityLevel, VerbosityConfig, ContextConfig, FilteringConfig, CompactionConfig, SummarizationConfig, CompactionResult, SummarizationResult, FilteringResult, ContextEvent, ContextEventHandler, ContextStats, CategorizedMessages, SmartCompactOptions, SmartCompactionResult, } from './types.js';
|
package/dist/context/index.js
CHANGED
|
@@ -6,5 +6,8 @@
|
|
|
6
6
|
* - Filtering (truncate large content)
|
|
7
7
|
* - Compaction (save to files, replace with references)
|
|
8
8
|
* - Summarization (compress history when near limit)
|
|
9
|
+
* - File access tracking (for context restoration hints)
|
|
9
10
|
*/
|
|
10
11
|
export { ContextManager, DEFAULT_CONTEXT_CONFIG } from './manager.js';
|
|
12
|
+
export { FileAccessTracker } from './file-tracker.js';
|
|
13
|
+
export { createFileTrackingHook, TRACKED_TOOLS } from './file-tracking-hook.js';
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
import type { Message, LLMProvider } from '../providers/types.js';
|
|
21
|
-
import type { ContextConfig, ContextStats, CompactionResult, SummarizationResult, ContextEventHandler, ContextCategory, CategoryBudgetInfo, BudgetAllocation, PreflightResult, VerbosityLevel } from './types.js';
|
|
21
|
+
import type { ContextConfig, ContextStats, CompactionResult, SummarizationResult, ContextEventHandler, ContextCategory, CategoryBudgetInfo, BudgetAllocation, PreflightResult, VerbosityLevel, CategorizedMessages, SmartCompactOptions, SmartCompactionResult } from './types.js';
|
|
22
|
+
import type { FileAccessTracker } from './file-tracker.js';
|
|
22
23
|
/**
|
|
23
24
|
* Default budget allocation
|
|
24
25
|
*/
|
|
@@ -43,6 +44,12 @@ export interface ContextManagerOptions {
|
|
|
43
44
|
* Event handler for context events
|
|
44
45
|
*/
|
|
45
46
|
onEvent?: ContextEventHandler;
|
|
47
|
+
/**
|
|
48
|
+
* File access tracker for context restoration hints.
|
|
49
|
+
* When provided, compaction/summarization will inject hints
|
|
50
|
+
* about previously accessed files.
|
|
51
|
+
*/
|
|
52
|
+
fileTracker?: FileAccessTracker;
|
|
46
53
|
}
|
|
47
54
|
/**
|
|
48
55
|
* ContextManager tracks and manages context window usage
|
|
@@ -51,6 +58,7 @@ export declare class ContextManager {
|
|
|
51
58
|
private readonly provider;
|
|
52
59
|
private readonly config;
|
|
53
60
|
private readonly onEvent?;
|
|
61
|
+
private readonly fileTracker?;
|
|
54
62
|
private cachedTokenCount;
|
|
55
63
|
private turnCount;
|
|
56
64
|
private compactionCount;
|
|
@@ -110,6 +118,23 @@ export declare class ContextManager {
|
|
|
110
118
|
* Get the current configuration
|
|
111
119
|
*/
|
|
112
120
|
getConfig(): ContextConfig;
|
|
121
|
+
/**
|
|
122
|
+
* Get the file access tracker (if configured)
|
|
123
|
+
*/
|
|
124
|
+
getFileTracker(): FileAccessTracker | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* Format context restoration hints based on current verbosity level.
|
|
127
|
+
* Returns empty string if no file tracker is configured or no files have been accessed.
|
|
128
|
+
*/
|
|
129
|
+
formatRestorationHints(): string;
|
|
130
|
+
/**
|
|
131
|
+
* Create a system message with context restoration hints.
|
|
132
|
+
* Returns undefined if no hints are available.
|
|
133
|
+
*
|
|
134
|
+
* The caller (Agent) is responsible for injecting this message
|
|
135
|
+
* after compaction or summarization.
|
|
136
|
+
*/
|
|
137
|
+
createRestorationHintMessage(): Message | undefined;
|
|
113
138
|
/**
|
|
114
139
|
* Get budget information for a specific category
|
|
115
140
|
*/
|
|
@@ -144,6 +169,49 @@ export declare class ContextManager {
|
|
|
144
169
|
* Determine which category a message belongs to
|
|
145
170
|
*/
|
|
146
171
|
private categorizeMessage;
|
|
172
|
+
/**
|
|
173
|
+
* Categorize all messages by type for smart compaction
|
|
174
|
+
*
|
|
175
|
+
* This method:
|
|
176
|
+
* 1. Identifies system messages
|
|
177
|
+
* 2. Separates recent messages (last N turns) that should never be compacted
|
|
178
|
+
* 3. Identifies tool results in older messages
|
|
179
|
+
* 4. Classifies remaining older messages as history
|
|
180
|
+
*
|
|
181
|
+
* @param messages All messages to categorize
|
|
182
|
+
* @param preserveRecentTurns Number of recent turns to preserve (default: config value)
|
|
183
|
+
*/
|
|
184
|
+
categorizeMessages(messages: Message[], preserveRecentTurns?: number): Promise<CategorizedMessages>;
|
|
185
|
+
/**
|
|
186
|
+
* Find which categories are over their budget allocation
|
|
187
|
+
*
|
|
188
|
+
* @param categorized Categorized messages from categorizeMessages()
|
|
189
|
+
* @returns Array of categories that exceed their budget, sorted by how much they exceed
|
|
190
|
+
*/
|
|
191
|
+
findOverBudgetCategories(categorized: CategorizedMessages): ContextCategory[];
|
|
192
|
+
/**
|
|
193
|
+
* Smart compaction that respects category budgets
|
|
194
|
+
*
|
|
195
|
+
* Strategy:
|
|
196
|
+
* 1. System messages: Never compacted (critical for agent behavior)
|
|
197
|
+
* 2. Recent messages: Never compacted (needed for conversation continuity)
|
|
198
|
+
* 3. Tool results: Save large results to files, replace with references
|
|
199
|
+
* 4. History: Summarize with LLM
|
|
200
|
+
*
|
|
201
|
+
* The method compacts categories in order of how much they exceed their budget.
|
|
202
|
+
*/
|
|
203
|
+
smartCompact(messages: Message[], options: SmartCompactOptions): Promise<{
|
|
204
|
+
messages: Message[];
|
|
205
|
+
result: SmartCompactionResult;
|
|
206
|
+
}>;
|
|
207
|
+
/**
|
|
208
|
+
* Compact tool results by saving large ones to files
|
|
209
|
+
*/
|
|
210
|
+
private compactToolResults;
|
|
211
|
+
/**
|
|
212
|
+
* Calculate utilization given a token count
|
|
213
|
+
*/
|
|
214
|
+
private calculateUtilization;
|
|
147
215
|
/**
|
|
148
216
|
* Estimate tokens for a string content
|
|
149
217
|
*/
|