@aexol/spectral 0.9.58 → 0.9.60
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/CHANGELOG.md +6 -0
- package/README.md +43 -0
- package/dist/agent/index.d.ts +6 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +133 -14
- package/dist/extensions/kanban-bridge.d.ts.map +1 -1
- package/dist/extensions/kanban-bridge.js +21 -1
- package/dist/extensions/web/index.d.ts +10 -0
- package/dist/extensions/web/index.d.ts.map +1 -0
- package/dist/extensions/web/index.js +11 -0
- package/dist/extensions/web/tools/fetch.d.ts +3 -0
- package/dist/extensions/web/tools/fetch.d.ts.map +1 -0
- package/dist/extensions/web/tools/fetch.js +80 -0
- package/dist/extensions/web/utils/fetcher.d.ts +34 -0
- package/dist/extensions/web/utils/fetcher.d.ts.map +1 -0
- package/dist/extensions/web/utils/fetcher.js +289 -0
- package/dist/extensions/web/utils/parser.d.ts +15 -0
- package/dist/extensions/web/utils/parser.d.ts.map +1 -0
- package/dist/extensions/web/utils/parser.js +205 -0
- package/dist/memory/branch.d.ts.map +1 -1
- package/dist/memory/branch.js +16 -8
- package/dist/memory/commands/status.d.ts.map +1 -1
- package/dist/memory/commands/status.js +3 -2
- package/dist/memory/config.d.ts +12 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +21 -2
- package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-trigger.js +19 -3
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/testing/random-text.js +1 -1
- package/dist/memory/tool-output-compressor.d.ts +14 -20
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +391 -137
- package/dist/memory/tools/compact-context.d.ts +5 -0
- package/dist/memory/tools/compact-context.d.ts.map +1 -0
- package/dist/memory/tools/compact-context.js +772 -0
- package/dist/relay/dispatcher.d.ts +1 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +61 -1
- package/dist/sdk/coding-agent/core/agent-session.d.ts +56 -3
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +408 -210
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +57 -7
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/compaction.js +146 -27
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts +24 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.js +411 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts +2 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/index.js +2 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts +71 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/policy.js +147 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +9 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +17 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.d.ts +4 -10
- package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +17 -17
- package/dist/sdk/coding-agent/index.d.ts +1 -1
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/server/agent-bridge.d.ts +12 -5
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +65 -17
- package/dist/server/handlers/paths-home.d.ts +11 -0
- package/dist/server/handlers/paths-home.d.ts.map +1 -0
- package/dist/server/handlers/paths-home.js +10 -0
- package/dist/server/handlers/paths-list.d.ts +24 -0
- package/dist/server/handlers/paths-list.d.ts.map +1 -0
- package/dist/server/handlers/paths-list.js +56 -0
- package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
- package/dist/server/handlers/paths-pick-directory.js +2 -1
- package/dist/server/handlers/sessions.d.ts +11 -1
- package/dist/server/handlers/sessions.d.ts.map +1 -1
- package/dist/server/handlers/sessions.js +22 -2
- package/dist/server/handlers/settings.d.ts +18 -1
- package/dist/server/handlers/settings.d.ts.map +1 -1
- package/dist/server/handlers/settings.js +63 -13
- package/dist/server/session-stream.d.ts +65 -37
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +525 -119
- package/dist/server/storage.d.ts +3 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +25 -0
- package/dist/server/wire.d.ts +127 -2
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -28,17 +28,10 @@
|
|
|
28
28
|
* - Log output: deduplicate repeated lines
|
|
29
29
|
* - Docker/build output: collapse progress lines
|
|
30
30
|
*/
|
|
31
|
+
import { DEFAULT_COMPACTION_POLICY, } from "../sdk/coding-agent/core/compaction/policy.js";
|
|
31
32
|
import { debugLog } from "./debug-log.js";
|
|
32
33
|
export const DEFAULT_COMPRESSOR_CONFIG = {
|
|
33
|
-
|
|
34
|
-
maxResultChars: 8000,
|
|
35
|
-
maxReadLines: 500,
|
|
36
|
-
maxLsLines: 100,
|
|
37
|
-
maxSearchLines: 200,
|
|
38
|
-
stripAnsi: true,
|
|
39
|
-
stripComments: true,
|
|
40
|
-
testFailuresOnly: true,
|
|
41
|
-
compactGit: true,
|
|
34
|
+
...DEFAULT_COMPACTION_POLICY.toolOutputCompressor,
|
|
42
35
|
};
|
|
43
36
|
// ============================================================================
|
|
44
37
|
// ANSI Stripping
|
|
@@ -91,19 +84,41 @@ function stripComments(content, ext) {
|
|
|
91
84
|
const result = [];
|
|
92
85
|
let inBlockComment = false;
|
|
93
86
|
for (const line of lines) {
|
|
94
|
-
|
|
95
|
-
//
|
|
87
|
+
let processed = line;
|
|
88
|
+
// Remove only block-comment segments so inline code before/after survives.
|
|
96
89
|
if (patterns.blockStart && patterns.blockEnd) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
let remaining = processed;
|
|
91
|
+
let withoutBlocks = "";
|
|
92
|
+
while (remaining) {
|
|
93
|
+
if (inBlockComment) {
|
|
94
|
+
const endIdx = remaining.indexOf(patterns.blockEnd);
|
|
95
|
+
if (endIdx < 0) {
|
|
96
|
+
remaining = "";
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
remaining = remaining.slice(endIdx + patterns.blockEnd.length);
|
|
102
100
|
inBlockComment = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const startIdx = remaining.indexOf(patterns.blockStart);
|
|
104
|
+
if (startIdx < 0) {
|
|
105
|
+
withoutBlocks += remaining;
|
|
106
|
+
remaining = "";
|
|
107
|
+
break;
|
|
103
108
|
}
|
|
104
|
-
|
|
109
|
+
withoutBlocks += remaining.slice(0, startIdx);
|
|
110
|
+
remaining = remaining.slice(startIdx + patterns.blockStart.length);
|
|
111
|
+
const endIdx = remaining.indexOf(patterns.blockEnd);
|
|
112
|
+
if (endIdx < 0) {
|
|
113
|
+
inBlockComment = true;
|
|
114
|
+
remaining = "";
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
remaining = remaining.slice(endIdx + patterns.blockEnd.length);
|
|
105
118
|
}
|
|
119
|
+
processed = withoutBlocks;
|
|
106
120
|
}
|
|
121
|
+
const trimmed = processed.trim();
|
|
107
122
|
// Skip line comments
|
|
108
123
|
if (patterns.line && trimmed.startsWith(patterns.line)) {
|
|
109
124
|
continue;
|
|
@@ -112,7 +127,7 @@ function stripComments(content, ext) {
|
|
|
112
127
|
if (!trimmed)
|
|
113
128
|
continue;
|
|
114
129
|
// Remove trailing whitespace
|
|
115
|
-
result.push(
|
|
130
|
+
result.push(processed.replace(/\s+$/, ""));
|
|
116
131
|
}
|
|
117
132
|
return result.join("\n");
|
|
118
133
|
}
|
|
@@ -218,17 +233,24 @@ function compressReadOutput(event, config) {
|
|
|
218
233
|
// Bash Compressor
|
|
219
234
|
// ============================================================================
|
|
220
235
|
/** Commands whose output should be filtered to failures only */
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
236
|
+
const TEST_COMMAND_PATTERNS = [
|
|
237
|
+
/(?:^|[;&|()]\s*)(?:npx\s+|pnpm\s+(?:exec|dlx)\s+|yarn\s+(?:exec\s+)?|bunx\s+)?(?:jest|vitest|pytest|rspec)\b/,
|
|
238
|
+
/(?:^|[;&|()]\s*)(?:go|cargo)\s+test\b/,
|
|
239
|
+
/(?:^|[;&|()]\s*)rake\s+test\b/,
|
|
240
|
+
/(?:^|[;&|()]\s*)(?:npx\s+)?playwright\s+test\b/,
|
|
241
|
+
/(?:^|[;&|()]\s*)(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?test(?::[\w.-]+)?\b/,
|
|
242
|
+
/(?:^|[;&|()]\s*)deno\s+(?:task\s+)?test\b/,
|
|
226
243
|
];
|
|
227
244
|
/** Commands where git-like compacting should apply */
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
245
|
+
const GIT_COMMAND_PATTERNS = [
|
|
246
|
+
/(?:^|[;&|()]\s*)git\s+(?:status|diff|log|show|push|pull|fetch|add|commit|branch|stash)\b/,
|
|
247
|
+
];
|
|
248
|
+
/** Commands where repeated progress-line dedupe is especially useful */
|
|
249
|
+
const BUILD_COMMAND_PATTERNS = [
|
|
250
|
+
/(?:^|[;&|()]\s*)(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:build|compile)\b/,
|
|
251
|
+
/(?:^|[;&|()]\s*)(?:npx\s+)?(?:tsc|esbuild|webpack)\b/,
|
|
252
|
+
/(?:^|[;&|()]\s*)(?:npx\s+)?(?:vite|next)\s+build\b/,
|
|
253
|
+
/(?:^|[;&|()]\s*)deno\s+(?:task\s+)?(?:build|compile)\b/,
|
|
232
254
|
];
|
|
233
255
|
/** Patterns to strip from git output */
|
|
234
256
|
const GIT_NOISE_PATTERNS = [
|
|
@@ -241,19 +263,17 @@ const GIT_NOISE_PATTERNS = [
|
|
|
241
263
|
/^\(use "git/,
|
|
242
264
|
/^\(create\/copy files/,
|
|
243
265
|
];
|
|
266
|
+
function normalizeShellCommand(command) {
|
|
267
|
+
return command.toLowerCase().replace(/\s+/g, " ").trim();
|
|
268
|
+
}
|
|
244
269
|
function detectCommandType(command) {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (base.startsWith(gitCmd))
|
|
252
|
-
return "git";
|
|
253
|
-
}
|
|
254
|
-
if (/\b(build|compile|tsc|esbuild|webpack|vite build|next build)\b/.test(base)) {
|
|
270
|
+
const normalized = normalizeShellCommand(command);
|
|
271
|
+
if (TEST_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized)))
|
|
272
|
+
return "test";
|
|
273
|
+
if (GIT_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized)))
|
|
274
|
+
return "git";
|
|
275
|
+
if (BUILD_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized)))
|
|
255
276
|
return "build";
|
|
256
|
-
}
|
|
257
277
|
return "other";
|
|
258
278
|
}
|
|
259
279
|
function filterGitOutput(content) {
|
|
@@ -277,13 +297,19 @@ function filterTestOutput(content) {
|
|
|
277
297
|
const result = [];
|
|
278
298
|
let failureSection = false;
|
|
279
299
|
let summarySection = false;
|
|
300
|
+
let sawRecognizedSignal = false;
|
|
301
|
+
const failurePattern = /^\s*(?:FAIL\b|FAILED\b|AssertionError\b|Error:|●|❯|×|\d+\))/i;
|
|
302
|
+
const summaryPattern = /^\s*(?:Test Suites|Test Files|Tests|Snapshots|Time|Duration|Ran all|Ran):/i;
|
|
303
|
+
const vitestSummaryPattern = /^\s*(?:Test Files|Tests|Duration)\s+\d|^\s*(?:Start at)\b/i;
|
|
280
304
|
for (const line of lines) {
|
|
281
305
|
// Start of failure section
|
|
282
|
-
if (
|
|
306
|
+
if (failurePattern.test(line)) {
|
|
307
|
+
sawRecognizedSignal = true;
|
|
283
308
|
failureSection = true;
|
|
284
309
|
}
|
|
285
310
|
// Start of summary section
|
|
286
|
-
if (
|
|
311
|
+
if (summaryPattern.test(line) || vitestSummaryPattern.test(line)) {
|
|
312
|
+
sawRecognizedSignal = true;
|
|
287
313
|
summarySection = true;
|
|
288
314
|
failureSection = false;
|
|
289
315
|
}
|
|
@@ -292,16 +318,162 @@ function filterTestOutput(content) {
|
|
|
292
318
|
result.push(line);
|
|
293
319
|
}
|
|
294
320
|
}
|
|
295
|
-
// If no failures found,
|
|
321
|
+
// If no failures found, keep only recognized summaries. If the output has no
|
|
322
|
+
// known failure/summary signal, preserve it rather than inventing success.
|
|
296
323
|
if (result.length === 0) {
|
|
297
|
-
const summaryLines = lines.filter((l) =>
|
|
324
|
+
const summaryLines = lines.filter((l) => summaryPattern.test(l) || vitestSummaryPattern.test(l));
|
|
298
325
|
if (summaryLines.length > 0) {
|
|
299
326
|
return summaryLines.join("\n");
|
|
300
327
|
}
|
|
301
|
-
return "
|
|
328
|
+
return sawRecognizedSignal ? "No recognized test failure details found." : content;
|
|
302
329
|
}
|
|
303
330
|
return result.join("\n");
|
|
304
331
|
}
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// Structured Output Summarization
|
|
334
|
+
// ============================================================================
|
|
335
|
+
const STRUCTURED_SUMMARY_MIN_CHARS = 500;
|
|
336
|
+
const STRUCTURED_SUMMARY_MAX_KEYS = 10;
|
|
337
|
+
const STRUCTURED_SUMMARY_MAX_FIELDS = 8;
|
|
338
|
+
const STRUCTURED_SUMMARY_MAX_RECORDS_TO_INSPECT = 200;
|
|
339
|
+
const SAFE_LABEL_MAX_CHARS = 48;
|
|
340
|
+
function isRecord(value) {
|
|
341
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
342
|
+
}
|
|
343
|
+
function safeLabel(label) {
|
|
344
|
+
if (label.length <= SAFE_LABEL_MAX_CHARS)
|
|
345
|
+
return label;
|
|
346
|
+
return `${label.slice(0, SAFE_LABEL_MAX_CHARS - 1)}…`;
|
|
347
|
+
}
|
|
348
|
+
function describeJsonType(value) {
|
|
349
|
+
if (value === null)
|
|
350
|
+
return "null";
|
|
351
|
+
if (Array.isArray(value))
|
|
352
|
+
return `array(${value.length})`;
|
|
353
|
+
if (isRecord(value))
|
|
354
|
+
return `object(${Object.keys(value).length} keys)`;
|
|
355
|
+
return typeof value;
|
|
356
|
+
}
|
|
357
|
+
function summarizeKeyCounts(records) {
|
|
358
|
+
const keyCounts = new Map();
|
|
359
|
+
for (const record of records.slice(0, STRUCTURED_SUMMARY_MAX_RECORDS_TO_INSPECT)) {
|
|
360
|
+
for (const key of Object.keys(record)) {
|
|
361
|
+
keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return [...keyCounts.entries()]
|
|
365
|
+
.sort(([leftKey, leftCount], [rightKey, rightCount]) => rightCount - leftCount || leftKey.localeCompare(rightKey))
|
|
366
|
+
.slice(0, STRUCTURED_SUMMARY_MAX_KEYS)
|
|
367
|
+
.map(([key, count]) => `${safeLabel(key)}(${count})`)
|
|
368
|
+
.join(", ");
|
|
369
|
+
}
|
|
370
|
+
function summarizeRecordShape(record) {
|
|
371
|
+
return Object.keys(record)
|
|
372
|
+
.sort((left, right) => left.localeCompare(right))
|
|
373
|
+
.slice(0, STRUCTURED_SUMMARY_MAX_FIELDS)
|
|
374
|
+
.map((key) => `${safeLabel(key)}:${describeJsonType(record[key])}`)
|
|
375
|
+
.join(", ");
|
|
376
|
+
}
|
|
377
|
+
function summarizeJsonValue(value) {
|
|
378
|
+
if (Array.isArray(value)) {
|
|
379
|
+
const itemTypes = new Map();
|
|
380
|
+
const records = [];
|
|
381
|
+
for (const item of value) {
|
|
382
|
+
const type = describeJsonType(item);
|
|
383
|
+
itemTypes.set(type, (itemTypes.get(type) ?? 0) + 1);
|
|
384
|
+
if (isRecord(item))
|
|
385
|
+
records.push(item);
|
|
386
|
+
}
|
|
387
|
+
const lines = [
|
|
388
|
+
"JSON summary:",
|
|
389
|
+
"- type: array",
|
|
390
|
+
`- items: ${value.length}`,
|
|
391
|
+
`- item types: ${[...itemTypes.entries()].map(([type, count]) => `${type}=${count}`).join(", ")}`,
|
|
392
|
+
];
|
|
393
|
+
if (records.length > 0) {
|
|
394
|
+
const inspected = Math.min(records.length, STRUCTURED_SUMMARY_MAX_RECORDS_TO_INSPECT);
|
|
395
|
+
lines.push(`- object keys inspected: ${inspected}/${records.length}`);
|
|
396
|
+
const keyCounts = summarizeKeyCounts(records);
|
|
397
|
+
if (keyCounts)
|
|
398
|
+
lines.push(`- top keys: ${keyCounts}`);
|
|
399
|
+
lines.push(`- sample shape: ${summarizeRecordShape(records[0])}`);
|
|
400
|
+
}
|
|
401
|
+
return lines.join("\n");
|
|
402
|
+
}
|
|
403
|
+
if (isRecord(value)) {
|
|
404
|
+
const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
|
|
405
|
+
const lines = [
|
|
406
|
+
"JSON summary:",
|
|
407
|
+
"- type: object",
|
|
408
|
+
`- keys: ${keys.length}`,
|
|
409
|
+
`- key preview: ${keys.slice(0, STRUCTURED_SUMMARY_MAX_KEYS).map(safeLabel).join(", ")}${keys.length > STRUCTURED_SUMMARY_MAX_KEYS ? `, … +${keys.length - STRUCTURED_SUMMARY_MAX_KEYS} more` : ""}`,
|
|
410
|
+
`- field types: ${summarizeRecordShape(value)}`,
|
|
411
|
+
];
|
|
412
|
+
return lines.join("\n");
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
function tryParseJson(input) {
|
|
417
|
+
try {
|
|
418
|
+
return JSON.parse(input);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function summarizeNdjson(trimmed) {
|
|
425
|
+
const lines = trimmed.split("\n").filter((line) => line.trim());
|
|
426
|
+
if (lines.length < 2)
|
|
427
|
+
return undefined;
|
|
428
|
+
const parsedRecords = [];
|
|
429
|
+
for (let index = 0; index < lines.length; index++) {
|
|
430
|
+
const line = lines[index];
|
|
431
|
+
const parsed = tryParseJson(line);
|
|
432
|
+
if (parsed === undefined)
|
|
433
|
+
return undefined;
|
|
434
|
+
if (index < STRUCTURED_SUMMARY_MAX_RECORDS_TO_INSPECT) {
|
|
435
|
+
parsedRecords.push(parsed);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const typeCounts = new Map();
|
|
439
|
+
const records = [];
|
|
440
|
+
for (const record of parsedRecords) {
|
|
441
|
+
const type = describeJsonType(record);
|
|
442
|
+
typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1);
|
|
443
|
+
if (isRecord(record))
|
|
444
|
+
records.push(record);
|
|
445
|
+
}
|
|
446
|
+
const linesOut = [
|
|
447
|
+
"NDJSON summary:",
|
|
448
|
+
`- records: ${lines.length}`,
|
|
449
|
+
`- inspected: ${parsedRecords.length}`,
|
|
450
|
+
`- record types: ${[...typeCounts.entries()].map(([type, count]) => `${type}=${count}`).join(", ")}`,
|
|
451
|
+
];
|
|
452
|
+
if (records.length > 0) {
|
|
453
|
+
const keyCounts = summarizeKeyCounts(records);
|
|
454
|
+
if (keyCounts)
|
|
455
|
+
linesOut.push(`- top keys: ${keyCounts}`);
|
|
456
|
+
linesOut.push(`- sample shape: ${summarizeRecordShape(records[0])}`);
|
|
457
|
+
}
|
|
458
|
+
return linesOut.join("\n");
|
|
459
|
+
}
|
|
460
|
+
function summarizeStructuredOutput(content) {
|
|
461
|
+
const trimmed = content.trim();
|
|
462
|
+
if (trimmed.length < STRUCTURED_SUMMARY_MIN_CHARS)
|
|
463
|
+
return undefined;
|
|
464
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
465
|
+
const parsed = tryParseJson(trimmed);
|
|
466
|
+
if (parsed !== undefined) {
|
|
467
|
+
const summary = summarizeJsonValue(parsed);
|
|
468
|
+
if (summary && summary.length < content.length)
|
|
469
|
+
return summary;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const ndjsonSummary = summarizeNdjson(trimmed);
|
|
473
|
+
if (ndjsonSummary && ndjsonSummary.length < content.length)
|
|
474
|
+
return ndjsonSummary;
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
305
477
|
function compressBashOutput(event, config) {
|
|
306
478
|
let content = event.content
|
|
307
479
|
.filter((c) => c.type === "text")
|
|
@@ -335,6 +507,10 @@ function compressBashOutput(event, config) {
|
|
|
335
507
|
}
|
|
336
508
|
break;
|
|
337
509
|
}
|
|
510
|
+
const structuredSummary = summarizeStructuredOutput(content);
|
|
511
|
+
if (structuredSummary) {
|
|
512
|
+
content = structuredSummary;
|
|
513
|
+
}
|
|
338
514
|
// Deduplicate repeated lines for all outputs
|
|
339
515
|
{
|
|
340
516
|
const deduped = deduplicateRepeatedLines(content);
|
|
@@ -348,6 +524,101 @@ function compressBashOutput(event, config) {
|
|
|
348
524
|
}
|
|
349
525
|
return content;
|
|
350
526
|
}
|
|
527
|
+
const PATH_TREE_MAX_DEPTH = 3;
|
|
528
|
+
const PATH_TREE_MAX_CHILDREN = 8;
|
|
529
|
+
const PATH_TREE_MAX_FILE_PREVIEW = 4;
|
|
530
|
+
function createPathTreeNode() {
|
|
531
|
+
return { totalPaths: 0, files: [], children: new Map() };
|
|
532
|
+
}
|
|
533
|
+
function comparePathPart(left, right) {
|
|
534
|
+
return left.localeCompare(right, undefined, { numeric: true });
|
|
535
|
+
}
|
|
536
|
+
function normalizeListedPath(path) {
|
|
537
|
+
let normalized = path.trim();
|
|
538
|
+
while (normalized.startsWith("./"))
|
|
539
|
+
normalized = normalized.slice(2);
|
|
540
|
+
return normalized.replace(/\/+$/, "");
|
|
541
|
+
}
|
|
542
|
+
function addPathToTree(root, path) {
|
|
543
|
+
const normalized = normalizeListedPath(path);
|
|
544
|
+
if (!normalized)
|
|
545
|
+
return;
|
|
546
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
547
|
+
if (parts.length === 0)
|
|
548
|
+
return;
|
|
549
|
+
root.totalPaths++;
|
|
550
|
+
let node = root;
|
|
551
|
+
for (let index = 0; index < parts.length; index++) {
|
|
552
|
+
const part = parts[index];
|
|
553
|
+
const isLeaf = index === parts.length - 1;
|
|
554
|
+
if (isLeaf) {
|
|
555
|
+
node.files.push(part);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
let child = node.children.get(part);
|
|
559
|
+
if (!child) {
|
|
560
|
+
child = createPathTreeNode();
|
|
561
|
+
node.children.set(part, child);
|
|
562
|
+
}
|
|
563
|
+
child.totalPaths++;
|
|
564
|
+
node = child;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function formatPreview(items, omittedLabel) {
|
|
568
|
+
const sorted = [...items].sort(comparePathPart);
|
|
569
|
+
const preview = sorted.slice(0, PATH_TREE_MAX_FILE_PREVIEW).map(safeLabel).join(", ");
|
|
570
|
+
const omitted = sorted.length - PATH_TREE_MAX_FILE_PREVIEW;
|
|
571
|
+
if (omitted <= 0)
|
|
572
|
+
return preview;
|
|
573
|
+
return `${preview}, … +${omitted} ${omittedLabel}`;
|
|
574
|
+
}
|
|
575
|
+
function renderPathTreeNode(name, node, depth, lines) {
|
|
576
|
+
const indent = " ".repeat(depth - 1);
|
|
577
|
+
const fileSuffix = node.files.length > 0 ? `, ${node.files.length} files` : "";
|
|
578
|
+
lines.push(`${indent}${name}/ (${node.totalPaths} paths${fileSuffix})`);
|
|
579
|
+
const childEntries = [...node.children.entries()].sort(([left], [right]) => comparePathPart(left, right));
|
|
580
|
+
if (depth >= PATH_TREE_MAX_DEPTH) {
|
|
581
|
+
if (childEntries.length > 0) {
|
|
582
|
+
const childPreview = childEntries
|
|
583
|
+
.slice(0, PATH_TREE_MAX_FILE_PREVIEW)
|
|
584
|
+
.map(([childName, child]) => `${safeLabel(childName)}/(${child.totalPaths})`);
|
|
585
|
+
const omitted = childEntries.length - PATH_TREE_MAX_FILE_PREVIEW;
|
|
586
|
+
lines.push(`${indent} dirs: ${childPreview.join(", ")}${omitted > 0 ? `, … +${omitted} more` : ""}`);
|
|
587
|
+
}
|
|
588
|
+
if (node.files.length > 0)
|
|
589
|
+
lines.push(`${indent} files: ${formatPreview(node.files, "more")}`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
for (const [childName, child] of childEntries.slice(0, PATH_TREE_MAX_CHILDREN)) {
|
|
593
|
+
renderPathTreeNode(childName, child, depth + 1, lines);
|
|
594
|
+
}
|
|
595
|
+
const omittedChildren = childEntries.length - PATH_TREE_MAX_CHILDREN;
|
|
596
|
+
if (omittedChildren > 0) {
|
|
597
|
+
lines.push(`${indent} … +${omittedChildren} more dirs`);
|
|
598
|
+
}
|
|
599
|
+
if (node.files.length > 0)
|
|
600
|
+
lines.push(`${indent} files: ${formatPreview(node.files, "more")}`);
|
|
601
|
+
}
|
|
602
|
+
function summarizePathTree(content) {
|
|
603
|
+
const paths = content.split("\n").map(normalizeListedPath).filter(Boolean);
|
|
604
|
+
if (paths.length === 0)
|
|
605
|
+
return undefined;
|
|
606
|
+
const root = createPathTreeNode();
|
|
607
|
+
for (const path of paths)
|
|
608
|
+
addPathToTree(root, path);
|
|
609
|
+
const lines = [`${root.totalPaths} paths grouped by directory:`];
|
|
610
|
+
const topEntries = [...root.children.entries()].sort(([left], [right]) => comparePathPart(left, right));
|
|
611
|
+
for (const [name, node] of topEntries.slice(0, PATH_TREE_MAX_CHILDREN)) {
|
|
612
|
+
renderPathTreeNode(name, node, 1, lines);
|
|
613
|
+
}
|
|
614
|
+
const omittedTop = topEntries.length - PATH_TREE_MAX_CHILDREN;
|
|
615
|
+
if (omittedTop > 0)
|
|
616
|
+
lines.push(`… +${omittedTop} more top-level dirs`);
|
|
617
|
+
if (root.files.length > 0)
|
|
618
|
+
lines.push(`./ (${root.files.length} files: ${formatPreview(root.files, "more")})`);
|
|
619
|
+
const summary = lines.join("\n");
|
|
620
|
+
return summary.length < content.length ? summary : undefined;
|
|
621
|
+
}
|
|
351
622
|
// ============================================================================
|
|
352
623
|
// Ls Compressor
|
|
353
624
|
// ============================================================================
|
|
@@ -363,21 +634,7 @@ function compressLsOutput(event, config) {
|
|
|
363
634
|
}
|
|
364
635
|
const lines = content.split("\n");
|
|
365
636
|
if (lines.length > config.maxLsLines) {
|
|
366
|
-
|
|
367
|
-
const groups = new Map();
|
|
368
|
-
for (const line of lines) {
|
|
369
|
-
const trimmed = line.trim();
|
|
370
|
-
if (!trimmed)
|
|
371
|
-
continue;
|
|
372
|
-
const slashIdx = trimmed.lastIndexOf("/");
|
|
373
|
-
const dir = slashIdx >= 0 ? trimmed.slice(0, slashIdx) : ".";
|
|
374
|
-
groups.set(dir, (groups.get(dir) ?? 0) + 1);
|
|
375
|
-
}
|
|
376
|
-
const grouped = [];
|
|
377
|
-
for (const [dir, count] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
378
|
-
grouped.push(`${dir}/ (${count} files)`);
|
|
379
|
-
}
|
|
380
|
-
return grouped.join("\n");
|
|
637
|
+
return summarizePathTree(content) ?? content;
|
|
381
638
|
}
|
|
382
639
|
return content;
|
|
383
640
|
}
|
|
@@ -436,36 +693,82 @@ function compressFindOutput(event, config) {
|
|
|
436
693
|
// Group by directory
|
|
437
694
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
438
695
|
if (lines.length > config.maxSearchLines) {
|
|
439
|
-
|
|
440
|
-
for (const line of lines) {
|
|
441
|
-
const trimmed = line.trim();
|
|
442
|
-
if (!trimmed)
|
|
443
|
-
continue;
|
|
444
|
-
const slashIdx = trimmed.lastIndexOf("/");
|
|
445
|
-
const dir = slashIdx >= 0 ? trimmed.slice(0, slashIdx) : ".";
|
|
446
|
-
if (!groups.has(dir))
|
|
447
|
-
groups.set(dir, []);
|
|
448
|
-
groups.get(dir).push(slashIdx >= 0 ? trimmed.slice(slashIdx + 1) : trimmed);
|
|
449
|
-
}
|
|
450
|
-
const grouped = [];
|
|
451
|
-
for (const [dir, files] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
452
|
-
if (files.length === 1) {
|
|
453
|
-
grouped.push(`${dir}/${files[0]}`);
|
|
454
|
-
}
|
|
455
|
-
else if (files.length <= 5) {
|
|
456
|
-
grouped.push(`${dir}/ (${files.length} files: ${files.join(", ")})`);
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
grouped.push(`${dir}/ (${files.length} files: ${files.slice(0, 3).join(", ")}... +${files.length - 3} more)`);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return grouped.join("\n");
|
|
696
|
+
return summarizePathTree(content) ?? content;
|
|
463
697
|
}
|
|
464
698
|
return content;
|
|
465
699
|
}
|
|
700
|
+
const statsByRuntime = new WeakMap();
|
|
701
|
+
function createTelemetry() {
|
|
702
|
+
return { events: 0, originalChars: 0, compressedChars: 0, byTool: {} };
|
|
703
|
+
}
|
|
704
|
+
function cloneTelemetry(stats) {
|
|
705
|
+
const byTool = {};
|
|
706
|
+
for (const [toolName, toolStats] of Object.entries(stats.byTool)) {
|
|
707
|
+
byTool[toolName] = { ...toolStats };
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
events: stats.events,
|
|
711
|
+
originalChars: stats.originalChars,
|
|
712
|
+
compressedChars: stats.compressedChars,
|
|
713
|
+
byTool,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function getMutableTelemetry(runtime) {
|
|
717
|
+
let stats = statsByRuntime.get(runtime);
|
|
718
|
+
if (!stats) {
|
|
719
|
+
stats = createTelemetry();
|
|
720
|
+
statsByRuntime.set(runtime, stats);
|
|
721
|
+
}
|
|
722
|
+
return stats;
|
|
723
|
+
}
|
|
724
|
+
export function getToolOutputCompressionTelemetry(runtime) {
|
|
725
|
+
return cloneTelemetry(getMutableTelemetry(runtime));
|
|
726
|
+
}
|
|
727
|
+
export function resetToolOutputCompressionTelemetry(runtime) {
|
|
728
|
+
statsByRuntime.set(runtime, createTelemetry());
|
|
729
|
+
}
|
|
730
|
+
function recordCompression(runtime, toolName, originalChars, compressedChars) {
|
|
731
|
+
const stats = getMutableTelemetry(runtime);
|
|
732
|
+
stats.events++;
|
|
733
|
+
stats.originalChars += originalChars;
|
|
734
|
+
stats.compressedChars += compressedChars;
|
|
735
|
+
const toolStats = stats.byTool[toolName] ?? { events: 0, originalChars: 0, compressedChars: 0 };
|
|
736
|
+
toolStats.events++;
|
|
737
|
+
toolStats.originalChars += originalChars;
|
|
738
|
+
toolStats.compressedChars += compressedChars;
|
|
739
|
+
stats.byTool[toolName] = toolStats;
|
|
740
|
+
return {
|
|
741
|
+
toolName,
|
|
742
|
+
originalChars,
|
|
743
|
+
compressedChars,
|
|
744
|
+
savingsPercent: Math.round((1 - compressedChars / Math.max(1, originalChars)) * 10_000) / 100,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function formatCompressionHeader(stats) {
|
|
748
|
+
return `[tool-output-compressor tool=${stats.toolName} originalChars=${stats.originalChars} bodyChars=${stats.compressedChars} savings=${stats.savingsPercent}%]`;
|
|
749
|
+
}
|
|
750
|
+
function resolveCompressorConfig(ctx) {
|
|
751
|
+
const manager = ctx.settingsManager;
|
|
752
|
+
return manager?.getCompactionPolicy?.().toolOutputCompressor ?? DEFAULT_COMPRESSOR_CONFIG;
|
|
753
|
+
}
|
|
754
|
+
function compressedContentResult(runtime, event, toolName, original, compressed) {
|
|
755
|
+
if (!compressed || compressed === original)
|
|
756
|
+
return undefined;
|
|
757
|
+
const stats = recordCompression(runtime, toolName, original.length, compressed.length);
|
|
758
|
+
debugLog("compressor.event", {
|
|
759
|
+
toolName: stats.toolName,
|
|
760
|
+
originalChars: stats.originalChars,
|
|
761
|
+
compressedChars: stats.compressedChars,
|
|
762
|
+
savingsPercent: stats.savingsPercent,
|
|
763
|
+
});
|
|
764
|
+
const replacementText = `${formatCompressionHeader(stats)}\n${compressed}`;
|
|
765
|
+
return {
|
|
766
|
+
content: [...event.content.filter((c) => c.type !== "text"), { type: "text", text: replacementText }],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
466
769
|
export function registerToolOutputCompressor(ext, runtime) {
|
|
467
|
-
const config = { ...DEFAULT_COMPRESSOR_CONFIG };
|
|
468
770
|
ext.on("tool_result", (_event, _ctx) => {
|
|
771
|
+
const config = resolveCompressorConfig(_ctx);
|
|
469
772
|
if (!config.enabled)
|
|
470
773
|
return;
|
|
471
774
|
// Only apply compression for known tool types
|
|
@@ -476,17 +779,7 @@ export function registerToolOutputCompressor(ext, runtime) {
|
|
|
476
779
|
.map((c) => c.text)
|
|
477
780
|
.join("\n");
|
|
478
781
|
const compressed = compressReadOutput(event, config);
|
|
479
|
-
|
|
480
|
-
debugLog("compressor.read", {
|
|
481
|
-
originalLen: original.length,
|
|
482
|
-
compressedLen: compressed.length,
|
|
483
|
-
savings: ((1 - compressed.length / Math.max(1, original.length)) * 100).toFixed(1) + "%",
|
|
484
|
-
});
|
|
485
|
-
return {
|
|
486
|
-
content: [...event.content.filter((c) => c.type !== "text"), { type: "text", text: compressed }],
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
return;
|
|
782
|
+
return compressedContentResult(runtime, event, "read", original, compressed);
|
|
490
783
|
}
|
|
491
784
|
if (_event.toolName === "bash") {
|
|
492
785
|
const event = _event;
|
|
@@ -495,19 +788,7 @@ export function registerToolOutputCompressor(ext, runtime) {
|
|
|
495
788
|
.map((c) => c.text)
|
|
496
789
|
.join("\n");
|
|
497
790
|
const compressed = compressBashOutput(event, config);
|
|
498
|
-
|
|
499
|
-
const command = event.input?.command ?? "";
|
|
500
|
-
debugLog("compressor.bash", {
|
|
501
|
-
command: command.slice(0, 80),
|
|
502
|
-
originalLen: original.length,
|
|
503
|
-
compressedLen: compressed.length,
|
|
504
|
-
savings: ((1 - compressed.length / Math.max(1, original.length)) * 100).toFixed(1) + "%",
|
|
505
|
-
});
|
|
506
|
-
return {
|
|
507
|
-
content: [...event.content.filter((c) => c.type !== "text"), { type: "text", text: compressed }],
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
return;
|
|
791
|
+
return compressedContentResult(runtime, event, "bash", original, compressed);
|
|
511
792
|
}
|
|
512
793
|
if (_event.toolName === "ls") {
|
|
513
794
|
const event = _event;
|
|
@@ -516,16 +797,7 @@ export function registerToolOutputCompressor(ext, runtime) {
|
|
|
516
797
|
.map((c) => c.text)
|
|
517
798
|
.join("\n");
|
|
518
799
|
const compressed = compressLsOutput(event, config);
|
|
519
|
-
|
|
520
|
-
debugLog("compressor.ls", {
|
|
521
|
-
originalLen: original.length,
|
|
522
|
-
compressedLen: compressed.length,
|
|
523
|
-
});
|
|
524
|
-
return {
|
|
525
|
-
content: [...event.content.filter((c) => c.type !== "text"), { type: "text", text: compressed }],
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
return;
|
|
800
|
+
return compressedContentResult(runtime, event, "ls", original, compressed);
|
|
529
801
|
}
|
|
530
802
|
if (_event.toolName === "grep") {
|
|
531
803
|
const event = _event;
|
|
@@ -534,16 +806,7 @@ export function registerToolOutputCompressor(ext, runtime) {
|
|
|
534
806
|
.map((c) => c.text)
|
|
535
807
|
.join("\n");
|
|
536
808
|
const compressed = compressGrepOutput(event, config);
|
|
537
|
-
|
|
538
|
-
debugLog("compressor.grep", {
|
|
539
|
-
originalLen: original.length,
|
|
540
|
-
compressedLen: compressed.length,
|
|
541
|
-
});
|
|
542
|
-
return {
|
|
543
|
-
content: [...event.content.filter((c) => c.type !== "text"), { type: "text", text: compressed }],
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
return;
|
|
809
|
+
return compressedContentResult(runtime, event, "grep", original, compressed);
|
|
547
810
|
}
|
|
548
811
|
if (_event.toolName === "find") {
|
|
549
812
|
const event = _event;
|
|
@@ -552,16 +815,7 @@ export function registerToolOutputCompressor(ext, runtime) {
|
|
|
552
815
|
.map((c) => c.text)
|
|
553
816
|
.join("\n");
|
|
554
817
|
const compressed = compressFindOutput(event, config);
|
|
555
|
-
|
|
556
|
-
debugLog("compressor.find", {
|
|
557
|
-
originalLen: original.length,
|
|
558
|
-
compressedLen: compressed.length,
|
|
559
|
-
});
|
|
560
|
-
return {
|
|
561
|
-
content: [...event.content.filter((c) => c.type !== "text"), { type: "text", text: compressed }],
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
return;
|
|
818
|
+
return compressedContentResult(runtime, event, "find", original, compressed);
|
|
565
819
|
}
|
|
566
820
|
// Unknown tool types: do nothing
|
|
567
821
|
return;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ExtensionAPI } from "../../sdk/coding-agent/index.js";
|
|
2
|
+
import type { Runtime } from "../runtime.js";
|
|
3
|
+
export declare const COMPACT_CONTEXT_TOOL_NAME = "compact_context";
|
|
4
|
+
export declare function registerCompactContextTool(ext: ExtensionAPI, runtime: Runtime): void;
|
|
5
|
+
//# sourceMappingURL=compact-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-context.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/compact-context.ts"],"names":[],"mappings":"AAYA,OAAO,EAAc,KAAK,YAAY,EAAyB,MAAM,iCAAiC,CAAC;AAIvG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAO,MAAM,yBAAyB,oBAAoB,CAAC;AAkmB3D,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAmQpF"}
|