@compilr-dev/sdk 0.7.32 → 0.9.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.js +20 -1
- package/dist/compressors/bash.d.ts +12 -0
- package/dist/compressors/bash.js +342 -0
- package/dist/compressors/index.d.ts +31 -0
- package/dist/compressors/index.js +169 -0
- package/dist/config.d.ts +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CompilrAgent — high-level wrapper around @compilr-dev/agents Agent
|
|
3
3
|
*/
|
|
4
4
|
import { Agent, ContextManager, createSuggestTool, } from '@compilr-dev/agents';
|
|
5
|
+
import { createCompressorHook } from './compressors/index.js';
|
|
5
6
|
import { resolveProvider } from './provider.js';
|
|
6
7
|
import { resolvePreset } from './presets/index.js';
|
|
7
8
|
import { assembleTools, deduplicateTools } from './tools.js';
|
|
@@ -241,6 +242,13 @@ class CompilrAgentImpl {
|
|
|
241
242
|
: [];
|
|
242
243
|
mergedHooks.beforeLLM = [...existingHooks, ...capabilityHooks];
|
|
243
244
|
}
|
|
245
|
+
// Add output compressor as the first afterTool hook (runs before delegation)
|
|
246
|
+
const existingAfterTool = config?.hooks?.afterTool
|
|
247
|
+
? Array.isArray(config.hooks.afterTool)
|
|
248
|
+
? config.hooks.afterTool
|
|
249
|
+
: [config.hooks.afterTool]
|
|
250
|
+
: [];
|
|
251
|
+
mergedHooks.afterTool = [createCompressorHook(), ...existingAfterTool];
|
|
244
252
|
// Build observation mask config — SDK defaults include platform tools
|
|
245
253
|
const observationMask = config?.context?.observationMask !== false
|
|
246
254
|
? {
|
|
@@ -314,7 +322,16 @@ class CompilrAgentImpl {
|
|
|
314
322
|
: false
|
|
315
323
|
: undefined,
|
|
316
324
|
hooks: mergedHooks,
|
|
317
|
-
|
|
325
|
+
enableFileTracking: config?.enableFileTracking ?? true,
|
|
326
|
+
pins: config?.pins
|
|
327
|
+
? {
|
|
328
|
+
maxAnchors: config.pins.maxAnchors ?? 50,
|
|
329
|
+
maxTokens: config.pins.maxTokens ?? 4000,
|
|
330
|
+
includeDefaults: config.pins.includeDefaults ?? true,
|
|
331
|
+
}
|
|
332
|
+
: {},
|
|
333
|
+
onIterationLimitReached: config?.onIterationLimitReached,
|
|
334
|
+
iterationLimitBehavior: config?.iterationLimitBehavior,
|
|
318
335
|
permissions: {
|
|
319
336
|
defaultLevel: permissionsConfig.defaultLevel,
|
|
320
337
|
onPermissionRequest: permissionsConfig.onPermissionRequest,
|
|
@@ -334,6 +351,8 @@ class CompilrAgentImpl {
|
|
|
334
351
|
this.totalUsage.outputTokens += event.tokens.outputTokens;
|
|
335
352
|
this.totalUsage.totalTokens += event.tokens.inputTokens + event.tokens.outputTokens;
|
|
336
353
|
}
|
|
354
|
+
// Forward to user-provided onEvent callback
|
|
355
|
+
config?.onEvent?.(event);
|
|
337
356
|
// Forward ALL events to external listener (if set via run/stream)
|
|
338
357
|
this.externalEventListener?.(event);
|
|
339
358
|
},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Output Compressors — command-specific output compression
|
|
3
|
+
*
|
|
4
|
+
* Each compressor understands the structure of a CLI tool's output
|
|
5
|
+
* and strips noise (progress bars, boilerplate, redundant info)
|
|
6
|
+
* while preserving signal (errors, changed files, test failures).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Compress bash command output based on the command that was run.
|
|
10
|
+
* Returns null if no compressor matches (output passes through unchanged).
|
|
11
|
+
*/
|
|
12
|
+
export declare function compressBashOutput(command: string, stdout: string): string | null;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Output Compressors — command-specific output compression
|
|
3
|
+
*
|
|
4
|
+
* Each compressor understands the structure of a CLI tool's output
|
|
5
|
+
* and strips noise (progress bars, boilerplate, redundant info)
|
|
6
|
+
* while preserving signal (errors, changed files, test failures).
|
|
7
|
+
*/
|
|
8
|
+
// ─── Router ─────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Compress bash command output based on the command that was run.
|
|
11
|
+
* Returns null if no compressor matches (output passes through unchanged).
|
|
12
|
+
*/
|
|
13
|
+
export function compressBashOutput(command, stdout) {
|
|
14
|
+
const cmd = command.trim();
|
|
15
|
+
// git commands
|
|
16
|
+
if (cmd.match(/^git\s+status/))
|
|
17
|
+
return compressGitStatus(stdout);
|
|
18
|
+
if (cmd.match(/^git\s+log/))
|
|
19
|
+
return compressGitLog(stdout);
|
|
20
|
+
if (cmd.match(/^git\s+diff/))
|
|
21
|
+
return compressGitDiff(stdout);
|
|
22
|
+
if (cmd.match(/^git\s+(add|commit|push|pull|fetch|merge|rebase|checkout|switch|branch)/))
|
|
23
|
+
return compressGitAction(stdout);
|
|
24
|
+
// npm/yarn/pnpm
|
|
25
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+install/))
|
|
26
|
+
return compressNpmInstall(stdout);
|
|
27
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+(test|run\s+test)/))
|
|
28
|
+
return compressTestOutput(stdout);
|
|
29
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+run\s+(lint|eslint)/))
|
|
30
|
+
return compressLintOutput(stdout);
|
|
31
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+run\s+(build|tsc)/))
|
|
32
|
+
return compressBuildOutput(stdout);
|
|
33
|
+
// Direct test runners
|
|
34
|
+
if (cmd.match(/^(jest|vitest|mocha|pytest|cargo\s+test|go\s+test)/))
|
|
35
|
+
return compressTestOutput(stdout);
|
|
36
|
+
// Direct linters
|
|
37
|
+
if (cmd.match(/^(eslint|tsc|ruff|cargo\s+clippy|golangci-lint)/))
|
|
38
|
+
return compressLintOutput(stdout);
|
|
39
|
+
// ls / find / tree
|
|
40
|
+
if (cmd.match(/^(ls|find|tree|fd)\b/))
|
|
41
|
+
return compressFileList(stdout);
|
|
42
|
+
// curl / wget (HTTP responses)
|
|
43
|
+
if (cmd.match(/^(curl|wget)\b/))
|
|
44
|
+
return compressCurlOutput(stdout);
|
|
45
|
+
return null; // No compressor matched
|
|
46
|
+
}
|
|
47
|
+
// ─── Git Status ─────────────────────────────────────────────────────────────
|
|
48
|
+
function compressGitStatus(output) {
|
|
49
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
50
|
+
if (lines.length <= 20)
|
|
51
|
+
return output;
|
|
52
|
+
const sections = [];
|
|
53
|
+
let current = null;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (line.startsWith('On branch') ||
|
|
56
|
+
line.startsWith('Your branch') ||
|
|
57
|
+
line.startsWith('HEAD detached')) {
|
|
58
|
+
sections.push({ header: line, files: [] });
|
|
59
|
+
}
|
|
60
|
+
else if (line.match(/^(Changes|Untracked|Unmerged)/)) {
|
|
61
|
+
current = { header: line, files: [] };
|
|
62
|
+
sections.push(current);
|
|
63
|
+
}
|
|
64
|
+
else if (current &&
|
|
65
|
+
(line.startsWith('\t') || line.match(/^\s+(modified|new file|deleted|renamed)/))) {
|
|
66
|
+
current.files.push(line.trim());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Rebuild with capped file lists
|
|
70
|
+
const maxFilesPerSection = 15;
|
|
71
|
+
const result = [];
|
|
72
|
+
for (const section of sections) {
|
|
73
|
+
result.push(section.header);
|
|
74
|
+
const shown = section.files.slice(0, maxFilesPerSection);
|
|
75
|
+
for (const f of shown)
|
|
76
|
+
result.push(` ${f}`);
|
|
77
|
+
if (section.files.length > maxFilesPerSection) {
|
|
78
|
+
result.push(` ... +${String(section.files.length - maxFilesPerSection)} more files`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result.join('\n');
|
|
82
|
+
}
|
|
83
|
+
// ─── Git Log ────────────────────────────────────────────────────────────────
|
|
84
|
+
function compressGitLog(output) {
|
|
85
|
+
const lines = output.split('\n');
|
|
86
|
+
if (lines.length <= 60)
|
|
87
|
+
return output;
|
|
88
|
+
// Keep first 40 lines (recent commits), summarize the rest
|
|
89
|
+
const kept = lines.slice(0, 40);
|
|
90
|
+
const remaining = lines.slice(40).filter((l) => l.match(/^commit\s/));
|
|
91
|
+
if (remaining.length > 0) {
|
|
92
|
+
kept.push(`\n... +${String(remaining.length)} older commits omitted`);
|
|
93
|
+
}
|
|
94
|
+
return kept.join('\n');
|
|
95
|
+
}
|
|
96
|
+
// ─── Git Diff ───────────────────────────────────────────────────────────────
|
|
97
|
+
function compressGitDiff(output) {
|
|
98
|
+
const lines = output.split('\n');
|
|
99
|
+
if (lines.length <= 100)
|
|
100
|
+
return output;
|
|
101
|
+
// Keep diff headers and first N lines of each file, collapse large hunks
|
|
102
|
+
const result = [];
|
|
103
|
+
let currentFile = '';
|
|
104
|
+
let hunkLines = 0;
|
|
105
|
+
const maxHunkLines = 30;
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
if (line.startsWith('diff --git')) {
|
|
108
|
+
currentFile = line;
|
|
109
|
+
hunkLines = 0;
|
|
110
|
+
result.push(line);
|
|
111
|
+
}
|
|
112
|
+
else if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')) {
|
|
113
|
+
hunkLines = 0;
|
|
114
|
+
result.push(line);
|
|
115
|
+
}
|
|
116
|
+
else if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
|
|
117
|
+
hunkLines++;
|
|
118
|
+
if (hunkLines <= maxHunkLines) {
|
|
119
|
+
result.push(line);
|
|
120
|
+
}
|
|
121
|
+
else if (hunkLines === maxHunkLines + 1) {
|
|
122
|
+
result.push(`... (hunk truncated, ${currentFile.split(' b/')[1] ?? 'file'} continues)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
result.push(line);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result.join('\n');
|
|
130
|
+
}
|
|
131
|
+
// ─── Git Action (add, commit, push, pull, etc.) ─────────────────────────────
|
|
132
|
+
function compressGitAction(output) {
|
|
133
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
134
|
+
if (lines.length <= 15)
|
|
135
|
+
return output;
|
|
136
|
+
// Keep summary lines, strip verbose file lists
|
|
137
|
+
const important = lines.filter((l) => l.match(/^(\[|To |From |Already|Everything|Branch|Updating|Fast-forward|CONFLICT|error:|fatal:|warning:|\s*\d+ file|create mode|delete mode)/) ||
|
|
138
|
+
l.includes('->') ||
|
|
139
|
+
l.includes('insertions') ||
|
|
140
|
+
l.includes('deletions'));
|
|
141
|
+
if (important.length > 0 && important.length < lines.length) {
|
|
142
|
+
const omitted = lines.length - important.length;
|
|
143
|
+
important.push(`(${String(omitted)} lines of detail omitted)`);
|
|
144
|
+
return important.join('\n');
|
|
145
|
+
}
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
// ─── npm install ────────────────────────────────────────────────────────────
|
|
149
|
+
function compressNpmInstall(output) {
|
|
150
|
+
const lines = output.split('\n');
|
|
151
|
+
if (lines.length <= 10)
|
|
152
|
+
return output;
|
|
153
|
+
// Keep: added/removed/changed summary, warnings, errors
|
|
154
|
+
// Strip: progress bars, individual package resolutions, timing
|
|
155
|
+
const kept = lines.filter((l) => {
|
|
156
|
+
const t = l.trim();
|
|
157
|
+
if (!t)
|
|
158
|
+
return false;
|
|
159
|
+
if (t.startsWith('npm warn') || t.startsWith('npm error') || t.startsWith('npm ERR'))
|
|
160
|
+
return true;
|
|
161
|
+
if (t.match(/^added \d|^removed \d|^changed \d|^up to date/))
|
|
162
|
+
return true;
|
|
163
|
+
if (t.includes('vulnerabilit'))
|
|
164
|
+
return true;
|
|
165
|
+
if (t.startsWith('found '))
|
|
166
|
+
return true;
|
|
167
|
+
return false;
|
|
168
|
+
});
|
|
169
|
+
if (kept.length === 0)
|
|
170
|
+
return output; // Couldn't parse — return original
|
|
171
|
+
return kept.join('\n');
|
|
172
|
+
}
|
|
173
|
+
// ─── Test Output ────────────────────────────────────────────────────────────
|
|
174
|
+
function compressTestOutput(output) {
|
|
175
|
+
const lines = output.split('\n');
|
|
176
|
+
if (lines.length <= 30)
|
|
177
|
+
return output;
|
|
178
|
+
const result = [];
|
|
179
|
+
let inFailure = false;
|
|
180
|
+
let failureLines = 0;
|
|
181
|
+
const maxFailureLines = 20;
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const t = line.trim();
|
|
184
|
+
// Always keep: summary lines, failures, errors
|
|
185
|
+
if (t.match(/^(Tests?|Test Suites?|FAIL|PASS|ERROR|✓|✗|✘|×|●|Ran \d|passed|failed|\d+ passing|\d+ failing|Test Files|Duration)/i)) {
|
|
186
|
+
result.push(line);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Keep failure context (limited)
|
|
190
|
+
if (t.match(/^(FAIL|✗|✘|×|Error:|AssertionError|Expected|Received|at\s)/i) || inFailure) {
|
|
191
|
+
if (t.match(/^(FAIL|✗|✘|×)/i)) {
|
|
192
|
+
inFailure = true;
|
|
193
|
+
failureLines = 0;
|
|
194
|
+
}
|
|
195
|
+
failureLines++;
|
|
196
|
+
if (failureLines <= maxFailureLines) {
|
|
197
|
+
result.push(line);
|
|
198
|
+
}
|
|
199
|
+
if (t === '' || t.match(/^(PASS|✓)/)) {
|
|
200
|
+
inFailure = false;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Strip: individual passing test names, progress, timing of each test
|
|
205
|
+
// (these are the bulk of test output — passing tests don't need names)
|
|
206
|
+
}
|
|
207
|
+
if (result.length < lines.length) {
|
|
208
|
+
const omitted = lines.length - result.length;
|
|
209
|
+
result.push(`(${String(omitted)} lines of passing test detail omitted)`);
|
|
210
|
+
}
|
|
211
|
+
return result.join('\n');
|
|
212
|
+
}
|
|
213
|
+
// ─── Lint Output ────────────────────────────────────────────────────────────
|
|
214
|
+
function compressLintOutput(output) {
|
|
215
|
+
const lines = output.split('\n');
|
|
216
|
+
if (lines.length <= 30)
|
|
217
|
+
return output;
|
|
218
|
+
// Group errors by rule, cap per rule
|
|
219
|
+
const errors = [];
|
|
220
|
+
const warnings = [];
|
|
221
|
+
const summaryLines = [];
|
|
222
|
+
const ruleCount = new Map();
|
|
223
|
+
const maxPerRule = 5;
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const t = line.trim();
|
|
226
|
+
// Summary lines
|
|
227
|
+
if (t.match(/^✖|^\d+ problem|^\d+ error|^\d+ warning|^error:|^warning:/i)) {
|
|
228
|
+
summaryLines.push(line);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// ESLint-style: "file:line:col error message rule"
|
|
232
|
+
const ruleMatch = t.match(/\s+(error|warning)\s+.+\s+(\S+)$/);
|
|
233
|
+
if (ruleMatch) {
|
|
234
|
+
const rule = ruleMatch[2];
|
|
235
|
+
const count = (ruleCount.get(rule) ?? 0) + 1;
|
|
236
|
+
ruleCount.set(rule, count);
|
|
237
|
+
if (count <= maxPerRule) {
|
|
238
|
+
if (ruleMatch[1] === 'error')
|
|
239
|
+
errors.push(line);
|
|
240
|
+
else
|
|
241
|
+
warnings.push(line);
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// TypeScript-style: "file(line,col): error TS..."
|
|
246
|
+
if (t.match(/error TS\d+/)) {
|
|
247
|
+
errors.push(line);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const result = [...errors, ...warnings.slice(0, 20)];
|
|
252
|
+
// Show rules that were capped
|
|
253
|
+
for (const [rule, count] of ruleCount) {
|
|
254
|
+
if (count > maxPerRule) {
|
|
255
|
+
result.push(` ... +${String(count - maxPerRule)} more ${rule}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
result.push(...summaryLines);
|
|
259
|
+
if (result.length < lines.length) {
|
|
260
|
+
return result.join('\n');
|
|
261
|
+
}
|
|
262
|
+
return output;
|
|
263
|
+
}
|
|
264
|
+
// ─── Build Output ───────────────────────────────────────────────────────────
|
|
265
|
+
function compressBuildOutput(output) {
|
|
266
|
+
const lines = output.split('\n');
|
|
267
|
+
if (lines.length <= 20)
|
|
268
|
+
return output;
|
|
269
|
+
// Keep errors and summary, strip progress/compilation messages
|
|
270
|
+
const kept = lines.filter((l) => {
|
|
271
|
+
const t = l.trim();
|
|
272
|
+
if (!t)
|
|
273
|
+
return false;
|
|
274
|
+
if (t.match(/^(error|Error|ERROR|warning|Warning|WARN)/))
|
|
275
|
+
return true;
|
|
276
|
+
if (t.match(/^(✓|✗|✘|Done|Built|Compiled|Successfully|Failed|FAIL)/))
|
|
277
|
+
return true;
|
|
278
|
+
if (t.match(/error TS\d+/))
|
|
279
|
+
return true;
|
|
280
|
+
if (t.includes('bundle') && t.includes('kB'))
|
|
281
|
+
return true; // Bundle size info
|
|
282
|
+
return false;
|
|
283
|
+
});
|
|
284
|
+
if (kept.length > 0 && kept.length < lines.length * 0.7) {
|
|
285
|
+
const omitted = lines.length - kept.length;
|
|
286
|
+
kept.push(`(${String(omitted)} lines of build output omitted)`);
|
|
287
|
+
return kept.join('\n');
|
|
288
|
+
}
|
|
289
|
+
return output;
|
|
290
|
+
}
|
|
291
|
+
// ─── File List (ls, find, tree) ─────────────────────────────────────────────
|
|
292
|
+
function compressFileList(output) {
|
|
293
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
294
|
+
if (lines.length <= 50)
|
|
295
|
+
return output;
|
|
296
|
+
// Group by top-level directory, cap entries
|
|
297
|
+
const groups = new Map();
|
|
298
|
+
for (const line of lines) {
|
|
299
|
+
const parts = line.replace(/^\.\//, '').split('/');
|
|
300
|
+
const dir = parts.length > 1 ? parts[0] : '.';
|
|
301
|
+
const arr = groups.get(dir) ?? [];
|
|
302
|
+
arr.push(line);
|
|
303
|
+
groups.set(dir, arr);
|
|
304
|
+
}
|
|
305
|
+
const maxPerDir = 10;
|
|
306
|
+
const result = [];
|
|
307
|
+
for (const [dir, files] of groups) {
|
|
308
|
+
if (dir !== '.')
|
|
309
|
+
result.push(`${dir}/`);
|
|
310
|
+
const shown = files.slice(0, maxPerDir);
|
|
311
|
+
for (const f of shown)
|
|
312
|
+
result.push(f);
|
|
313
|
+
if (files.length > maxPerDir) {
|
|
314
|
+
result.push(` ... +${String(files.length - maxPerDir)} more in ${dir}/`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (result.length < lines.length) {
|
|
318
|
+
result.push(`\n(${String(lines.length)} total entries, showing ${String(result.length)} lines)`);
|
|
319
|
+
}
|
|
320
|
+
return result.join('\n');
|
|
321
|
+
}
|
|
322
|
+
// ─── curl / wget ────────────────────────────────────────────────────────────
|
|
323
|
+
function compressCurlOutput(output) {
|
|
324
|
+
const lines = output.split('\n');
|
|
325
|
+
if (lines.length <= 30)
|
|
326
|
+
return output;
|
|
327
|
+
// Strip progress bars (curl -# output), keep headers and body
|
|
328
|
+
const kept = lines.filter((l) => {
|
|
329
|
+
const t = l.trim();
|
|
330
|
+
// Strip progress indicators
|
|
331
|
+
if (t.match(/^[#\s]*\d+(\.\d+)?%/) || t.match(/^\s*\d+\s+\d+\s+\d+\s+\d+/))
|
|
332
|
+
return false;
|
|
333
|
+
// Strip curl stats
|
|
334
|
+
if (t.match(/^\s*(Dload|Upload|Total|Spent|Left|Speed)/))
|
|
335
|
+
return false;
|
|
336
|
+
return true;
|
|
337
|
+
});
|
|
338
|
+
if (kept.length < lines.length) {
|
|
339
|
+
return kept.join('\n');
|
|
340
|
+
}
|
|
341
|
+
return output;
|
|
342
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Output Compressors — Format-aware output compression
|
|
3
|
+
*
|
|
4
|
+
* Reduces token usage by 60-90% on common tool outputs.
|
|
5
|
+
* Understands output structure (git, npm, test runners, etc.)
|
|
6
|
+
* and strips noise while preserving signal.
|
|
7
|
+
*
|
|
8
|
+
* Runs as an AfterToolHook — transparent to agents.
|
|
9
|
+
* Inspired by rtk-ai/rtk.
|
|
10
|
+
*/
|
|
11
|
+
import type { AfterToolHook } from '@compilr-dev/agents';
|
|
12
|
+
export interface CompressorConfig {
|
|
13
|
+
/** Minimum output length to trigger compression (default: 500) */
|
|
14
|
+
minLength?: number;
|
|
15
|
+
/** Enable/disable specific compressors */
|
|
16
|
+
disabled?: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an AfterToolHook that compresses tool outputs.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const agent = createCompilrAgent({
|
|
24
|
+
* hooks: {
|
|
25
|
+
* afterTool: [createCompressorHook()],
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function createCompressorHook(config?: CompressorConfig): AfterToolHook;
|
|
31
|
+
export { compressBashOutput } from './bash.js';
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Output Compressors — Format-aware output compression
|
|
3
|
+
*
|
|
4
|
+
* Reduces token usage by 60-90% on common tool outputs.
|
|
5
|
+
* Understands output structure (git, npm, test runners, etc.)
|
|
6
|
+
* and strips noise while preserving signal.
|
|
7
|
+
*
|
|
8
|
+
* Runs as an AfterToolHook — transparent to agents.
|
|
9
|
+
* Inspired by rtk-ai/rtk.
|
|
10
|
+
*/
|
|
11
|
+
import { compressBashOutput } from './bash.js';
|
|
12
|
+
// ─── Hook Factory ───────────────────────────────────────────────────────────
|
|
13
|
+
/**
|
|
14
|
+
* Create an AfterToolHook that compresses tool outputs.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const agent = createCompilrAgent({
|
|
19
|
+
* hooks: {
|
|
20
|
+
* afterTool: [createCompressorHook()],
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createCompressorHook(config) {
|
|
26
|
+
const minLength = config?.minLength ?? 500;
|
|
27
|
+
return (ctx) => {
|
|
28
|
+
if (!ctx.result.success)
|
|
29
|
+
return undefined;
|
|
30
|
+
const result = ctx.result.result;
|
|
31
|
+
if (!result || typeof result !== 'object')
|
|
32
|
+
return undefined;
|
|
33
|
+
const toolName = ctx.toolName;
|
|
34
|
+
// ── Bash tool ────────────────────────────────────────────────────
|
|
35
|
+
if (toolName === 'bash') {
|
|
36
|
+
const bashResult = result;
|
|
37
|
+
const stdout = bashResult.stdout;
|
|
38
|
+
if (!stdout || stdout.length < minLength)
|
|
39
|
+
return undefined;
|
|
40
|
+
// Get the command from tool input
|
|
41
|
+
const command = ctx.input.command ?? '';
|
|
42
|
+
const compressed = compressBashOutput(command, stdout);
|
|
43
|
+
if (compressed !== null && compressed.length < stdout.length) {
|
|
44
|
+
return {
|
|
45
|
+
result: {
|
|
46
|
+
...ctx.result,
|
|
47
|
+
result: { ...bashResult, stdout: compressed },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ── Grep tool ────────────────────────────────────────────────────
|
|
53
|
+
if (toolName === 'grep') {
|
|
54
|
+
const grepResult = result;
|
|
55
|
+
const content = grepResult.content;
|
|
56
|
+
if (content && typeof content === 'string' && content.length >= minLength) {
|
|
57
|
+
const compressed = compressGrepOutput(content);
|
|
58
|
+
if (compressed.length < content.length) {
|
|
59
|
+
return {
|
|
60
|
+
result: {
|
|
61
|
+
...ctx.result,
|
|
62
|
+
result: { ...grepResult, content: compressed },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── Read file tool ───────────────────────────────────────────────
|
|
69
|
+
if (toolName === 'read_file') {
|
|
70
|
+
const fileResult = result;
|
|
71
|
+
const content = fileResult.content;
|
|
72
|
+
if (content && typeof content === 'string' && content.length >= minLength) {
|
|
73
|
+
const compressed = compressFileContent(content);
|
|
74
|
+
if (compressed.length < content.length) {
|
|
75
|
+
return {
|
|
76
|
+
result: {
|
|
77
|
+
...ctx.result,
|
|
78
|
+
result: { ...fileResult, content: compressed },
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// ─── Grep Compressor ────────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Compress grep output: deduplicate, cap matches per file.
|
|
90
|
+
*/
|
|
91
|
+
function compressGrepOutput(content) {
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
if (lines.length <= 50)
|
|
94
|
+
return content;
|
|
95
|
+
// Group by file
|
|
96
|
+
const groups = new Map();
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const colonIdx = line.indexOf(':');
|
|
99
|
+
if (colonIdx > 0) {
|
|
100
|
+
const file = line.substring(0, colonIdx);
|
|
101
|
+
const rest = line.substring(colonIdx + 1);
|
|
102
|
+
const arr = groups.get(file) ?? [];
|
|
103
|
+
arr.push(rest);
|
|
104
|
+
groups.set(file, arr);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Cap at 10 matches per file
|
|
108
|
+
const maxPerFile = 10;
|
|
109
|
+
const compressed = [];
|
|
110
|
+
for (const [file, matches] of groups) {
|
|
111
|
+
const shown = matches.slice(0, maxPerFile);
|
|
112
|
+
for (const m of shown) {
|
|
113
|
+
compressed.push(`${file}:${m}`);
|
|
114
|
+
}
|
|
115
|
+
if (matches.length > maxPerFile) {
|
|
116
|
+
compressed.push(` ... +${String(matches.length - maxPerFile)} more matches in ${file}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return compressed.join('\n');
|
|
120
|
+
}
|
|
121
|
+
// ─── File Content Compressor ────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Compress file content: collapse consecutive blank lines, large comment blocks.
|
|
124
|
+
*/
|
|
125
|
+
function compressFileContent(content) {
|
|
126
|
+
const lines = content.split('\n');
|
|
127
|
+
if (lines.length <= 100)
|
|
128
|
+
return content;
|
|
129
|
+
const result = [];
|
|
130
|
+
let consecutiveBlanks = 0;
|
|
131
|
+
let inBlockComment = false;
|
|
132
|
+
let blockCommentLines = 0;
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
// Collapse consecutive blank lines
|
|
136
|
+
if (trimmed === '') {
|
|
137
|
+
consecutiveBlanks++;
|
|
138
|
+
if (consecutiveBlanks <= 1)
|
|
139
|
+
result.push(line);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
consecutiveBlanks = 0;
|
|
143
|
+
// Collapse large block comments (keep first and last line)
|
|
144
|
+
if (trimmed.startsWith('/*') && !trimmed.endsWith('*/')) {
|
|
145
|
+
inBlockComment = true;
|
|
146
|
+
blockCommentLines = 0;
|
|
147
|
+
result.push(line);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (inBlockComment) {
|
|
151
|
+
blockCommentLines++;
|
|
152
|
+
if (trimmed.endsWith('*/') || trimmed === '*/') {
|
|
153
|
+
if (blockCommentLines > 5) {
|
|
154
|
+
result.push(` // ... ${String(blockCommentLines - 1)} comment lines omitted`);
|
|
155
|
+
}
|
|
156
|
+
result.push(line);
|
|
157
|
+
inBlockComment = false;
|
|
158
|
+
}
|
|
159
|
+
else if (blockCommentLines <= 3) {
|
|
160
|
+
result.push(line);
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
result.push(line);
|
|
165
|
+
}
|
|
166
|
+
return result.join('\n');
|
|
167
|
+
}
|
|
168
|
+
// ─── Re-exports ─────────────────────────────────────────────────────────────
|
|
169
|
+
export { compressBashOutput } from './bash.js';
|
package/dist/config.d.ts
CHANGED
|
@@ -245,6 +245,45 @@ export interface CompilrAgentConfig {
|
|
|
245
245
|
action: string;
|
|
246
246
|
reason?: string;
|
|
247
247
|
}) => void;
|
|
248
|
+
/**
|
|
249
|
+
* Event handler for monitoring agent execution.
|
|
250
|
+
* Called alongside the SDK's internal usage tracking — both run on every event.
|
|
251
|
+
*/
|
|
252
|
+
onEvent?: (event: AgentEvent) => void;
|
|
253
|
+
/**
|
|
254
|
+
* Enable file access tracking for context restoration hints.
|
|
255
|
+
* When enabled, the agent tracks which files were read, referenced, and modified.
|
|
256
|
+
* After context compaction, hints are injected to help the LLM understand
|
|
257
|
+
* what files it previously accessed.
|
|
258
|
+
* Default: true when contextManager is created (i.e., almost always).
|
|
259
|
+
*/
|
|
260
|
+
enableFileTracking?: boolean;
|
|
261
|
+
/**
|
|
262
|
+
* Pin/anchor configuration. Controls the AnchorManager for critical information
|
|
263
|
+
* that survives context compaction.
|
|
264
|
+
*/
|
|
265
|
+
pins?: {
|
|
266
|
+
/** Maximum number of pins. Default: 50 */
|
|
267
|
+
maxAnchors?: number;
|
|
268
|
+
/** Maximum total tokens for pins. Default: 4000 */
|
|
269
|
+
maxTokens?: number;
|
|
270
|
+
/** Include built-in safety pins. Default: true */
|
|
271
|
+
includeDefaults?: boolean;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Callback invoked when the agent reaches its iteration limit.
|
|
275
|
+
* Return a positive number to extend by that many iterations, or false to stop.
|
|
276
|
+
*/
|
|
277
|
+
onIterationLimitReached?: (context: {
|
|
278
|
+
iteration: number;
|
|
279
|
+
maxIterations: number;
|
|
280
|
+
toolCallCount: number;
|
|
281
|
+
}) => Promise<number | false>;
|
|
282
|
+
/**
|
|
283
|
+
* Behavior when max iterations is reached and no onIterationLimitReached callback
|
|
284
|
+
* is provided. Default: 'error'.
|
|
285
|
+
*/
|
|
286
|
+
iterationLimitBehavior?: 'error' | 'summarize' | 'continue';
|
|
248
287
|
/**
|
|
249
288
|
* Dynamic capability loading configuration.
|
|
250
289
|
* When enabled, tools are loaded on-demand for token efficiency.
|
package/dist/index.d.ts
CHANGED
|
@@ -79,3 +79,5 @@ export { readFileTool, writeFileTool, createBashTool, bashTool, bashOutputTool,
|
|
|
79
79
|
export { gitStatusTool, gitDiffTool, gitLogTool, gitCommitTool, gitBranchTool, gitStashTool, gitBlameTool, gitFileHistoryTool, detectProjectTool, findProjectRootTool, runTestsTool, runLintTool, runBuildTool, runFormatTool, findDefinitionTool, findReferencesTool, findTodosTool, checkOutdatedTool, findVulnerabilitiesTool, analyzeTestCoverageTool, getFileStructureTool, getComplexityTool, allCodingTools, unifiedTools, } from '@compilr-dev/agents-coding';
|
|
80
80
|
export { createLogger, createSilentLogger } from '@compilr-dev/logger';
|
|
81
81
|
export type { Logger, LoggerOptions, LogLevel } from '@compilr-dev/logger';
|
|
82
|
+
export { createCompressorHook, compressBashOutput } from './compressors/index.js';
|
|
83
|
+
export type { CompressorConfig } from './compressors/index.js';
|
package/dist/index.js
CHANGED
|
@@ -225,3 +225,7 @@ allCodingTools, unifiedTools, } from '@compilr-dev/agents-coding';
|
|
|
225
225
|
// Logger (re-export for consumers)
|
|
226
226
|
// =============================================================================
|
|
227
227
|
export { createLogger, createSilentLogger } from '@compilr-dev/logger';
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// Tool Output Compressors
|
|
230
|
+
// =============================================================================
|
|
231
|
+
export { createCompressorHook, compressBashOutput } from './compressors/index.js';
|