@tyvm/knowhow 0.0.144 → 0.0.145

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 (151) hide show
  1. package/bin/knowhow.js +22 -20
  2. package/package.json +1 -1
  3. package/scripts/migrate-usage-json.js +147 -0
  4. package/src/agents/base/base.ts +65 -10
  5. package/src/agents/tools/patch.ts +84 -2
  6. package/src/agents/tools/textSearch.ts +98 -3
  7. package/src/chat/modules/AgentModule.ts +129 -6
  8. package/src/chat/modules/SessionsModule.ts +6 -0
  9. package/src/chat/types.ts +14 -0
  10. package/src/chat.ts +11 -0
  11. package/src/cli.ts +2 -0
  12. package/src/clients/anthropic.ts +56 -1
  13. package/src/clients/contextLimits.ts +40 -0
  14. package/src/clients/gemini.ts +59 -5
  15. package/src/clients/http.ts +85 -4
  16. package/src/clients/index.ts +4 -0
  17. package/src/clients/openai.ts +179 -31
  18. package/src/clients/pricing/fireworks.ts +50 -1
  19. package/src/clients/pricing/index.ts +1 -1
  20. package/src/clients/pricing/models.ts +3 -1
  21. package/src/clients/pricing/openai.ts +2 -2
  22. package/src/clients/pricing/qwen.ts +80 -0
  23. package/src/clients/qwen.ts +16 -0
  24. package/src/clients/types.ts +69 -1
  25. package/src/clients/xai.ts +73 -4
  26. package/src/commands/agent.ts +20 -1
  27. package/src/index.ts +9 -0
  28. package/src/processors/Base64ImageDetector.ts +46 -7
  29. package/src/processors/GemmaToolProcessor.ts +163 -0
  30. package/src/processors/index.ts +1 -0
  31. package/src/services/AgentSyncFs.ts +37 -0
  32. package/src/services/SessionManager.ts +14 -0
  33. package/src/services/Tools.ts +3 -0
  34. package/src/services/modules/computerUse.ts +216 -0
  35. package/src/services/modules/index.ts +47 -9
  36. package/src/services/modules/types.ts +23 -1
  37. package/src/types.ts +3 -0
  38. package/tests/clients/contextLimits.test.ts +4 -0
  39. package/tests/clients/openai.images.test.ts +105 -0
  40. package/tests/clients/pricing.test.ts +6 -0
  41. package/tests/compressor/usageBase64.json +159 -0
  42. package/tests/compressor/usageBase64.test.ts +175 -0
  43. package/tests/patching/fixtures/transcript-mangles/agent-sync-fs-nested-catch.json +9 -0
  44. package/tests/patching/fixtures/transcript-mangles/automation-functions-crossed.json +9 -0
  45. package/tests/patching/fixtures/transcript-mangles/fruit-ninja-confirmed-retirement.json +8 -0
  46. package/tests/patching/patchFileOutput.test.ts +112 -1
  47. package/tests/patching/transcriptMangleRegression.test.ts +125 -0
  48. package/tests/processors/Base64ImageDetector.test.ts +36 -1
  49. package/ts_build/package.json +1 -1
  50. package/ts_build/src/agents/base/base.d.ts +8 -1
  51. package/ts_build/src/agents/base/base.js +37 -3
  52. package/ts_build/src/agents/base/base.js.map +1 -1
  53. package/ts_build/src/agents/tools/patch.d.ts +1 -0
  54. package/ts_build/src/agents/tools/patch.js +35 -2
  55. package/ts_build/src/agents/tools/patch.js.map +1 -1
  56. package/ts_build/src/agents/tools/textSearch.js +56 -2
  57. package/ts_build/src/agents/tools/textSearch.js.map +1 -1
  58. package/ts_build/src/chat/modules/AgentModule.d.ts +14 -1
  59. package/ts_build/src/chat/modules/AgentModule.js +73 -5
  60. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  61. package/ts_build/src/chat/modules/SessionsModule.js +4 -0
  62. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  63. package/ts_build/src/chat/types.d.ts +11 -0
  64. package/ts_build/src/chat.js +7 -0
  65. package/ts_build/src/chat.js.map +1 -1
  66. package/ts_build/src/cli.d.ts +2 -1
  67. package/ts_build/src/cli.js +1 -0
  68. package/ts_build/src/cli.js.map +1 -1
  69. package/ts_build/src/clients/anthropic.js +28 -1
  70. package/ts_build/src/clients/anthropic.js.map +1 -1
  71. package/ts_build/src/clients/contextLimits.js +25 -0
  72. package/ts_build/src/clients/contextLimits.js.map +1 -1
  73. package/ts_build/src/clients/gemini.js +48 -4
  74. package/ts_build/src/clients/gemini.js.map +1 -1
  75. package/ts_build/src/clients/http.d.ts +2 -0
  76. package/ts_build/src/clients/http.js +66 -9
  77. package/ts_build/src/clients/http.js.map +1 -1
  78. package/ts_build/src/clients/index.d.ts +1 -0
  79. package/ts_build/src/clients/index.js +4 -0
  80. package/ts_build/src/clients/index.js.map +1 -1
  81. package/ts_build/src/clients/openai.d.ts +6 -1
  82. package/ts_build/src/clients/openai.js +131 -33
  83. package/ts_build/src/clients/openai.js.map +1 -1
  84. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -1
  85. package/ts_build/src/clients/pricing/fireworks.js +33 -1
  86. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  87. package/ts_build/src/clients/pricing/index.d.ts +1 -1
  88. package/ts_build/src/clients/pricing/index.js +6 -2
  89. package/ts_build/src/clients/pricing/index.js.map +1 -1
  90. package/ts_build/src/clients/pricing/models.d.ts +2 -1
  91. package/ts_build/src/clients/pricing/models.js +4 -1
  92. package/ts_build/src/clients/pricing/models.js.map +1 -1
  93. package/ts_build/src/clients/pricing/openai.js +2 -2
  94. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  95. package/ts_build/src/clients/pricing/qwen.d.ts +5 -0
  96. package/ts_build/src/clients/pricing/qwen.js +30 -0
  97. package/ts_build/src/clients/pricing/qwen.js.map +1 -0
  98. package/ts_build/src/clients/qwen.d.ts +4 -0
  99. package/ts_build/src/clients/qwen.js +15 -0
  100. package/ts_build/src/clients/qwen.js.map +1 -0
  101. package/ts_build/src/clients/types.d.ts +12 -0
  102. package/ts_build/src/clients/xai.d.ts +1 -0
  103. package/ts_build/src/clients/xai.js +56 -9
  104. package/ts_build/src/clients/xai.js.map +1 -1
  105. package/ts_build/src/commands/agent.js +10 -1
  106. package/ts_build/src/commands/agent.js.map +1 -1
  107. package/ts_build/src/index.d.ts +2 -0
  108. package/ts_build/src/index.js +1 -0
  109. package/ts_build/src/index.js.map +1 -1
  110. package/ts_build/src/processors/Base64ImageDetector.js +22 -3
  111. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  112. package/ts_build/src/processors/GemmaToolProcessor.d.ts +14 -0
  113. package/ts_build/src/processors/GemmaToolProcessor.js +97 -0
  114. package/ts_build/src/processors/GemmaToolProcessor.js.map +1 -0
  115. package/ts_build/src/processors/index.d.ts +1 -0
  116. package/ts_build/src/processors/index.js +3 -1
  117. package/ts_build/src/processors/index.js.map +1 -1
  118. package/ts_build/src/services/AgentSyncFs.js +17 -0
  119. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  120. package/ts_build/src/services/SessionManager.js +12 -0
  121. package/ts_build/src/services/SessionManager.js.map +1 -1
  122. package/ts_build/src/services/Tools.d.ts +2 -0
  123. package/ts_build/src/services/Tools.js.map +1 -1
  124. package/ts_build/src/services/modules/computerUse.d.ts +163 -0
  125. package/ts_build/src/services/modules/computerUse.js +3 -0
  126. package/ts_build/src/services/modules/computerUse.js.map +1 -0
  127. package/ts_build/src/services/modules/index.d.ts +1 -0
  128. package/ts_build/src/services/modules/index.js +23 -6
  129. package/ts_build/src/services/modules/index.js.map +1 -1
  130. package/ts_build/src/services/modules/types.d.ts +4 -0
  131. package/ts_build/src/types.d.ts +3 -0
  132. package/ts_build/src/types.js.map +1 -1
  133. package/ts_build/src/workers/tools/index.d.ts +1 -1
  134. package/ts_build/tests/clients/contextLimits.test.js +2 -0
  135. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  136. package/ts_build/tests/clients/openai.images.test.d.ts +1 -0
  137. package/ts_build/tests/clients/openai.images.test.js +94 -0
  138. package/ts_build/tests/clients/openai.images.test.js.map +1 -0
  139. package/ts_build/tests/clients/pricing.test.js +4 -0
  140. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  141. package/ts_build/tests/compressor/usageBase64.test.d.ts +1 -0
  142. package/ts_build/tests/compressor/usageBase64.test.js +109 -0
  143. package/ts_build/tests/compressor/usageBase64.test.js.map +1 -0
  144. package/ts_build/tests/patching/patchFileOutput.test.js +82 -0
  145. package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -1
  146. package/ts_build/tests/patching/transcriptMangleRegression.test.d.ts +1 -0
  147. package/ts_build/tests/patching/transcriptMangleRegression.test.js +104 -0
  148. package/ts_build/tests/patching/transcriptMangleRegression.test.js.map +1 -0
  149. package/ts_build/tests/processors/Base64ImageDetector.test.js +24 -0
  150. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  151. package/x-home.png +0 -0
package/bin/knowhow.js CHANGED
@@ -1,23 +1,25 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node --no-node-snapshot
2
2
 
3
- const { spawnSync } = require("node:child_process");
4
- const path = require("node:path");
5
-
6
- // If the current Node process wasn't started with --no-node-snapshot,
7
- // re-exec Node with the flag and forward the original arguments.
8
- if (!process.execArgv.includes("--no-node-snapshot")) {
9
- const cliEntrypoint = path.join(__dirname, "../ts_build/src/cli.js");
3
+ // knowhow CLI entry point.
4
+ //
5
+ // --no-node-snapshot is required for isolated-vm (used by executeScript).
6
+ // It is declared in the shebang above so that Node starts with it directly —
7
+ // no re-exec needed. This means the process stays as a direct child of the
8
+ // terminal (e.g. Ghostty) and inherits its macOS TCC (Screen Recording /
9
+ // Accessibility) permissions without needing to grant them to node itself.
10
10
 
11
- const result = spawnSync(
12
- process.execPath,
13
- ["--no-node-snapshot", "--enable-source-maps", cliEntrypoint, ...process.argv.slice(2)],
14
- {
15
- stdio: "inherit",
16
- }
17
- );
18
-
19
- process.exit(result.status ?? 1);
20
- }
11
+ const path = require("node:path");
12
+ const cliJs = path.join(__dirname, "../ts_build/src/cli.js");
21
13
 
22
- // Already running with the flag → just load the CLI
23
- require("../ts_build/src/cli.js");
14
+ // cli.js only auto-runs main() when it is the entry module (require.main ===
15
+ // module). Since we're requiring it here, we call main() ourselves so the
16
+ // CLI actually runs and exits cleanly.
17
+ const cli = require(cliJs);
18
+ cli.main()
19
+ .catch((e) => {
20
+ console.error(e);
21
+ process.exit(1);
22
+ })
23
+ .then(() => {
24
+ process.exit(0);
25
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.144",
3
+ "version": "0.0.145",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Migration script: Compress usage.json files in .knowhow/processes/agents/
4
+ *
5
+ * The old format writes the full message thread with every usage entry, causing
6
+ * exponential file growth. This script applies the same compression logic as the
7
+ * new appendUsageEntry code: if entry[N].messages is a prefix of entry[N+1].messages,
8
+ * replace entry[N].messages with [{ PREV_CACHE_HIT: true }].
9
+ *
10
+ * Usage:
11
+ * node scripts/migrate-usage-json.js # dry run (shows space savings)
12
+ * node scripts/migrate-usage-json.js --migrate # actually migrates files
13
+ */
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+
18
+ const AGENTS_DIR = path.join(
19
+ __dirname,
20
+ "../.knowhow/processes/agents"
21
+ );
22
+
23
+ const DRY_RUN = !process.argv.includes("--migrate");
24
+
25
+ if (DRY_RUN) {
26
+ console.log("=== DRY RUN (pass --migrate to actually migrate) ===\n");
27
+ } else {
28
+ console.log("=== MIGRATING FILES ===\n");
29
+ }
30
+
31
+ function compressEntries(entries) {
32
+ let compressed = 0;
33
+ for (let i = 0; i < entries.length - 1; i++) {
34
+ const curr = entries[i];
35
+ const next = entries[i + 1];
36
+ const prevMessages = curr?.messages;
37
+ const nextMessages = next?.messages;
38
+
39
+ // Skip if already compressed
40
+ if (
41
+ Array.isArray(prevMessages) &&
42
+ prevMessages.length === 1 &&
43
+ prevMessages[0]?.PREV_CACHE_HIT
44
+ ) {
45
+ continue;
46
+ }
47
+
48
+ if (
49
+ Array.isArray(prevMessages) &&
50
+ Array.isArray(nextMessages) &&
51
+ prevMessages.length > 0 &&
52
+ nextMessages.length >= prevMessages.length &&
53
+ JSON.stringify(nextMessages.slice(0, prevMessages.length)) ===
54
+ JSON.stringify(prevMessages)
55
+ ) {
56
+ curr.messages = [{ PREV_CACHE_HIT: true }];
57
+ compressed++;
58
+ }
59
+ }
60
+ return compressed;
61
+ }
62
+
63
+ let totalOriginalBytes = 0;
64
+ let totalNewBytes = 0;
65
+ let totalFilesProcessed = 0;
66
+ let totalFilesChanged = 0;
67
+ let totalEntriesCompressed = 0;
68
+ let errors = 0;
69
+
70
+ const agentDirs = fs.readdirSync(AGENTS_DIR);
71
+
72
+ for (const dir of agentDirs) {
73
+ const usagePath = path.join(AGENTS_DIR, dir, "usage.json");
74
+
75
+ if (!fs.existsSync(usagePath)) continue;
76
+
77
+ try {
78
+ const originalContent = fs.readFileSync(usagePath, "utf8");
79
+ const originalBytes = Buffer.byteLength(originalContent, "utf8");
80
+ totalOriginalBytes += originalBytes;
81
+ totalFilesProcessed++;
82
+
83
+ let entries;
84
+ try {
85
+ entries = JSON.parse(originalContent);
86
+ } catch (e) {
87
+ console.warn(` [SKIP] ${dir}/usage.json - invalid JSON: ${e.message}`);
88
+ errors++;
89
+ continue;
90
+ }
91
+
92
+ if (!Array.isArray(entries) || entries.length === 0) {
93
+ totalNewBytes += originalBytes;
94
+ continue;
95
+ }
96
+
97
+ const compressed = compressEntries(entries);
98
+ totalEntriesCompressed += compressed;
99
+
100
+ const newContent = JSON.stringify(entries, null, 2);
101
+ const newBytes = Buffer.byteLength(newContent, "utf8");
102
+ totalNewBytes += newBytes;
103
+
104
+ const savedBytes = originalBytes - newBytes;
105
+ const savedPct = ((savedBytes / originalBytes) * 100).toFixed(1);
106
+
107
+ if (compressed > 0) {
108
+ totalFilesChanged++;
109
+ console.log(
110
+ ` ${dir}/usage.json: ${compressed} entries compressed, ` +
111
+ `${(originalBytes / 1024).toFixed(1)}KB -> ${(newBytes / 1024).toFixed(1)}KB ` +
112
+ `(-${(savedBytes / 1024).toFixed(1)}KB, ${savedPct}%)`
113
+ );
114
+
115
+ if (!DRY_RUN) {
116
+ fs.writeFileSync(usagePath, newContent, "utf8");
117
+ }
118
+ }
119
+ } catch (e) {
120
+ console.error(` [ERROR] ${dir}: ${e.message}`);
121
+ errors++;
122
+ }
123
+ }
124
+
125
+ const totalSaved = totalOriginalBytes - totalNewBytes;
126
+ const totalSavedPct = ((totalSaved / totalOriginalBytes) * 100).toFixed(1);
127
+
128
+ console.log("\n=== SUMMARY ===");
129
+ console.log(`Files found with usage.json: ${totalFilesProcessed}`);
130
+ console.log(`Files with compressible data: ${totalFilesChanged}`);
131
+ console.log(`Total entries compressed: ${totalEntriesCompressed}`);
132
+ console.log(
133
+ `Total size before: ${(totalOriginalBytes / 1024 / 1024).toFixed(2)} MB`
134
+ );
135
+ console.log(
136
+ `Total size after: ${(totalNewBytes / 1024 / 1024).toFixed(2)} MB`
137
+ );
138
+ console.log(
139
+ `Space saved: ${(totalSaved / 1024 / 1024).toFixed(2)} MB (${totalSavedPct}%)`
140
+ );
141
+ if (errors > 0) console.log(`Errors: ${errors}`);
142
+
143
+ if (DRY_RUN && totalFilesChanged > 0) {
144
+ console.log(
145
+ "\nRun with --migrate to apply changes (backups written as .bak files)."
146
+ );
147
+ }
@@ -43,6 +43,11 @@ export interface ToolCallEvent {
43
43
  functionResp: any;
44
44
  }
45
45
 
46
+ type ChatCompletion = Parameters<GenericClient["createChatCompletion"]>[0];
47
+ type CachedChatCompletion = { messages: ChatCompletion["messages"] } & Partial<
48
+ Omit<ChatCompletion, "messages">
49
+ >;
50
+
46
51
  /**
47
52
  * A source is anything that can (a) start producing data, (b) hand each datum to
48
53
  * a callback, and (c) be torn down. This is the unified contract behind
@@ -145,6 +150,12 @@ export abstract class BaseAgent implements IAgent {
145
150
  protected taskBreakdown = "";
146
151
  protected summaries = [] as string[];
147
152
  protected currentTaskId: string | null = null;
153
+ /**
154
+ * The taskId of the parent agent that spawned this task, if any.
155
+ * Used by self-referential tools (e.g. replyToParent) to know who to
156
+ * report back to without relying solely on reading metadata.json off disk.
157
+ */
158
+ public parentTaskId: string | null = null;
148
159
 
149
160
  public agentEvents = new EventService();
150
161
  public eventTypes = {
@@ -349,6 +360,14 @@ export abstract class BaseAgent implements IAgent {
349
360
  return this.provider;
350
361
  }
351
362
 
363
+ getParentTaskId(): string | null {
364
+ return this.parentTaskId;
365
+ }
366
+
367
+ setParentTaskId(value: string | null) {
368
+ this.parentTaskId = value ?? null;
369
+ }
370
+
352
371
  setProvider(value: keyof typeof Clients.clients) {
353
372
  this.provider = value;
354
373
  this.client = null; // Reset client to force re-fetch
@@ -595,14 +614,31 @@ export abstract class BaseAgent implements IAgent {
595
614
  * no-op).
596
615
  */
597
616
  protected async createAgentCompletion(
598
- params: Parameters<GenericClient["createChatCompletion"]>[0],
617
+ params: CachedChatCompletion,
599
618
  options: {
600
619
  interruptValue?: CompletionResponse;
601
620
  } = {}
602
621
  ): Promise<CompletionResponse> {
603
622
  const { interruptValue } = options;
604
623
 
605
- const callPromise = this.getClient().createChatCompletion(params);
624
+ // If you change the tools, or any of this, you bust cache
625
+ // only safe to change messages
626
+ const defaultedParams: ChatCompletion = {
627
+ model: this.getModel(),
628
+ tools: this.getEnabledTools(),
629
+ tool_choice: "auto",
630
+ long_ttl_cache: this.runTime() > 300_000,
631
+ ...(this.reasoningEffort !== undefined && {
632
+ reasoning_effort: this.reasoningEffort,
633
+ }),
634
+ ...(this.summarizeReasoning !== undefined && {
635
+ reasoning_summary: this.summarizeReasoning,
636
+ }),
637
+
638
+ ...params,
639
+ };
640
+
641
+ const callPromise = this.getClient().createChatCompletion(defaultedParams);
606
642
  const response = interruptValue
607
643
  ? await this.makeInterruptible(callPromise, interruptValue)
608
644
  : await callPromise;
@@ -663,6 +699,7 @@ export abstract class BaseAgent implements IAgent {
663
699
  this.tools.callTool(toolCall, this.getEnabledToolNames(), {
664
700
  caller: this,
665
701
  taskId: this.currentTaskId,
702
+ parentTaskId: this.parentTaskId ?? undefined,
666
703
  }),
667
704
  interruptResult
668
705
  );
@@ -677,6 +714,14 @@ export abstract class BaseAgent implements IAgent {
677
714
 
678
715
  logMessages(messages: Message[]) {
679
716
  for (const message of messages) {
717
+ // Surface the model's reasoning summary (what it's "thinking") when the
718
+ // provider exposes one, so it appears alongside the assistant's output.
719
+ const reasoningSummary = (message as any).reasoning_summary;
720
+ if (message.role === "assistant" && reasoningSummary) {
721
+ this.agentEvents.emit(this.eventTypes.agentSay, {
722
+ message: `💭 ${reasoningSummary}`,
723
+ });
724
+ }
680
725
  if (message.role === "assistant" && message.content) {
681
726
  this.agentEvents.emit(this.eventTypes.agentSay, {
682
727
  message: message.content,
@@ -901,10 +946,7 @@ export abstract class BaseAgent implements IAgent {
901
946
  // would pre-empt the next tool call and drop its result. Any message
902
947
  // supplied with the poke was already added as a pending user message
903
948
  // above, so the agent will still see it on its next step.
904
- this.log(
905
- "No active interruptible operation to interrupt",
906
- "warn"
907
- );
949
+ this.log("No active interruptible operation to interrupt", "warn");
908
950
  }
909
951
  }
910
952
 
@@ -1435,12 +1477,18 @@ export abstract class BaseAgent implements IAgent {
1435
1477
  };
1436
1478
 
1437
1479
  const teardownPromise = Promise.resolve(source.start(emit)).catch((e) => {
1438
- this.log(`observe(${source.label}) start failed: ${e?.message ?? e}`, "warn");
1480
+ this.log(
1481
+ `observe(${source.label}) start failed: ${e?.message ?? e}`,
1482
+ "warn"
1483
+ );
1439
1484
  return undefined;
1440
1485
  });
1441
1486
 
1442
1487
  const expiry = maxDurationMs
1443
- ? setTimeout(() => this.stopObserving(id, "max duration reached"), maxDurationMs)
1488
+ ? setTimeout(
1489
+ () => this.stopObserving(id, "max duration reached"),
1490
+ maxDurationMs
1491
+ )
1444
1492
  : undefined;
1445
1493
  if (expiry && typeof (expiry as any).unref === "function") {
1446
1494
  (expiry as any).unref();
@@ -1601,7 +1649,15 @@ export abstract class BaseAgent implements IAgent {
1601
1649
  const response = await this.createAgentCompletion({
1602
1650
  model,
1603
1651
  messages: taskBreakdownMessages,
1604
- max_tokens: 2000,
1652
+ tools: this.getEnabledTools(),
1653
+ tool_choice: "auto",
1654
+ long_ttl_cache: this.runTime() > 300_000,
1655
+ ...(this.reasoningEffort !== undefined && {
1656
+ reasoning_effort: this.reasoningEffort,
1657
+ }),
1658
+ ...(this.summarizeReasoning !== undefined && {
1659
+ reasoning_summary: this.summarizeReasoning,
1660
+ }),
1605
1661
  });
1606
1662
 
1607
1663
  this.taskBreakdown = response.choices[0].message.content;
@@ -1644,7 +1700,6 @@ export abstract class BaseAgent implements IAgent {
1644
1700
  ] as Message[];
1645
1701
 
1646
1702
  const response = await this.createAgentCompletion({
1647
- model,
1648
1703
  messages: compressMessagesPayload,
1649
1704
  });
1650
1705
 
@@ -907,6 +907,46 @@ export function fixPatch(originalContent: string, patch: string): string {
907
907
  return finalPatch;
908
908
  }
909
909
 
910
+ /**
911
+ * Verify that auto-correction did not broaden or change the requested edit.
912
+ * Line locations and hunk headers may change, but the exact multiset of added
913
+ * and removed lines must remain the same.
914
+ */
915
+ export function autoCorrectionPreservesChanges(
916
+ originalContent: string,
917
+ updatedContent: string,
918
+ requestedPatch: string
919
+ ): boolean {
920
+ const changedLines = (patch: string, prefix: "+" | "-"): string[] =>
921
+ parseHunks(patch)
922
+ .flatMap((hunk) =>
923
+ hunk.lines
924
+ .filter((line) => line.startsWith(prefix))
925
+ .map((line) => line.slice(1))
926
+ )
927
+ .sort();
928
+
929
+ const actualPatch = createTwoFilesPatch(
930
+ "original",
931
+ "updated",
932
+ originalContent,
933
+ updatedContent,
934
+ "",
935
+ "",
936
+ { context: 0 }
937
+ );
938
+
939
+ const requestedAdditions = changedLines(requestedPatch, "+");
940
+ const requestedRemovals = changedLines(requestedPatch, "-");
941
+ const actualAdditions = changedLines(actualPatch, "+");
942
+ const actualRemovals = changedLines(actualPatch, "-");
943
+
944
+ return (
945
+ JSON.stringify(actualAdditions) === JSON.stringify(requestedAdditions) &&
946
+ JSON.stringify(actualRemovals) === JSON.stringify(requestedRemovals)
947
+ );
948
+ }
949
+
910
950
  // --- Existing Application/Utility Code (Keep as is, ensure imports/exports are correct) ---
911
951
 
912
952
  export async function savePatchError(
@@ -976,9 +1016,32 @@ export async function patchFile(
976
1016
  originalContent = await readFile(filePath, "utf8"); // Use async read
977
1017
  }
978
1018
 
979
- let updatedContent = applyPatch(originalContent, patch);
980
- let appliedPatch = patch; // Keep track of which patch succeeded
981
1019
  const patchHunks = parseHunks(patch);
1020
+ // `diff.applyPatch` can report success for malformed hunks whose declared
1021
+ // line counts disagree with their bodies. In that case it consumes the
1022
+ // wrong number of lines and can move additions across scope boundaries.
1023
+ // Send those hunks through our re-anchoring logic instead of trusting the
1024
+ // library's successful-but-corrupt result.
1025
+ const hasInvalidHunkCounts = patchHunks.some((hunk) => {
1026
+ const originalBodyCount = hunk.lines.filter(
1027
+ (line) =>
1028
+ !line.startsWith("+") &&
1029
+ line !== "\"
1030
+ ).length;
1031
+ const newBodyCount = hunk.lines.filter(
1032
+ (line) =>
1033
+ !line.startsWith("-") &&
1034
+ line !== "\"
1035
+ ).length;
1036
+ return (
1037
+ originalBodyCount !== hunk.originalLineCount ||
1038
+ newBodyCount !== hunk.newLineCount
1039
+ );
1040
+ });
1041
+ let updatedContent = hasInvalidHunkCounts
1042
+ ? false
1043
+ : applyPatch(originalContent, patch);
1044
+ let appliedPatch = patch; // Keep track of which patch succeeded
982
1045
  const patchHasAdditions = patchHunks.some((h) => h.additions.length > 0);
983
1046
  const patchHasDeletions = patchHunks.some((h) => h.subtractions.length > 0);
984
1047
 
@@ -1113,6 +1176,25 @@ export async function patchFile(
1113
1176
 
1114
1177
  const wasFixed = appliedPatch !== patch;
1115
1178
 
1179
+ // Auto-correction may re-anchor hunks, but it must never invent removals,
1180
+ // drop requested edits, or move lines by representing them as extra edits.
1181
+ // Reject before events or disk writes if its actual delta differs from the
1182
+ // submitted patch.
1183
+ if (
1184
+ wasFixed &&
1185
+ !autoCorrectionPreservesChanges(
1186
+ originalContent,
1187
+ updatedContent as string,
1188
+ patch
1189
+ )
1190
+ ) {
1191
+ return [
1192
+ `❌ Patch failed: auto-correction changed the requested edit, so no changes were written.`,
1193
+ `File: ${filePath} (${originalContent.split("\n").length} lines)`,
1194
+ `Tip: Re-read the file and submit smaller hunks with unique surrounding context.`,
1195
+ ].join("\n");
1196
+ }
1197
+
1116
1198
  const eventResults: any[] = [];
1117
1199
  // Emit pre-edit blocking event
1118
1200
  if (context.Events) {
@@ -1,10 +1,85 @@
1
1
  import { getConfiguredEmbeddings } from "../../embeddings";
2
2
  import { execAsync } from "./execCommand";
3
3
 
4
+ // Max number of matching lines (with full text) we show per file before we
5
+ // summarize the rest as a list of line numbers.
6
+ const PER_FILE_LINE_LIMIT = 5;
7
+
8
+ interface FileMatches {
9
+ file: string;
10
+ // [lineNumber, lineText]
11
+ lines: Array<[number, string]>;
12
+ }
13
+
14
+ /**
15
+ * Parse `ag --group --numbers` style output into per-file match groups.
16
+ * With --group, ag prints the file path on its own line, followed by
17
+ * `<lineNumber>:<text>` lines, then a blank line between files.
18
+ */
19
+ function parseAgGroupedOutput(stdout: string): FileMatches[] {
20
+ const results: FileMatches[] = [];
21
+ let current: FileMatches | null = null;
22
+
23
+ for (const rawLine of stdout.split("\n")) {
24
+ if (rawLine === "") {
25
+ // Blank line separates file groups
26
+ current = null;
27
+ continue;
28
+ }
29
+
30
+ const numberMatch = rawLine.match(/^(\d+):([\s\S]*)$/);
31
+ if (numberMatch && current) {
32
+ current.lines.push([Number(numberMatch[1]), numberMatch[2]]);
33
+ } else {
34
+ // Treat as a file header line
35
+ current = { file: rawLine, lines: [] };
36
+ results.push(current);
37
+ }
38
+ }
39
+
40
+ return results;
41
+ }
42
+
43
+ /**
44
+ * Format the parsed matches into a helpful, readable report. Instead of ag's
45
+ * "Too many matches ... Skipping" error, we report the total match count, the
46
+ * line numbers, and cap the displayed lines at PER_FILE_LINE_LIMIT.
47
+ */
48
+ function formatMatches(files: FileMatches[]): string {
49
+ if (files.length === 0) return "No matches found.";
50
+
51
+ const sections: string[] = [];
52
+
53
+ for (const { file, lines } of files) {
54
+ const total = lines.length;
55
+ const shown = lines.slice(0, PER_FILE_LINE_LIMIT);
56
+
57
+ const header = `${file} (${total} match${total === 1 ? "" : "es"})`;
58
+ const body = shown
59
+ .map(([num, text]) => `${num}:${text}`)
60
+ .join("\n");
61
+
62
+ let section = `${header}\n${body}`;
63
+
64
+ if (total > PER_FILE_LINE_LIMIT) {
65
+ const remaining = lines.slice(PER_FILE_LINE_LIMIT);
66
+ const remainingLineNumbers = remaining.map(([num]) => num);
67
+ section +=
68
+ `\n... ${remaining.length} more match${
69
+ remaining.length === 1 ? "" : "es"
70
+ } (showing first ${PER_FILE_LINE_LIMIT} lines). ` +
71
+ `Remaining matches on lines: [${remainingLineNumbers.join(", ")}]`;
72
+ }
73
+
74
+ sections.push(section);
75
+ }
76
+
77
+ return sections.join("\n\n");
78
+ }
79
+
4
80
  export async function textSearch(searchTerm) {
5
81
  try {
6
82
  // Escape the search term for safe shell usage
7
- // Replace single quotes with '\'' which closes quote, adds escaped quote, reopens quote
8
83
  // 1) Normalize whitespace (turn newlines/tabs into spaces)
9
84
  const normalized = String(searchTerm)
10
85
  .replace(/\r\n?/g, "\n") // normalize CRLF/CR → LF
@@ -16,8 +91,28 @@ export async function textSearch(searchTerm) {
16
91
  // 2) Escape single quotes for safe single-quoted shell arg
17
92
  const escapedTerm = normalized.replace(/'/g, "'\\''");
18
93
 
19
- const command = `ag -m 3 -Q '${escapedTerm}'`;
20
- return await execAsync(command);
94
+ // Use --group + --numbers so we can parse per-file matches ourselves and
95
+ // produce a richer summary (counts + line numbers) instead of ag's
96
+ // unhelpful "Too many matches" error. We intentionally do NOT pass -m here.
97
+ const command = `ag --group --numbers -Q '${escapedTerm}'`;
98
+ const { stdout, stderr } = await execAsync(command);
99
+
100
+ const files = parseAgGroupedOutput(stdout || "");
101
+ const formatted = formatMatches(files);
102
+
103
+ // Preserve any non-"too many matches" stderr (real warnings/errors), but
104
+ // drop ag's per-file "Too many matches ... Skipping" noise since we now
105
+ // handle match limits ourselves.
106
+ const filteredStderr = (stderr || "")
107
+ .split("\n")
108
+ .filter((line) => !/Too many matches/i.test(line))
109
+ .join("\n")
110
+ .trim();
111
+
112
+ return {
113
+ stdout: formatted,
114
+ stderr: filteredStderr,
115
+ };
21
116
  } catch (err) {
22
117
  console.log(
23
118
  "Falling back to embeddings text search since ag was not available"