pi-rtk-optimizer 0.3.3 → 0.5.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +102 -67
  2. package/README.md +292 -290
  3. package/config/config.example.json +36 -35
  4. package/package.json +4 -4
  5. package/src/additional-coverage-test.ts +278 -0
  6. package/src/boolean-format.ts +3 -0
  7. package/src/command-rewriter-test.ts +160 -120
  8. package/src/command-rewriter.ts +594 -585
  9. package/src/config-modal-test.ts +168 -0
  10. package/src/config-modal.ts +613 -600
  11. package/src/config-store.ts +224 -217
  12. package/src/index-test.ts +54 -0
  13. package/src/index.ts +410 -289
  14. package/src/output-compactor-test.ts +500 -158
  15. package/src/output-compactor.ts +432 -349
  16. package/src/record-utils.ts +6 -0
  17. package/src/rewrite-bypass.ts +332 -173
  18. package/src/rewrite-pipeline-safety.ts +154 -0
  19. package/src/rewrite-rules.ts +255 -255
  20. package/src/rtk-command-environment.ts +64 -0
  21. package/src/runtime-guard-test.ts +42 -50
  22. package/src/runtime-guard.ts +14 -14
  23. package/src/techniques/build.ts +155 -155
  24. package/src/techniques/emoji.ts +91 -0
  25. package/src/techniques/git.ts +231 -229
  26. package/src/techniques/index.ts +10 -16
  27. package/src/techniques/linter.ts +151 -161
  28. package/src/techniques/path-utils.ts +67 -0
  29. package/src/techniques/rtk.ts +136 -0
  30. package/src/techniques/search.ts +67 -76
  31. package/src/techniques/source.ts +253 -253
  32. package/src/techniques/test-output.ts +172 -172
  33. package/src/test-helpers.ts +10 -0
  34. package/src/tool-execution-sanitizer.ts +69 -0
  35. package/src/types-shims.d.ts +192 -183
  36. package/src/types.ts +103 -114
  37. package/src/zellij-modal.ts +1001 -1001
  38. package/src/compat-commands.ts +0 -207
@@ -1,349 +1,432 @@
1
- import {
2
- aggregateLinterOutput,
3
- aggregateTestOutput,
4
- compactGitOutput,
5
- detectLanguage,
6
- filterBuildOutput,
7
- filterSourceCode,
8
- groupSearchResults,
9
- smartTruncate,
10
- stripAnsiFast,
11
- truncate,
12
- } from "./techniques/index.js";
13
- import { trackOutputSavings } from "./output-metrics.js";
14
- import type { RtkIntegrationConfig } from "./types.js";
15
-
16
- interface ContentBlock {
17
- type: string;
18
- text?: string;
19
- [key: string]: unknown;
20
- }
21
-
22
- interface ToolResultLikeEvent {
23
- toolName: string;
24
- input?: unknown;
25
- content?: unknown;
26
- }
27
-
28
- export interface ToolResultCompactionMetadata {
29
- applied: boolean;
30
- techniques: string[];
31
- truncated: boolean;
32
- originalCharCount: number;
33
- compactedCharCount: number;
34
- originalLineCount: number;
35
- compactedLineCount: number;
36
- }
37
-
38
- export interface ToolResultCompactionOutcome {
39
- changed: boolean;
40
- content?: unknown[];
41
- techniques: string[];
42
- metadata?: ToolResultCompactionMetadata;
43
- }
44
-
45
- const LOSSY_TECHNIQUE_PREFIXES = [
46
- "build",
47
- "test",
48
- "git",
49
- "linter",
50
- "search",
51
- "truncate",
52
- "smart-truncate",
53
- "source:",
54
- ] as const;
55
-
56
- const READ_EXACT_OUTPUT_LINE_THRESHOLD = 80;
57
- const READ_COMPACTION_BANNER_PREFIX = "[RTK compacted output:";
58
-
59
- function toRecord(value: unknown): Record<string, unknown> {
60
- if (!value || typeof value !== "object" || Array.isArray(value)) {
61
- return {};
62
- }
63
- return value as Record<string, unknown>;
64
- }
65
-
66
- function toArray(value: unknown): unknown[] {
67
- return Array.isArray(value) ? value : [];
68
- }
69
-
70
- function normalizeCommand(input: Record<string, unknown>): string | undefined {
71
- const raw = input.command;
72
- if (typeof raw === "string" && raw.trim()) {
73
- return raw;
74
- }
75
- return undefined;
76
- }
77
-
78
- function normalizePath(input: Record<string, unknown>): string {
79
- const raw = input.path;
80
- if (typeof raw === "string") {
81
- return raw;
82
- }
83
- return "";
84
- }
85
-
86
- function hasExplicitReadRange(input: Record<string, unknown>): boolean {
87
- return input.offset !== undefined || input.limit !== undefined;
88
- }
89
-
90
- function shouldPreserveExactReadOutput(text: string, input: Record<string, unknown>): boolean {
91
- if (hasExplicitReadRange(input)) {
92
- return true;
93
- }
94
-
95
- return countLines(text) <= READ_EXACT_OUTPUT_LINE_THRESHOLD;
96
- }
97
-
98
- function formatReadCompactionBanner(techniques: string[]): string {
99
- return `${READ_COMPACTION_BANNER_PREFIX} ${techniques.join(", ")}]`;
100
- }
101
-
102
- function countLines(text: string): number {
103
- if (!text) {
104
- return 0;
105
- }
106
-
107
- const normalized = text.endsWith("\n") ? text.slice(0, -1) : text;
108
- if (!normalized) {
109
- return 1;
110
- }
111
-
112
- return normalized.split("\n").length;
113
- }
114
-
115
- function hasLossyCompaction(techniques: string[]): boolean {
116
- return techniques.some((technique) =>
117
- LOSSY_TECHNIQUE_PREFIXES.some((prefix) =>
118
- prefix.endsWith(":") ? technique.startsWith(prefix) : technique === prefix,
119
- ),
120
- );
121
- }
122
-
123
- function compactBashText(
124
- text: string,
125
- command: string | undefined,
126
- config: RtkIntegrationConfig,
127
- ): { text: string; techniques: string[] } {
128
- let nextText = text;
129
- const techniques: string[] = [];
130
- const compaction = config.outputCompaction;
131
-
132
- if (compaction.stripAnsi) {
133
- const stripped = stripAnsiFast(nextText);
134
- if (stripped !== nextText) {
135
- nextText = stripped;
136
- techniques.push("ansi");
137
- }
138
- }
139
-
140
- if (compaction.filterBuildOutput) {
141
- const compacted = filterBuildOutput(nextText, command);
142
- if (compacted !== null && compacted !== nextText) {
143
- nextText = compacted;
144
- techniques.push("build");
145
- }
146
- }
147
-
148
- if (compaction.aggregateTestOutput) {
149
- const compacted = aggregateTestOutput(nextText, command);
150
- if (compacted !== null && compacted !== nextText) {
151
- nextText = compacted;
152
- techniques.push("test");
153
- }
154
- }
155
-
156
- if (compaction.compactGitOutput) {
157
- const compacted = compactGitOutput(nextText, command);
158
- if (compacted !== null && compacted !== nextText) {
159
- nextText = compacted;
160
- techniques.push("git");
161
- }
162
- }
163
-
164
- if (compaction.aggregateLinterOutput) {
165
- const compacted = aggregateLinterOutput(nextText, command);
166
- if (compacted !== null && compacted !== nextText) {
167
- nextText = compacted;
168
- techniques.push("linter");
169
- }
170
- }
171
-
172
- if (compaction.truncate.enabled && nextText.length > compaction.truncate.maxChars) {
173
- nextText = truncate(nextText, compaction.truncate.maxChars);
174
- techniques.push("truncate");
175
- }
176
-
177
- return { text: nextText, techniques };
178
- }
179
-
180
- function compactReadText(
181
- text: string,
182
- filePath: string,
183
- config: RtkIntegrationConfig,
184
- preserveExactReadOutput: boolean,
185
- ): { text: string; techniques: string[] } {
186
- if (preserveExactReadOutput) {
187
- return { text, techniques: [] };
188
- }
189
-
190
- let nextText = text;
191
- const techniques: string[] = [];
192
- const compaction = config.outputCompaction;
193
-
194
- if (compaction.stripAnsi) {
195
- const stripped = stripAnsiFast(nextText);
196
- if (stripped !== nextText) {
197
- nextText = stripped;
198
- techniques.push("ansi");
199
- }
200
- }
201
-
202
- const language = detectLanguage(filePath);
203
- if (compaction.sourceCodeFilteringEnabled && compaction.sourceCodeFiltering !== "none") {
204
- const filtered = filterSourceCode(nextText, language, compaction.sourceCodeFiltering);
205
- if (filtered !== nextText) {
206
- nextText = filtered;
207
- techniques.push(`source:${compaction.sourceCodeFiltering}`);
208
- }
209
- }
210
-
211
- if (compaction.smartTruncate.enabled) {
212
- const lineCount = nextText.split("\n").length;
213
- if (lineCount > compaction.smartTruncate.maxLines) {
214
- const compacted = smartTruncate(nextText, compaction.smartTruncate.maxLines, language);
215
- if (compacted !== nextText) {
216
- nextText = compacted;
217
- techniques.push("smart-truncate");
218
- }
219
- }
220
- }
221
-
222
- if (compaction.truncate.enabled && nextText.length > compaction.truncate.maxChars) {
223
- nextText = truncate(nextText, compaction.truncate.maxChars);
224
- techniques.push("truncate");
225
- }
226
-
227
- if (techniques.length > 0 && !nextText.startsWith(READ_COMPACTION_BANNER_PREFIX)) {
228
- nextText = `${formatReadCompactionBanner(techniques)}\n${nextText}`;
229
- }
230
-
231
- return { text: nextText, techniques };
232
- }
233
-
234
- function compactGrepText(text: string, config: RtkIntegrationConfig): { text: string; techniques: string[] } {
235
- let nextText = text;
236
- const techniques: string[] = [];
237
- const compaction = config.outputCompaction;
238
-
239
- if (compaction.stripAnsi) {
240
- const stripped = stripAnsiFast(nextText);
241
- if (stripped !== nextText) {
242
- nextText = stripped;
243
- techniques.push("ansi");
244
- }
245
- }
246
-
247
- if (compaction.groupSearchOutput) {
248
- const grouped = groupSearchResults(nextText);
249
- if (grouped !== null && grouped !== nextText) {
250
- nextText = grouped;
251
- techniques.push("search");
252
- }
253
- }
254
-
255
- if (compaction.truncate.enabled && nextText.length > compaction.truncate.maxChars) {
256
- nextText = truncate(nextText, compaction.truncate.maxChars);
257
- techniques.push("truncate");
258
- }
259
-
260
- return { text: nextText, techniques };
261
- }
262
-
263
- export function compactToolResult(
264
- event: ToolResultLikeEvent,
265
- config: RtkIntegrationConfig,
266
- ): ToolResultCompactionOutcome {
267
- if (!config.outputCompaction.enabled) {
268
- return { changed: false, techniques: [] };
269
- }
270
-
271
- const input = toRecord(event.input);
272
- const sourceContent = toArray(event.content);
273
- if (sourceContent.length === 0) {
274
- return { changed: false, techniques: [] };
275
- }
276
-
277
- let changed = false;
278
- const allTechniques = new Set<string>();
279
- const originalChunks: string[] = [];
280
- const filteredChunks: string[] = [];
281
-
282
- const nextContent = sourceContent.map((block) => {
283
- if (!block || typeof block !== "object" || Array.isArray(block)) {
284
- return block;
285
- }
286
-
287
- const contentBlock = block as ContentBlock;
288
- if (contentBlock.type !== "text" || typeof contentBlock.text !== "string") {
289
- return block;
290
- }
291
-
292
- let transformed = { text: contentBlock.text, techniques: [] as string[] };
293
- if (event.toolName === "bash") {
294
- transformed = compactBashText(contentBlock.text, normalizeCommand(input), config);
295
- } else if (event.toolName === "read") {
296
- transformed = compactReadText(
297
- contentBlock.text,
298
- normalizePath(input),
299
- config,
300
- shouldPreserveExactReadOutput(contentBlock.text, input),
301
- );
302
- } else if (event.toolName === "grep") {
303
- transformed = compactGrepText(contentBlock.text, config);
304
- }
305
-
306
- for (const technique of transformed.techniques) {
307
- allTechniques.add(technique);
308
- }
309
-
310
- originalChunks.push(contentBlock.text);
311
- filteredChunks.push(transformed.text);
312
-
313
- if (transformed.text !== contentBlock.text) {
314
- changed = true;
315
- return { ...contentBlock, text: transformed.text };
316
- }
317
-
318
- return block;
319
- });
320
-
321
- if (!changed) {
322
- return { changed: false, techniques: [] };
323
- }
324
-
325
- const techniques = Array.from(allTechniques);
326
- const originalText = originalChunks.join("\n");
327
- const compactedText = filteredChunks.join("\n");
328
-
329
- if (config.outputCompaction.trackSavings) {
330
- trackOutputSavings(originalText, compactedText, event.toolName, techniques);
331
- }
332
-
333
- const metadata: ToolResultCompactionMetadata = {
334
- applied: true,
335
- techniques,
336
- truncated: hasLossyCompaction(techniques),
337
- originalCharCount: originalText.length,
338
- compactedCharCount: compactedText.length,
339
- originalLineCount: countLines(originalText),
340
- compactedLineCount: countLines(compactedText),
341
- };
342
-
343
- return {
344
- changed: true,
345
- content: nextContent,
346
- techniques,
347
- metadata,
348
- };
349
- }
1
+ import { homedir } from "node:os";
2
+ import { dirname, join, resolve, sep } from "node:path";
3
+ import {
4
+ aggregateLinterOutput,
5
+ aggregateTestOutput,
6
+ compactGitOutput,
7
+ detectLanguage,
8
+ filterBuildOutput,
9
+ filterSourceCode,
10
+ groupSearchResults,
11
+ sanitizeRtkEmojiOutput,
12
+ smartTruncate,
13
+ stripAnsiFast,
14
+ stripRtkHookWarnings,
15
+ truncate,
16
+ } from "./techniques/index.js";
17
+ import { trackOutputSavings } from "./output-metrics.js";
18
+ import { toRecord } from "./record-utils.js";
19
+ import type { RtkIntegrationConfig } from "./types.js";
20
+
21
+ interface ContentBlock {
22
+ type: string;
23
+ text?: string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ interface ToolResultLikeEvent {
28
+ toolName: string;
29
+ input?: unknown;
30
+ content?: unknown;
31
+ }
32
+
33
+ export interface ToolResultCompactionMetadata {
34
+ applied: boolean;
35
+ techniques: string[];
36
+ truncated: boolean;
37
+ originalCharCount: number;
38
+ compactedCharCount: number;
39
+ originalLineCount: number;
40
+ compactedLineCount: number;
41
+ }
42
+
43
+ export interface ToolResultCompactionOutcome {
44
+ changed: boolean;
45
+ content?: unknown[];
46
+ techniques: string[];
47
+ metadata?: ToolResultCompactionMetadata;
48
+ }
49
+
50
+ const LOSSY_TECHNIQUE_PREFIXES = [
51
+ "build",
52
+ "test",
53
+ "git",
54
+ "linter",
55
+ "search",
56
+ "truncate",
57
+ "smart-truncate",
58
+ "source:",
59
+ ] as const;
60
+
61
+ const READ_EXACT_OUTPUT_LINE_THRESHOLD = 80;
62
+ const READ_COMPACTION_BANNER_PREFIX = "[RTK compacted output:";
63
+ const USER_SKILL_ROOTS = [join(homedir(), ".pi", "agent", "skills"), join(homedir(), ".agents", "skills")];
64
+
65
+ function normalizePathForComparison(path: string): string {
66
+ return process.platform === "win32" ? path.toLowerCase() : path;
67
+ }
68
+
69
+ function isPathUnderRoot(targetPath: string, rootPath: string): boolean {
70
+ const normalizedTarget = normalizePathForComparison(resolve(targetPath));
71
+ const normalizedRoot = normalizePathForComparison(resolve(rootPath));
72
+ if (normalizedTarget === normalizedRoot) {
73
+ return true;
74
+ }
75
+
76
+ const rootWithSeparator = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
77
+ return normalizedTarget.startsWith(rootWithSeparator);
78
+ }
79
+
80
+ function isUnderAnyAncestorAgentsSkills(targetPath: string): boolean {
81
+ let currentDir = resolve(process.cwd());
82
+ while (true) {
83
+ if (isPathUnderRoot(targetPath, join(currentDir, ".agents", "skills"))) {
84
+ return true;
85
+ }
86
+
87
+ const parentDir = dirname(currentDir);
88
+ if (parentDir === currentDir) {
89
+ return false;
90
+ }
91
+
92
+ currentDir = parentDir;
93
+ }
94
+ }
95
+
96
+ function isSkillReadPath(filePath: string): boolean {
97
+ if (!filePath.trim()) {
98
+ return false;
99
+ }
100
+
101
+ const resolvedPath = resolve(filePath);
102
+ if (USER_SKILL_ROOTS.some((root) => isPathUnderRoot(resolvedPath, root))) {
103
+ return true;
104
+ }
105
+
106
+ if (isPathUnderRoot(resolvedPath, join(process.cwd(), ".pi", "skills"))) {
107
+ return true;
108
+ }
109
+
110
+ return isUnderAnyAncestorAgentsSkills(resolvedPath);
111
+ }
112
+
113
+ function toArray(value: unknown): unknown[] {
114
+ return Array.isArray(value) ? value : [];
115
+ }
116
+
117
+ function normalizeCommand(input: Record<string, unknown>): string | undefined {
118
+ const raw = input.command;
119
+ if (typeof raw === "string" && raw.trim()) {
120
+ return raw;
121
+ }
122
+ return undefined;
123
+ }
124
+
125
+ function normalizePath(input: Record<string, unknown>): string {
126
+ const raw = input.path;
127
+ if (typeof raw === "string") {
128
+ return raw;
129
+ }
130
+ return "";
131
+ }
132
+
133
+ function hasExplicitReadRange(input: Record<string, unknown>): boolean {
134
+ return input.offset !== undefined || input.limit !== undefined;
135
+ }
136
+
137
+ function shouldPreserveExactReadOutput(
138
+ text: string,
139
+ input: Record<string, unknown>,
140
+ config: RtkIntegrationConfig,
141
+ ): boolean {
142
+ if (hasExplicitReadRange(input)) {
143
+ return true;
144
+ }
145
+
146
+ if (config.outputCompaction.preserveExactSkillReads && isSkillReadPath(normalizePath(input))) {
147
+ return true;
148
+ }
149
+
150
+ return countLines(text) <= READ_EXACT_OUTPUT_LINE_THRESHOLD;
151
+ }
152
+
153
+ function shouldApplyReadSourceFiltering(text: string, config: RtkIntegrationConfig): boolean {
154
+ const compaction = config.outputCompaction;
155
+ const lineCount = countLines(text);
156
+
157
+ return (
158
+ (compaction.smartTruncate.enabled && lineCount > compaction.smartTruncate.maxLines) ||
159
+ (compaction.truncate.enabled && text.length > compaction.truncate.maxChars)
160
+ );
161
+ }
162
+
163
+ function formatReadCompactionBanner(techniques: string[]): string {
164
+ return `${READ_COMPACTION_BANNER_PREFIX} ${techniques.join(", ")}]`;
165
+ }
166
+
167
+ function countLines(text: string): number {
168
+ if (!text) {
169
+ return 0;
170
+ }
171
+
172
+ const normalized = text.endsWith("\n") ? text.slice(0, -1) : text;
173
+ if (!normalized) {
174
+ return 1;
175
+ }
176
+
177
+ return normalized.split("\n").length;
178
+ }
179
+
180
+ function hasLossyCompaction(techniques: string[]): boolean {
181
+ return techniques.some((technique) =>
182
+ LOSSY_TECHNIQUE_PREFIXES.some((prefix) =>
183
+ prefix.endsWith(":") ? technique.startsWith(prefix) : technique === prefix,
184
+ ),
185
+ );
186
+ }
187
+
188
+ function compactBashText(
189
+ text: string,
190
+ command: string | undefined,
191
+ config: RtkIntegrationConfig,
192
+ ): { text: string; techniques: string[] } {
193
+ let nextText = text;
194
+ const techniques: string[] = [];
195
+ const compaction = config.outputCompaction;
196
+
197
+ if (compaction.stripAnsi) {
198
+ const stripped = stripAnsiFast(nextText);
199
+ if (stripped !== nextText) {
200
+ nextText = stripped;
201
+ techniques.push("ansi");
202
+ }
203
+ }
204
+
205
+ const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
206
+ if (withoutRtkHookWarnings !== null && withoutRtkHookWarnings !== nextText) {
207
+ nextText = withoutRtkHookWarnings;
208
+ techniques.push("rtk-hook-warning");
209
+ }
210
+
211
+ const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
212
+ if (withoutRtkEmoji !== null && withoutRtkEmoji !== nextText) {
213
+ nextText = withoutRtkEmoji;
214
+ techniques.push("rtk-emoji");
215
+ }
216
+
217
+ if (compaction.filterBuildOutput) {
218
+ const compacted = filterBuildOutput(nextText, command);
219
+ if (compacted !== null && compacted !== nextText) {
220
+ nextText = compacted;
221
+ techniques.push("build");
222
+ }
223
+ }
224
+
225
+ if (compaction.aggregateTestOutput) {
226
+ const compacted = aggregateTestOutput(nextText, command);
227
+ if (compacted !== null && compacted !== nextText) {
228
+ nextText = compacted;
229
+ techniques.push("test");
230
+ }
231
+ }
232
+
233
+ if (compaction.compactGitOutput) {
234
+ const compacted = compactGitOutput(nextText, command);
235
+ if (compacted !== null && compacted !== nextText) {
236
+ nextText = compacted;
237
+ techniques.push("git");
238
+ }
239
+ }
240
+
241
+ if (compaction.aggregateLinterOutput) {
242
+ const compacted = aggregateLinterOutput(nextText, command);
243
+ if (compacted !== null && compacted !== nextText) {
244
+ nextText = compacted;
245
+ techniques.push("linter");
246
+ }
247
+ }
248
+
249
+ if (compaction.truncate.enabled && nextText.length > compaction.truncate.maxChars) {
250
+ nextText = truncate(nextText, compaction.truncate.maxChars);
251
+ techniques.push("truncate");
252
+ }
253
+
254
+ return { text: nextText, techniques };
255
+ }
256
+
257
+ function compactReadText(
258
+ text: string,
259
+ filePath: string,
260
+ config: RtkIntegrationConfig,
261
+ preserveExactReadOutput: boolean,
262
+ ): { text: string; techniques: string[] } {
263
+ if (preserveExactReadOutput) {
264
+ return { text, techniques: [] };
265
+ }
266
+
267
+ let nextText = text;
268
+ const techniques: string[] = [];
269
+ const compaction = config.outputCompaction;
270
+
271
+ if (compaction.stripAnsi) {
272
+ const stripped = stripAnsiFast(nextText);
273
+ if (stripped !== nextText) {
274
+ nextText = stripped;
275
+ techniques.push("ansi");
276
+ }
277
+ }
278
+
279
+ const language = detectLanguage(filePath);
280
+ // Only apply lossy source filtering when a downstream line/char safeguard would otherwise trigger.
281
+ if (
282
+ compaction.sourceCodeFilteringEnabled &&
283
+ compaction.sourceCodeFiltering !== "none" &&
284
+ shouldApplyReadSourceFiltering(text, config)
285
+ ) {
286
+ const filtered = filterSourceCode(nextText, language, compaction.sourceCodeFiltering);
287
+ if (filtered !== nextText) {
288
+ nextText = filtered;
289
+ techniques.push(`source:${compaction.sourceCodeFiltering}`);
290
+ }
291
+ }
292
+
293
+ if (compaction.smartTruncate.enabled) {
294
+ const lineCount = nextText.split("\n").length;
295
+ if (lineCount > compaction.smartTruncate.maxLines) {
296
+ const compacted = smartTruncate(nextText, compaction.smartTruncate.maxLines, language);
297
+ if (compacted !== nextText) {
298
+ nextText = compacted;
299
+ techniques.push("smart-truncate");
300
+ }
301
+ }
302
+ }
303
+
304
+ if (compaction.truncate.enabled && nextText.length > compaction.truncate.maxChars) {
305
+ nextText = truncate(nextText, compaction.truncate.maxChars);
306
+ techniques.push("truncate");
307
+ }
308
+
309
+ if (techniques.length > 0 && !nextText.startsWith(READ_COMPACTION_BANNER_PREFIX)) {
310
+ nextText = `${formatReadCompactionBanner(techniques)}\n${nextText}`;
311
+ }
312
+
313
+ return { text: nextText, techniques };
314
+ }
315
+
316
+ function compactGrepText(text: string, config: RtkIntegrationConfig): { text: string; techniques: string[] } {
317
+ let nextText = text;
318
+ const techniques: string[] = [];
319
+ const compaction = config.outputCompaction;
320
+
321
+ if (compaction.stripAnsi) {
322
+ const stripped = stripAnsiFast(nextText);
323
+ if (stripped !== nextText) {
324
+ nextText = stripped;
325
+ techniques.push("ansi");
326
+ }
327
+ }
328
+
329
+ if (compaction.groupSearchOutput) {
330
+ const grouped = groupSearchResults(nextText);
331
+ if (grouped !== null && grouped !== nextText) {
332
+ nextText = grouped;
333
+ techniques.push("search");
334
+ }
335
+ }
336
+
337
+ if (compaction.truncate.enabled && nextText.length > compaction.truncate.maxChars) {
338
+ nextText = truncate(nextText, compaction.truncate.maxChars);
339
+ techniques.push("truncate");
340
+ }
341
+
342
+ return { text: nextText, techniques };
343
+ }
344
+
345
+ export function compactToolResult(
346
+ event: ToolResultLikeEvent,
347
+ config: RtkIntegrationConfig,
348
+ ): ToolResultCompactionOutcome {
349
+ if (!config.outputCompaction.enabled) {
350
+ return { changed: false, techniques: [] };
351
+ }
352
+
353
+ const input = toRecord(event.input);
354
+ const sourceContent = toArray(event.content);
355
+ if (sourceContent.length === 0) {
356
+ return { changed: false, techniques: [] };
357
+ }
358
+
359
+ let changed = false;
360
+ const allTechniques = new Set<string>();
361
+ const originalChunks: string[] = [];
362
+ const filteredChunks: string[] = [];
363
+
364
+ const nextContent = sourceContent.map((block) => {
365
+ if (!block || typeof block !== "object" || Array.isArray(block)) {
366
+ return block;
367
+ }
368
+
369
+ const contentBlock = block as ContentBlock;
370
+ if (contentBlock.type !== "text" || typeof contentBlock.text !== "string") {
371
+ return block;
372
+ }
373
+
374
+ let transformed = { text: contentBlock.text, techniques: [] as string[] };
375
+ if (event.toolName === "bash") {
376
+ transformed = compactBashText(contentBlock.text, normalizeCommand(input), config);
377
+ } else if (event.toolName === "read") {
378
+ const normalizedPath = normalizePath(input);
379
+ transformed = compactReadText(
380
+ contentBlock.text,
381
+ normalizedPath,
382
+ config,
383
+ shouldPreserveExactReadOutput(contentBlock.text, input, config),
384
+ );
385
+ } else if (event.toolName === "grep") {
386
+ transformed = compactGrepText(contentBlock.text, config);
387
+ }
388
+
389
+ for (const technique of transformed.techniques) {
390
+ allTechniques.add(technique);
391
+ }
392
+
393
+ originalChunks.push(contentBlock.text);
394
+ filteredChunks.push(transformed.text);
395
+
396
+ if (transformed.text !== contentBlock.text) {
397
+ changed = true;
398
+ return { ...contentBlock, text: transformed.text };
399
+ }
400
+
401
+ return block;
402
+ });
403
+
404
+ if (!changed) {
405
+ return { changed: false, techniques: [] };
406
+ }
407
+
408
+ const techniques = Array.from(allTechniques);
409
+ const originalText = originalChunks.join("\n");
410
+ const compactedText = filteredChunks.join("\n");
411
+
412
+ if (config.outputCompaction.trackSavings) {
413
+ trackOutputSavings(originalText, compactedText, event.toolName, techniques);
414
+ }
415
+
416
+ const metadata: ToolResultCompactionMetadata = {
417
+ applied: true,
418
+ techniques,
419
+ truncated: hasLossyCompaction(techniques),
420
+ originalCharCount: originalText.length,
421
+ compactedCharCount: compactedText.length,
422
+ originalLineCount: countLines(originalText),
423
+ compactedLineCount: countLines(compactedText),
424
+ };
425
+
426
+ return {
427
+ changed: true,
428
+ content: nextContent,
429
+ techniques,
430
+ metadata,
431
+ };
432
+ }