@aexol/spectral 0.9.58 → 0.9.59

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +43 -0
  3. package/dist/agent/index.d.ts +6 -1
  4. package/dist/agent/index.d.ts.map +1 -1
  5. package/dist/agent/index.js +133 -14
  6. package/dist/extensions/web/index.d.ts +10 -0
  7. package/dist/extensions/web/index.d.ts.map +1 -0
  8. package/dist/extensions/web/index.js +11 -0
  9. package/dist/extensions/web/tools/fetch.d.ts +3 -0
  10. package/dist/extensions/web/tools/fetch.d.ts.map +1 -0
  11. package/dist/extensions/web/tools/fetch.js +80 -0
  12. package/dist/extensions/web/utils/fetcher.d.ts +34 -0
  13. package/dist/extensions/web/utils/fetcher.d.ts.map +1 -0
  14. package/dist/extensions/web/utils/fetcher.js +289 -0
  15. package/dist/extensions/web/utils/parser.d.ts +15 -0
  16. package/dist/extensions/web/utils/parser.d.ts.map +1 -0
  17. package/dist/extensions/web/utils/parser.js +205 -0
  18. package/dist/memory/branch.d.ts.map +1 -1
  19. package/dist/memory/branch.js +16 -8
  20. package/dist/memory/commands/status.d.ts.map +1 -1
  21. package/dist/memory/commands/status.js +3 -2
  22. package/dist/memory/config.d.ts +12 -0
  23. package/dist/memory/config.d.ts.map +1 -1
  24. package/dist/memory/config.js +21 -2
  25. package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
  26. package/dist/memory/hooks/compaction-trigger.js +19 -3
  27. package/dist/memory/index.d.ts.map +1 -1
  28. package/dist/memory/index.js +2 -0
  29. package/dist/memory/tool-output-compressor.d.ts +14 -20
  30. package/dist/memory/tool-output-compressor.d.ts.map +1 -1
  31. package/dist/memory/tool-output-compressor.js +391 -137
  32. package/dist/memory/tools/compact-context.d.ts +5 -0
  33. package/dist/memory/tools/compact-context.d.ts.map +1 -0
  34. package/dist/memory/tools/compact-context.js +772 -0
  35. package/dist/relay/dispatcher.d.ts +1 -1
  36. package/dist/relay/dispatcher.d.ts.map +1 -1
  37. package/dist/relay/dispatcher.js +61 -1
  38. package/dist/sdk/coding-agent/core/agent-session.d.ts +56 -3
  39. package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
  40. package/dist/sdk/coding-agent/core/agent-session.js +408 -210
  41. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +57 -7
  42. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
  43. package/dist/sdk/coding-agent/core/compaction/compaction.js +146 -27
  44. package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts +24 -0
  45. package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts.map +1 -0
  46. package/dist/sdk/coding-agent/core/compaction/dcp-lite.js +411 -0
  47. package/dist/sdk/coding-agent/core/compaction/index.d.ts +2 -0
  48. package/dist/sdk/coding-agent/core/compaction/index.d.ts.map +1 -1
  49. package/dist/sdk/coding-agent/core/compaction/index.js +2 -0
  50. package/dist/sdk/coding-agent/core/compaction/policy.d.ts +71 -0
  51. package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -0
  52. package/dist/sdk/coding-agent/core/compaction/policy.js +147 -0
  53. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  54. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +9 -0
  55. package/dist/sdk/coding-agent/core/extensions/types.d.ts +17 -0
  56. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  57. package/dist/sdk/coding-agent/core/settings-manager.d.ts +4 -10
  58. package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
  59. package/dist/sdk/coding-agent/core/settings-manager.js +17 -17
  60. package/dist/sdk/coding-agent/index.d.ts +1 -1
  61. package/dist/sdk/coding-agent/index.d.ts.map +1 -1
  62. package/dist/server/agent-bridge.d.ts +12 -5
  63. package/dist/server/agent-bridge.d.ts.map +1 -1
  64. package/dist/server/agent-bridge.js +65 -17
  65. package/dist/server/handlers/paths-home.d.ts +11 -0
  66. package/dist/server/handlers/paths-home.d.ts.map +1 -0
  67. package/dist/server/handlers/paths-home.js +10 -0
  68. package/dist/server/handlers/paths-list.d.ts +24 -0
  69. package/dist/server/handlers/paths-list.d.ts.map +1 -0
  70. package/dist/server/handlers/paths-list.js +56 -0
  71. package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
  72. package/dist/server/handlers/paths-pick-directory.js +2 -1
  73. package/dist/server/handlers/sessions.d.ts +11 -1
  74. package/dist/server/handlers/sessions.d.ts.map +1 -1
  75. package/dist/server/handlers/sessions.js +22 -2
  76. package/dist/server/handlers/settings.d.ts +18 -1
  77. package/dist/server/handlers/settings.d.ts.map +1 -1
  78. package/dist/server/handlers/settings.js +63 -13
  79. package/dist/server/session-stream.d.ts +65 -37
  80. package/dist/server/session-stream.d.ts.map +1 -1
  81. package/dist/server/session-stream.js +525 -119
  82. package/dist/server/storage.d.ts +3 -0
  83. package/dist/server/storage.d.ts.map +1 -1
  84. package/dist/server/storage.js +25 -0
  85. package/dist/server/wire.d.ts +127 -2
  86. package/dist/server/wire.d.ts.map +1 -1
  87. 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
- enabled: true,
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
- const trimmed = line.trim();
95
- // Handle block comments
87
+ let processed = line;
88
+ // Remove only block-comment segments so inline code before/after survives.
96
89
  if (patterns.blockStart && patterns.blockEnd) {
97
- if (!inBlockComment && trimmed.includes(patterns.blockStart)) {
98
- inBlockComment = true;
99
- }
100
- if (inBlockComment) {
101
- if (trimmed.includes(patterns.blockEnd)) {
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
- continue;
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(line.replace(/\s+$/, ""));
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 TEST_COMMANDS = [
222
- "jest", "vitest", "pytest", "go test", "cargo test",
223
- "rspec", "rake test", "playwright test", "npx playwright",
224
- "npm test", "npm run test", "yarn test", "pnpm test",
225
- "bun test",
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 GIT_COMMANDS = [
229
- "git status", "git diff", "git log", "git show",
230
- "git push", "git pull", "git fetch", "git add",
231
- "git commit", "git branch", "git stash",
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 base = command.split(/\s+/).slice(0, 3).join(" ").toLowerCase();
246
- for (const testCmd of TEST_COMMANDS) {
247
- if (base.startsWith(testCmd))
248
- return "test";
249
- }
250
- for (const gitCmd of GIT_COMMANDS) {
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 (/^FAIL|^\s*●|^\s*\d+\)|^\s*FAILED|^AssertionError|^Error:/i.test(line)) {
306
+ if (failurePattern.test(line)) {
307
+ sawRecognizedSignal = true;
283
308
  failureSection = true;
284
309
  }
285
310
  // Start of summary section
286
- if (/^(Test Suites|Tests|Snapshots|Time|Ran all):/i.test(line)) {
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, just return a summary
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) => /^(Tests|Test Suites|Snapshots|Time|Ran):/i.test(l.trim()));
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 "All tests passed.";
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
- // Group by directory when listing many files
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
- const groups = new Map();
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
- if (compressed && compressed !== original) {
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
- if (compressed && compressed !== original) {
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
- if (compressed && compressed !== original) {
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
- if (compressed && compressed !== original) {
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
- if (compressed && compressed !== original) {
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"}