@tyvm/knowhow 0.0.144 → 0.0.146

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 (253) 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/CliChatService.ts +16 -0
  8. package/src/chat/modules/AgentModule.ts +159 -29
  9. package/src/chat/modules/SessionsModule.ts +13 -5
  10. package/src/chat/types.ts +15 -0
  11. package/src/chat.ts +11 -0
  12. package/src/cli.ts +2 -0
  13. package/src/clients/anthropic.ts +80 -1
  14. package/src/clients/contextLimits.ts +40 -0
  15. package/src/clients/fireworks.ts +3 -0
  16. package/src/clients/gemini.ts +67 -5
  17. package/src/clients/http.ts +85 -4
  18. package/src/clients/index.ts +4 -0
  19. package/src/clients/openai.ts +186 -35
  20. package/src/clients/pricing/fireworks.ts +50 -1
  21. package/src/clients/pricing/index.ts +1 -1
  22. package/src/clients/pricing/models.ts +3 -1
  23. package/src/clients/pricing/openai.ts +2 -2
  24. package/src/clients/pricing/qwen.ts +80 -0
  25. package/src/clients/qwen.ts +16 -0
  26. package/src/clients/types.ts +99 -8
  27. package/src/clients/xai.ts +96 -8
  28. package/src/commands/agent.ts +29 -1
  29. package/src/commands/services.ts +49 -3
  30. package/src/index.ts +9 -0
  31. package/src/plugins/plugins.ts +16 -0
  32. package/src/processors/Base64ImageDetector.ts +46 -7
  33. package/src/processors/GemmaToolProcessor.ts +163 -0
  34. package/src/processors/TokenCompressor.ts +40 -8
  35. package/src/processors/ToolResponseCache.ts +17 -8
  36. package/src/processors/index.ts +1 -0
  37. package/src/processors/tools/boundedToolResponse.ts +49 -0
  38. package/src/processors/tools/grepToolResponse.ts +164 -38
  39. package/src/processors/tools/index.ts +2 -0
  40. package/src/processors/tools/jqToolResponse.ts +40 -10
  41. package/src/processors/tools/listStoredToolResponses.ts +51 -10
  42. package/src/processors/tools/tailToolResponse.ts +85 -17
  43. package/src/services/AgentSyncFs.ts +37 -0
  44. package/src/services/LazyToolsService.ts +7 -0
  45. package/src/services/Mcp.ts +9 -2
  46. package/src/services/RuntimeReloadService.ts +31 -0
  47. package/src/services/SessionManager.ts +14 -0
  48. package/src/services/Tools.ts +14 -0
  49. package/src/services/TracingService.ts +116 -0
  50. package/src/services/index.ts +8 -0
  51. package/src/services/modules/computerUse.ts +216 -0
  52. package/src/services/modules/index.ts +90 -9
  53. package/src/services/modules/types.ts +31 -1
  54. package/src/types.ts +3 -0
  55. package/src/util/Trace.ts +236 -0
  56. package/tests/clients/contextLimits.test.ts +4 -0
  57. package/tests/clients/fireworks.test.ts +65 -0
  58. package/tests/clients/openai.images.test.ts +105 -0
  59. package/tests/clients/pricing.test.ts +6 -0
  60. package/tests/clients/reasoningEffort.test.ts +79 -0
  61. package/tests/compressor/toolResponseCache.test.ts +2 -1
  62. package/tests/compressor/usageBase64.json +159 -0
  63. package/tests/compressor/usageBase64.test.ts +175 -0
  64. package/tests/manual/clients/completions.test.ts +27 -10
  65. package/tests/patching/fixtures/transcript-mangles/agent-sync-fs-nested-catch.json +9 -0
  66. package/tests/patching/fixtures/transcript-mangles/automation-functions-crossed.json +9 -0
  67. package/tests/patching/fixtures/transcript-mangles/fruit-ninja-confirmed-retirement.json +8 -0
  68. package/tests/patching/patchFileOutput.test.ts +112 -1
  69. package/tests/patching/transcriptMangleRegression.test.ts +125 -0
  70. package/tests/processors/Base64ImageDetector.test.ts +36 -1
  71. package/tests/processors/TokenCompressor.test.ts +17 -3
  72. package/tests/processors/tools/boundedRetrievalTools.test.ts +68 -0
  73. package/tests/processors/tools/grepToolResponse.test.ts +88 -1
  74. package/tests/unit/plugins/pluginLoading.test.ts +31 -0
  75. package/tests/unit/services/RuntimeReloadService.test.ts +29 -0
  76. package/ts_build/package.json +1 -1
  77. package/ts_build/src/agents/base/base.d.ts +8 -1
  78. package/ts_build/src/agents/base/base.js +37 -3
  79. package/ts_build/src/agents/base/base.js.map +1 -1
  80. package/ts_build/src/agents/tools/patch.d.ts +1 -0
  81. package/ts_build/src/agents/tools/patch.js +35 -2
  82. package/ts_build/src/agents/tools/patch.js.map +1 -1
  83. package/ts_build/src/agents/tools/textSearch.js +56 -2
  84. package/ts_build/src/agents/tools/textSearch.js.map +1 -1
  85. package/ts_build/src/chat/CliChatService.js +15 -0
  86. package/ts_build/src/chat/CliChatService.js.map +1 -1
  87. package/ts_build/src/chat/modules/AgentModule.d.ts +14 -1
  88. package/ts_build/src/chat/modules/AgentModule.js +86 -24
  89. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  90. package/ts_build/src/chat/modules/SessionsModule.js +5 -6
  91. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  92. package/ts_build/src/chat/types.d.ts +12 -0
  93. package/ts_build/src/chat.js +7 -0
  94. package/ts_build/src/chat.js.map +1 -1
  95. package/ts_build/src/cli.d.ts +2 -1
  96. package/ts_build/src/cli.js +1 -0
  97. package/ts_build/src/cli.js.map +1 -1
  98. package/ts_build/src/clients/anthropic.d.ts +1 -0
  99. package/ts_build/src/clients/anthropic.js +40 -1
  100. package/ts_build/src/clients/anthropic.js.map +1 -1
  101. package/ts_build/src/clients/contextLimits.js +25 -0
  102. package/ts_build/src/clients/contextLimits.js.map +1 -1
  103. package/ts_build/src/clients/fireworks.js +1 -0
  104. package/ts_build/src/clients/fireworks.js.map +1 -1
  105. package/ts_build/src/clients/gemini.js +56 -4
  106. package/ts_build/src/clients/gemini.js.map +1 -1
  107. package/ts_build/src/clients/http.d.ts +2 -0
  108. package/ts_build/src/clients/http.js +66 -9
  109. package/ts_build/src/clients/http.js.map +1 -1
  110. package/ts_build/src/clients/index.d.ts +1 -0
  111. package/ts_build/src/clients/index.js +4 -0
  112. package/ts_build/src/clients/index.js.map +1 -1
  113. package/ts_build/src/clients/openai.d.ts +6 -1
  114. package/ts_build/src/clients/openai.js +135 -35
  115. package/ts_build/src/clients/openai.js.map +1 -1
  116. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -1
  117. package/ts_build/src/clients/pricing/fireworks.js +33 -1
  118. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  119. package/ts_build/src/clients/pricing/index.d.ts +1 -1
  120. package/ts_build/src/clients/pricing/index.js +6 -2
  121. package/ts_build/src/clients/pricing/index.js.map +1 -1
  122. package/ts_build/src/clients/pricing/models.d.ts +2 -1
  123. package/ts_build/src/clients/pricing/models.js +4 -1
  124. package/ts_build/src/clients/pricing/models.js.map +1 -1
  125. package/ts_build/src/clients/pricing/openai.js +2 -2
  126. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  127. package/ts_build/src/clients/pricing/qwen.d.ts +5 -0
  128. package/ts_build/src/clients/pricing/qwen.js +30 -0
  129. package/ts_build/src/clients/pricing/qwen.js.map +1 -0
  130. package/ts_build/src/clients/qwen.d.ts +4 -0
  131. package/ts_build/src/clients/qwen.js +15 -0
  132. package/ts_build/src/clients/qwen.js.map +1 -0
  133. package/ts_build/src/clients/types.d.ts +16 -1
  134. package/ts_build/src/clients/types.js +14 -0
  135. package/ts_build/src/clients/types.js.map +1 -1
  136. package/ts_build/src/clients/xai.d.ts +2 -0
  137. package/ts_build/src/clients/xai.js +79 -13
  138. package/ts_build/src/clients/xai.js.map +1 -1
  139. package/ts_build/src/commands/agent.js +15 -1
  140. package/ts_build/src/commands/agent.js.map +1 -1
  141. package/ts_build/src/commands/services.d.ts +2 -0
  142. package/ts_build/src/commands/services.js +45 -3
  143. package/ts_build/src/commands/services.js.map +1 -1
  144. package/ts_build/src/index.d.ts +2 -0
  145. package/ts_build/src/index.js +1 -0
  146. package/ts_build/src/index.js.map +1 -1
  147. package/ts_build/src/plugins/plugins.d.ts +1 -0
  148. package/ts_build/src/plugins/plugins.js +14 -0
  149. package/ts_build/src/plugins/plugins.js.map +1 -1
  150. package/ts_build/src/processors/Base64ImageDetector.js +22 -3
  151. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  152. package/ts_build/src/processors/GemmaToolProcessor.d.ts +14 -0
  153. package/ts_build/src/processors/GemmaToolProcessor.js +97 -0
  154. package/ts_build/src/processors/GemmaToolProcessor.js.map +1 -0
  155. package/ts_build/src/processors/TokenCompressor.js +15 -6
  156. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  157. package/ts_build/src/processors/ToolResponseCache.d.ts +3 -3
  158. package/ts_build/src/processors/ToolResponseCache.js +8 -8
  159. package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
  160. package/ts_build/src/processors/index.d.ts +1 -0
  161. package/ts_build/src/processors/index.js +3 -1
  162. package/ts_build/src/processors/index.js.map +1 -1
  163. package/ts_build/src/processors/tools/boundedToolResponse.d.ts +7 -0
  164. package/ts_build/src/processors/tools/boundedToolResponse.js +27 -0
  165. package/ts_build/src/processors/tools/boundedToolResponse.js.map +1 -0
  166. package/ts_build/src/processors/tools/grepToolResponse.d.ts +4 -0
  167. package/ts_build/src/processors/tools/grepToolResponse.js +101 -25
  168. package/ts_build/src/processors/tools/grepToolResponse.js.map +1 -1
  169. package/ts_build/src/processors/tools/index.d.ts +2 -2
  170. package/ts_build/src/processors/tools/index.js.map +1 -1
  171. package/ts_build/src/processors/tools/jqToolResponse.d.ts +5 -1
  172. package/ts_build/src/processors/tools/jqToolResponse.js +26 -9
  173. package/ts_build/src/processors/tools/jqToolResponse.js.map +1 -1
  174. package/ts_build/src/processors/tools/listStoredToolResponses.d.ts +7 -1
  175. package/ts_build/src/processors/tools/listStoredToolResponses.js +39 -9
  176. package/ts_build/src/processors/tools/listStoredToolResponses.js.map +1 -1
  177. package/ts_build/src/processors/tools/tailToolResponse.d.ts +5 -0
  178. package/ts_build/src/processors/tools/tailToolResponse.js +56 -13
  179. package/ts_build/src/processors/tools/tailToolResponse.js.map +1 -1
  180. package/ts_build/src/services/AgentSyncFs.js +17 -0
  181. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  182. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  183. package/ts_build/src/services/LazyToolsService.js +5 -0
  184. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  185. package/ts_build/src/services/Mcp.js +8 -2
  186. package/ts_build/src/services/Mcp.js.map +1 -1
  187. package/ts_build/src/services/RuntimeReloadService.d.ts +12 -0
  188. package/ts_build/src/services/RuntimeReloadService.js +26 -0
  189. package/ts_build/src/services/RuntimeReloadService.js.map +1 -0
  190. package/ts_build/src/services/SessionManager.js +12 -0
  191. package/ts_build/src/services/SessionManager.js.map +1 -1
  192. package/ts_build/src/services/Tools.d.ts +3 -0
  193. package/ts_build/src/services/Tools.js +5 -0
  194. package/ts_build/src/services/Tools.js.map +1 -1
  195. package/ts_build/src/services/TracingService.d.ts +16 -0
  196. package/ts_build/src/services/TracingService.js +35 -0
  197. package/ts_build/src/services/TracingService.js.map +1 -0
  198. package/ts_build/src/services/index.d.ts +6 -0
  199. package/ts_build/src/services/index.js +7 -1
  200. package/ts_build/src/services/index.js.map +1 -1
  201. package/ts_build/src/services/modules/computerUse.d.ts +163 -0
  202. package/ts_build/src/services/modules/computerUse.js +3 -0
  203. package/ts_build/src/services/modules/computerUse.js.map +1 -0
  204. package/ts_build/src/services/modules/index.d.ts +6 -0
  205. package/ts_build/src/services/modules/index.js +49 -6
  206. package/ts_build/src/services/modules/index.js.map +1 -1
  207. package/ts_build/src/services/modules/types.d.ts +7 -0
  208. package/ts_build/src/types.d.ts +3 -0
  209. package/ts_build/src/types.js.map +1 -1
  210. package/ts_build/src/util/Trace.d.ts +13 -0
  211. package/ts_build/src/util/Trace.js +107 -0
  212. package/ts_build/src/util/Trace.js.map +1 -0
  213. package/ts_build/src/workers/tools/index.d.ts +1 -1
  214. package/ts_build/tests/clients/contextLimits.test.js +2 -0
  215. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  216. package/ts_build/tests/clients/fireworks.test.d.ts +1 -0
  217. package/ts_build/tests/clients/fireworks.test.js +65 -0
  218. package/ts_build/tests/clients/fireworks.test.js.map +1 -0
  219. package/ts_build/tests/clients/openai.images.test.d.ts +1 -0
  220. package/ts_build/tests/clients/openai.images.test.js +94 -0
  221. package/ts_build/tests/clients/openai.images.test.js.map +1 -0
  222. package/ts_build/tests/clients/pricing.test.js +4 -0
  223. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  224. package/ts_build/tests/clients/reasoningEffort.test.d.ts +1 -0
  225. package/ts_build/tests/clients/reasoningEffort.test.js +68 -0
  226. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -0
  227. package/ts_build/tests/compressor/toolResponseCache.test.js +1 -1
  228. package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -1
  229. package/ts_build/tests/compressor/usageBase64.test.d.ts +1 -0
  230. package/ts_build/tests/compressor/usageBase64.test.js +109 -0
  231. package/ts_build/tests/compressor/usageBase64.test.js.map +1 -0
  232. package/ts_build/tests/manual/clients/completions.test.js +19 -6
  233. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
  234. package/ts_build/tests/patching/patchFileOutput.test.js +82 -0
  235. package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -1
  236. package/ts_build/tests/patching/transcriptMangleRegression.test.d.ts +1 -0
  237. package/ts_build/tests/patching/transcriptMangleRegression.test.js +104 -0
  238. package/ts_build/tests/patching/transcriptMangleRegression.test.js.map +1 -0
  239. package/ts_build/tests/processors/Base64ImageDetector.test.js +24 -0
  240. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  241. package/ts_build/tests/processors/TokenCompressor.test.js +11 -2
  242. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  243. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.d.ts +1 -0
  244. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js +57 -0
  245. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js.map +1 -0
  246. package/ts_build/tests/processors/tools/grepToolResponse.test.js +37 -0
  247. package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -1
  248. package/ts_build/tests/unit/plugins/pluginLoading.test.js +59 -0
  249. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  250. package/ts_build/tests/unit/services/RuntimeReloadService.test.d.ts +1 -0
  251. package/ts_build/tests/unit/services/RuntimeReloadService.test.js +26 -0
  252. package/ts_build/tests/unit/services/RuntimeReloadService.test.js.map +1 -0
  253. package/x-home.png +0 -0
@@ -6,11 +6,33 @@ export interface GrepOptions {
6
6
  contextBefore?: number;
7
7
  contextAfter?: number;
8
8
  maxResults?: number;
9
+ resultOffset?: number;
10
+ maxCharacters?: number;
11
+ lineCharacterOffset?: number;
12
+ maxLineCharacters?: number;
9
13
  }
10
14
 
11
- /**
12
- * Grep through tool response data to find matching lines
13
- */
15
+ const DEFAULT_MAX_RESULTS = 100;
16
+ const MAX_RESULTS = 1000;
17
+ const DEFAULT_MAX_CHARACTERS = 20_000;
18
+ const MAX_CHARACTERS = 50_000;
19
+ const DEFAULT_MAX_LINE_CHARACTERS = 4_000;
20
+ const MAX_LINE_CHARACTERS = 10_000;
21
+ const FOOTER_RESERVE = 700;
22
+
23
+ function boundedInteger(
24
+ value: number | undefined,
25
+ fallback: number,
26
+ min: number,
27
+ max: number
28
+ ): number {
29
+ return Math.min(
30
+ max,
31
+ Math.max(min, Number.isFinite(value) ? Math.floor(value as number) : fallback)
32
+ );
33
+ }
34
+
35
+ /** Search stored tool-response text without ever returning an unbounded result. */
14
36
  export async function executeGrep(
15
37
  data: string,
16
38
  toolCallId: string,
@@ -26,51 +48,145 @@ export async function executeGrep(
26
48
  return name ? `${id} (${name})` : id;
27
49
  })
28
50
  .join("\n - ");
29
- return `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`;
51
+ const maxCharacters = boundedInteger(
52
+ options?.maxCharacters,
53
+ DEFAULT_MAX_CHARACTERS,
54
+ 1000,
55
+ MAX_CHARACTERS
56
+ );
57
+ return `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`.slice(
58
+ 0,
59
+ maxCharacters
60
+ );
30
61
  }
31
62
 
32
63
  try {
33
64
  const lines = data.split("\n");
34
65
  const matchedResults: string[] = [];
35
- const ignoreCase = options?.ignoreCase || false;
36
- const invertMatch = options?.invertMatch || false;
37
- const contextBefore = options?.contextBefore || 0;
38
- const contextAfter = options?.contextAfter || 0;
39
- const maxResults = options?.maxResults || 1000;
40
-
41
- // Create regex from pattern
42
- const flags = ignoreCase ? "i" : "";
43
- const regex = new RegExp(pattern, flags);
44
-
45
- for (let i = 0; i < lines.length && matchedResults.length < maxResults; i++) {
46
- const line = lines[i];
47
- const matches = regex.test(line);
48
- const shouldInclude = invertMatch ? !matches : matches;
49
-
50
- if (shouldInclude) {
51
- const startIdx = Math.max(0, i - contextBefore);
52
- const endIdx = Math.min(lines.length - 1, i + contextAfter);
53
-
54
- const contextLines = [];
66
+ const ignoreCase = options?.ignoreCase ?? false;
67
+ const invertMatch = options?.invertMatch ?? false;
68
+ const contextBefore = boundedInteger(options?.contextBefore, 0, 0, 1000);
69
+ const contextAfter = boundedInteger(options?.contextAfter, 0, 0, 1000);
70
+ const maxResults = boundedInteger(options?.maxResults, DEFAULT_MAX_RESULTS, 1, MAX_RESULTS);
71
+ const resultOffset = boundedInteger(options?.resultOffset, 0, 0, Number.MAX_SAFE_INTEGER);
72
+ const maxCharacters = boundedInteger(
73
+ options?.maxCharacters,
74
+ DEFAULT_MAX_CHARACTERS,
75
+ 1000,
76
+ MAX_CHARACTERS
77
+ );
78
+ const lineCharacterOffset = boundedInteger(
79
+ options?.lineCharacterOffset,
80
+ 0,
81
+ 0,
82
+ Number.MAX_SAFE_INTEGER
83
+ );
84
+ const maxLineCharacters = boundedInteger(
85
+ options?.maxLineCharacters,
86
+ DEFAULT_MAX_LINE_CHARACTERS,
87
+ 100,
88
+ MAX_LINE_CHARACTERS
89
+ );
90
+ const contentBudget = Math.max(1, maxCharacters - FOOTER_RESERVE);
91
+ const regex = new RegExp(pattern, ignoreCase ? "i" : "");
92
+
93
+ let totalMatches = 0;
94
+ let outputCharacters = 0;
95
+ let outputTruncated = false;
96
+ const pagedLineNumbers = new Set<number>();
97
+
98
+ for (let i = 0; i < lines.length; i++) {
99
+ const matches = regex.test(lines[i]);
100
+ if (invertMatch ? matches : !matches) continue;
101
+
102
+ const resultIndex = totalMatches++;
103
+ const startIdx = Math.max(0, i - contextBefore);
104
+ const endIdx = Math.min(lines.length - 1, i + contextAfter);
105
+
106
+ if (
107
+ resultIndex >= resultOffset &&
108
+ matchedResults.length < maxResults &&
109
+ !outputTruncated
110
+ ) {
111
+ const contextLines: string[] = [];
55
112
  for (let j = startIdx; j <= endIdx; j++) {
113
+ const sourceLine = lines[j];
114
+ const visibleLine = sourceLine.slice(
115
+ lineCharacterOffset,
116
+ lineCharacterOffset + maxLineCharacters
117
+ );
56
118
  const prefix = j === i ? "> " : " ";
57
- contextLines.push(`${prefix}${j + 1}: ${lines[j]}`);
119
+ let rendered = `${prefix}${j + 1}: ${visibleLine}`;
120
+
121
+ if (lineCharacterOffset >= sourceLine.length && sourceLine.length > 0) {
122
+ rendered += ` [no characters at offset ${lineCharacterOffset}; line length=${sourceLine.length}]`;
123
+ } else if (lineCharacterOffset + maxLineCharacters < sourceLine.length) {
124
+ pagedLineNumbers.add(j + 1);
125
+ rendered += `\n [line ${j + 1} truncated; characters ${lineCharacterOffset}-${lineCharacterOffset + visibleLine.length - 1} of ${sourceLine.length}]`;
126
+ }
127
+ contextLines.push(rendered);
128
+ }
129
+
130
+ const separatorLength = matchedResults.length > 0 ? 5 : 0;
131
+ const result = contextLines.join("\n");
132
+ const remaining = contentBudget - outputCharacters - separatorLength;
133
+ if (result.length > remaining) {
134
+ if (matchedResults.length === 0 && remaining > 80) {
135
+ matchedResults.push(
136
+ `${result.slice(0, remaining - 48)}\n[result page truncated by maxCharacters]`
137
+ );
138
+ outputCharacters = contentBudget;
139
+ }
140
+ outputTruncated = true;
141
+ } else {
142
+ matchedResults.push(result);
143
+ outputCharacters += separatorLength + result.length;
58
144
  }
59
-
60
- matchedResults.push(contextLines.join("\n"));
61
-
62
- // Skip ahead to avoid overlapping context
63
- i += contextAfter;
64
145
  }
146
+
147
+ // Avoid duplicate result blocks when their after-context overlaps a match.
148
+ i += contextAfter;
65
149
  }
66
150
 
67
- if (matchedResults.length === 0) {
68
- return `No matches found for pattern "${pattern}" in toolCallId "${toolCallId}"`;
151
+ if (totalMatches === 0) {
152
+ const preview = pattern.length > 200 ? `${pattern.slice(0, 200)}…` : pattern;
153
+ return `No matches found for pattern "${preview}" in toolCallId "${toolCallId}"`.slice(
154
+ 0,
155
+ maxCharacters
156
+ );
157
+ }
158
+
159
+ const consumedResults = matchedResults.length;
160
+ const nextResultOffset = resultOffset + consumedResults;
161
+ const hasMoreResults = nextResultOffset < totalMatches;
162
+ const range = consumedResults
163
+ ? `${resultOffset + 1}-${nextResultOffset}`
164
+ : `none (offset ${resultOffset} is past the final result)`;
165
+ const footerLines = [
166
+ "",
167
+ `[grep page: results ${range} of ${totalMatches}; ${consumedResults} returned; maxCharacters=${maxCharacters}]`,
168
+ ];
169
+
170
+ if (pagedLineNumbers.size > 0) {
171
+ footerLines.push(
172
+ `[Long source line(s) ${Array.from(pagedLineNumbers).join(", ")} were sliced. Repeat with options.lineCharacterOffset=${lineCharacterOffset + maxLineCharacters} to read their next characters.]`
173
+ );
174
+ }
175
+ if (hasMoreResults) {
176
+ footerLines.push(
177
+ `[More matches available. Repeat with options.resultOffset=${nextResultOffset}.]`
178
+ );
179
+ } else {
180
+ footerLines.push("[End of matches.]");
69
181
  }
70
182
 
71
- return matchedResults.join("\n---\n");
72
- } catch (error: any) {
73
- return `Grep Error: ${error.message}`;
183
+ return (matchedResults.join("\n---\n") + footerLines.join("\n")).slice(
184
+ 0,
185
+ maxCharacters
186
+ );
187
+ } catch (error: unknown) {
188
+ const message = error instanceof Error ? error.message : String(error);
189
+ return `Grep Error: ${message.slice(0, 500)}`;
74
190
  }
75
191
  }
76
192
 
@@ -79,7 +195,7 @@ export const grepToolResponseDefinition: Tool = {
79
195
  function: {
80
196
  name: "grepToolResponse",
81
197
  description:
82
- "Search through a stored tool response using grep-like pattern matching. Useful when a tool response is too large and you need to find specific lines or content without re-running the tool. Returns matching lines with optional context.",
198
+ "Search a stored tool response using a regular expression. Results and individual source lines are bounded and paginated. Follow the returned resultOffset or lineCharacterOffset instruction to retrieve more.",
83
199
  parameters: {
84
200
  type: "object",
85
201
  positional: true,
@@ -94,8 +210,18 @@ export const grepToolResponseDefinition: Tool = {
94
210
  },
95
211
  options: {
96
212
  type: "object",
97
- description: "Optional grep settings: ignoreCase (boolean), invertMatch (boolean), contextBefore (number), contextAfter (number), maxResults (number, default: 1000)",
98
- properties: {},
213
+ description: "Optional grep, output-bound, and pagination settings.",
214
+ properties: {
215
+ ignoreCase: { type: "boolean", description: "Case-insensitive matching." },
216
+ invertMatch: { type: "boolean", description: "Return non-matching lines." },
217
+ contextBefore: { type: "number", description: "Context lines before a match (maximum 1000)." },
218
+ contextAfter: { type: "number", description: "Context lines after a match (maximum 1000)." },
219
+ maxResults: { type: "number", description: "Matches per page (default 100, maximum 1000)." },
220
+ resultOffset: { type: "number", description: "Zero-based match offset for the next result page." },
221
+ maxCharacters: { type: "number", description: "Response size (default 20000, hard maximum 50000)." },
222
+ lineCharacterOffset: { type: "number", description: "Character offset within every source line, for paging very long lines." },
223
+ maxLineCharacters: { type: "number", description: "Characters shown per source line (default 4000, maximum 10000)." },
224
+ },
99
225
  },
100
226
  },
101
227
  required: ["toolCallId", "pattern"],
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  jqToolResponseDefinition,
3
3
  executeJqQuery,
4
+ JqOptions,
4
5
  } from "./jqToolResponse";
5
6
 
6
7
  export {
@@ -18,4 +19,5 @@ export {
18
19
  export {
19
20
  listStoredToolResponsesDefinition,
20
21
  executeListStoredToolResponses,
22
+ ListStoredToolResponsesOptions,
21
23
  } from "./listStoredToolResponses";
@@ -1,5 +1,11 @@
1
1
  import { Tool } from "../../clients";
2
2
  import * as jq from "node-jq";
3
+ import { paginateToolResponse } from "./boundedToolResponse";
4
+
5
+ export interface JqOptions {
6
+ characterOffset?: number;
7
+ maxCharacters?: number;
8
+ }
3
9
 
4
10
  /**
5
11
  * Attempts to parse content as JSON and returns parsed object if successful
@@ -47,7 +53,8 @@ export async function executeJqQuery(
47
53
  toolCallId: string,
48
54
  jqQuery: string,
49
55
  availableIds: string[],
50
- toolNameMap?: { [toolCallId: string]: string }
56
+ toolNameMap?: { [toolCallId: string]: string },
57
+ options?: JqOptions
51
58
  ): Promise<string> {
52
59
  if (!data) {
53
60
  const idList = availableIds
@@ -56,30 +63,39 @@ export async function executeJqQuery(
56
63
  return name ? `${id} (${name})` : id;
57
64
  })
58
65
  .join("\n - ");
59
- return `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`;
66
+ return paginateToolResponse(
67
+ `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`,
68
+ options,
69
+ "available toolCallId list"
70
+ );
60
71
  }
61
72
 
62
73
  try {
63
74
  // First parse the stored string as JSON, then handle nested JSON strings
64
75
  const jsonData = tryParseJson(data);
65
76
  if (!jsonData) {
66
- return `Error: Tool response data is not valid JSON for toolCallId "${toolCallId}"`;
77
+ return paginateToolResponse(
78
+ `Error: Tool response data is not valid JSON for toolCallId "${toolCallId}"`,
79
+ options,
80
+ "jq input error"
81
+ );
67
82
  }
68
83
  const parsedData = parseNestedJsonStrings(jsonData);
69
84
 
70
85
  // Execute JQ query
71
86
  const result = await jq.run(jqQuery, parsedData, { input: "json" });
72
87
 
73
- // Handle the result based on its type
88
+ let rendered: string;
74
89
  if (typeof result === "string") {
75
- return result;
90
+ rendered = result;
76
91
  } else if (typeof result === "number" || typeof result === "boolean") {
77
- return String(result);
92
+ rendered = String(result);
78
93
  } else if (result === null) {
79
- return "null";
94
+ rendered = "null";
80
95
  } else {
81
- return JSON.stringify(result);
96
+ rendered = JSON.stringify(result);
82
97
  }
98
+ return paginateToolResponse(rendered, options, "jq result");
83
99
  } catch (error: any) {
84
100
  // If JQ fails, try to provide helpful error message
85
101
  let errorMessage = `JQ Query Error: ${error.message}`;
@@ -99,7 +115,7 @@ export async function executeJqQuery(
99
115
  ).substring(0, 500)}...`;
100
116
  }
101
117
 
102
- return errorMessage;
118
+ return paginateToolResponse(errorMessage, options, "jq error");
103
119
  }
104
120
  }
105
121
 
@@ -108,7 +124,7 @@ export const jqToolResponseDefinition: Tool = {
108
124
  function: {
109
125
  name: "jqToolResponse",
110
126
  description:
111
- "Execute a JQ query on a stored tool response to extract specific data. Use this when you need to extract specific information from any tool response that has been stored. This is the preferred way to search or filter compressed JSON tool responses — it parses the data automatically without requiring repeated expandTokens calls. IMPORTANT: You do NOT know the toolCallId at the time you call a tool — you must call listStoredToolResponses first to discover the correct toolCallId. The listStoredToolResponses output shows each response's tool name so you can identify which response belongs to which call. How to determine the correct JQ query: (1) For mcp_* tool responses (external MCP tools like mcp_1_*): the response is stored as a raw JSON object — use '.' to access the root directly, e.g. '.children | map(.name)' or '.state'. Do NOT use .content[0].text | fromjson for these. (2) For compressed MCP tool responses (._mcp_format === true): use '._data' e.g. '._data.children | map(.name)'. (3) For standard built-in tool responses: data may be nested under '.content[0].text | fromjson'. Use jqToolResponse instead of expandTokens whenever the stored data is JSON.",
127
+ "Execute a JQ query on a stored tool response. Output is bounded to 20,000 characters by default; use options.characterOffset to page through a large result. Prefer a selective JQ query over paging a broad query. Call listStoredToolResponses first to discover the toolCallId. MCP responses are generally rooted at '.' (or '._data' when _mcp_format is true); standard built-in responses may be nested under '.content[0].text | fromjson'.",
112
128
  parameters: {
113
129
  type: "object",
114
130
  positional: true,
@@ -122,6 +138,20 @@ export const jqToolResponseDefinition: Tool = {
122
138
  description:
123
139
  "The JQ query to execute on the tool response data. For mcp_* tool responses (raw JSON object): '.children | map({id: .id, name: .name})' (extract fields from children array), '.children | map(select(.state == \"PENDING\")) | length' (count pending children), '.name' (get a top-level field). For compressed responses (._mcp_format true): '._data.children | map(.name)' or '._data | map(select(.state == \"PENDING\")) | length'. For standard built-in tool responses: '.content[0].text | fromjson | map(.title)' (extract titles from standard MCP array), '.content[0].text | fromjson | map(select(.createdAt > \"2025-01-01\"))' (filter by date).",
124
140
  },
141
+ options: {
142
+ type: "object",
143
+ description: "Optional output pagination settings.",
144
+ properties: {
145
+ characterOffset: {
146
+ type: "number",
147
+ description: "Zero-based character offset into the rendered JQ result.",
148
+ },
149
+ maxCharacters: {
150
+ type: "number",
151
+ description: "Response size (default 20000, hard maximum 50000).",
152
+ },
153
+ },
154
+ },
125
155
  },
126
156
  required: ["toolCallId", "jqQuery"],
127
157
  },
@@ -1,4 +1,12 @@
1
1
  import { Tool } from "../../clients";
2
+ import { boundedInteger, paginateToolResponse } from "./boundedToolResponse";
3
+
4
+ export interface ListStoredToolResponsesOptions {
5
+ resultOffset?: number;
6
+ maxResults?: number;
7
+ characterOffset?: number;
8
+ maxCharacters?: number;
9
+ }
2
10
 
3
11
  export interface ToolResponseInfo {
4
12
  toolCallId: string;
@@ -21,7 +29,8 @@ export async function executeListStoredToolResponses(
21
29
  toolName?: string;
22
30
  };
23
31
  },
24
- toolNameMap: { [toolCallId: string]: string }
32
+ toolNameMap: { [toolCallId: string]: string },
33
+ options?: ListStoredToolResponsesOptions
25
34
  ): Promise<string> {
26
35
  const toolCallIds = Object.keys(storage);
27
36
 
@@ -50,13 +59,15 @@ export async function executeListStoredToolResponses(
50
59
  // Sort by most recent first
51
60
  responses.sort((a, b) => b.storedAt - a.storedAt);
52
61
 
53
- // Format the output in a readable way
54
- const output = responses
62
+ const resultOffset = boundedInteger(options?.resultOffset, 0, 0, responses.length);
63
+ const maxResults = boundedInteger(options?.maxResults, 100, 1, 1000);
64
+ const page = responses.slice(resultOffset, resultOffset + maxResults);
65
+ const output = page
55
66
  .map((resp) => {
56
67
  const date = new Date(resp.storedAt).toISOString();
57
68
  return `
58
- Tool Call ID: ${resp.toolCallId}
59
- Tool Name: ${resp.toolName}
69
+ Tool Call ID: ${resp.toolCallId.slice(0, 500)}
70
+ Tool Name: ${resp.toolName.slice(0, 500)}
60
71
  Size: ${resp.size} characters
61
72
  Stored At: ${date}
62
73
  Preview: ${resp.preview}
@@ -64,7 +75,15 @@ Preview: ${resp.preview}
64
75
  })
65
76
  .join("\n");
66
77
 
67
- return `Found ${responses.length} stored tool response(s):\n${output}`;
78
+ const nextOffset = resultOffset + page.length;
79
+ const footer = nextOffset < responses.length
80
+ ? `\n[More stored responses available. Repeat with options.resultOffset=${nextOffset}.]`
81
+ : "\n[End of stored responses.]";
82
+ return paginateToolResponse(
83
+ `Found ${responses.length} stored tool response(s); showing ${resultOffset + 1}-${nextOffset}:\n${output}${footer}`,
84
+ options,
85
+ "stored-response list"
86
+ );
68
87
  }
69
88
 
70
89
  export const listStoredToolResponsesDefinition: Tool = {
@@ -72,12 +91,34 @@ export const listStoredToolResponsesDefinition: Tool = {
72
91
  function: {
73
92
  name: "listStoredToolResponses",
74
93
  description:
75
- "List all stored tool responses with metadata including tool call ID, tool name, size, timestamp, and a preview of the content. ALWAYS call this before using jqToolResponse, grepToolResponse, or tailToolResponse — you need the toolCallId from this list to query a specific response. The tool name shown here corresponds to the tool that produced the response (e.g. 'mcp_1_knowhow-web_GetOrgUserTask'), making it easy to identify which response you want.",
94
+ "List a bounded page of stored tool responses with IDs, tool names, sizes, timestamps, and previews. Call this before jqToolResponse, grepToolResponse, or tailToolResponse to discover a toolCallId.",
76
95
  parameters: {
77
96
  type: "object",
78
- positional: false,
79
- properties: {},
80
- required: [],
97
+ positional: true,
98
+ properties: {
99
+ options: {
100
+ type: "object",
101
+ description: "Optional result and output pagination settings.",
102
+ properties: {
103
+ resultOffset: {
104
+ type: "number",
105
+ description: "Zero-based response offset (default 0).",
106
+ },
107
+ maxResults: {
108
+ type: "number",
109
+ description: "Responses per page (default 100, maximum 1000).",
110
+ },
111
+ characterOffset: {
112
+ type: "number",
113
+ description: "Character offset within the rendered page.",
114
+ },
115
+ maxCharacters: {
116
+ type: "number",
117
+ description: "Response size (default 20000, hard maximum 50000).",
118
+ },
119
+ },
120
+ },
121
+ },
81
122
  },
82
123
  },
83
124
  };
@@ -1,7 +1,13 @@
1
1
  import { Tool } from "../../clients";
2
+ import { boundedInteger, paginateToolResponse } from "./boundedToolResponse";
2
3
 
3
4
  export interface TailOptions {
4
5
  lines?: number;
6
+ endLine?: number;
7
+ lineCharacterOffset?: number;
8
+ maxLineCharacters?: number;
9
+ characterOffset?: number;
10
+ maxCharacters?: number;
5
11
  }
6
12
 
7
13
  /**
@@ -21,31 +27,71 @@ export async function executeTail(
21
27
  return name ? `${id} (${name})` : id;
22
28
  })
23
29
  .join("\n - ");
24
- return `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`;
30
+ return paginateToolResponse(
31
+ `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`,
32
+ {
33
+ characterOffset: options?.characterOffset,
34
+ maxCharacters: options?.maxCharacters,
35
+ },
36
+ "available toolCallId list"
37
+ );
25
38
  }
26
39
 
27
40
  try {
28
41
  const lines = data.split("\n");
29
- const numLines = options?.lines ?? 10;
42
+ const numLines = boundedInteger(options?.lines, 10, 0, 1000);
30
43
 
31
- // Handle edge case: 0 lines requested
32
44
  if (numLines <= 0) {
33
45
  return "";
34
46
  }
35
47
 
36
- // Get the last n lines
37
- const startIdx = Math.max(0, lines.length - numLines);
38
- const tailLines = lines.slice(startIdx);
48
+ const endLine = boundedInteger(options?.endLine, lines.length, 1, lines.length);
49
+ const startIdx = Math.max(0, endLine - numLines);
50
+ const lineCharacterOffset = boundedInteger(
51
+ options?.lineCharacterOffset,
52
+ 0,
53
+ 0,
54
+ Number.MAX_SAFE_INTEGER
55
+ );
56
+ const maxLineCharacters = boundedInteger(
57
+ options?.maxLineCharacters,
58
+ 4000,
59
+ 100,
60
+ 10_000
61
+ );
62
+ const longLines: number[] = [];
39
63
 
40
- // Format with line numbers
41
- const formatted = tailLines.map((line, idx) => {
42
- const lineNum = startIdx + idx + 1;
43
- return `${lineNum}: ${line}`;
64
+ const formatted = lines.slice(startIdx, endLine).map((line, idx) => {
65
+ const lineNumber = startIdx + idx + 1;
66
+ const visible = line.slice(
67
+ lineCharacterOffset,
68
+ lineCharacterOffset + maxLineCharacters
69
+ );
70
+ if (lineCharacterOffset + maxLineCharacters < line.length) {
71
+ longLines.push(lineNumber);
72
+ }
73
+ return `${lineNumber}: ${visible}`;
44
74
  });
45
75
 
46
- return formatted.join("\n");
47
- } catch (error: any) {
48
- return `Tail Error: ${error.message}`;
76
+ const notes: string[] = [];
77
+ if (typeof options?.endLine === "number" && startIdx > 0) {
78
+ notes.push(`[Earlier lines available: repeat with options.endLine=${startIdx}.]`);
79
+ }
80
+ if (longLines.length > 0) notes.push(`[Long lines ${longLines.join(", ")} were sliced; repeat with options.lineCharacterOffset=${lineCharacterOffset + maxLineCharacters}.]`);
81
+ const rendered =
82
+ formatted.join("\n") +
83
+ (notes.length > 0 ? `\n\n${notes.join("\n")}` : "");
84
+ return paginateToolResponse(
85
+ rendered,
86
+ {
87
+ characterOffset: options?.characterOffset,
88
+ maxCharacters: options?.maxCharacters,
89
+ },
90
+ "tail result"
91
+ );
92
+ } catch (error: unknown) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ return `Tail Error: ${message.slice(0, 500)}`;
49
95
  }
50
96
  }
51
97
 
@@ -54,22 +100,44 @@ export const tailToolResponseDefinition: Tool = {
54
100
  function: {
55
101
  name: "tailToolResponse",
56
102
  description:
57
- "Get the last n lines from a stored tool response. Similar to the Unix 'tail' command, useful when a tool response is too large and you only need to see the end. Returns the last n lines with line numbers.",
103
+ "Get a bounded page of lines from the end of a stored tool response. Use endLine to page backward and lineCharacterOffset to page through unusually long lines.",
58
104
  parameters: {
59
105
  type: "object",
60
106
  positional: true,
61
107
  properties: {
62
108
  toolCallId: {
63
109
  type: "string",
64
- description: "The toolCallId of the stored tool response",
110
+ description: "The toolCallId of the stored tool response.",
65
111
  },
66
112
  options: {
67
113
  type: "object",
68
- description: "Optional tail settings: lines (number, default: 10) - number of lines to return from the end",
114
+ description: "Optional tail and pagination settings.",
69
115
  properties: {
70
116
  lines: {
71
117
  type: "number",
72
- description: "Number of lines to return from the end (default: 10)",
118
+ description: "Lines per page (default 10, maximum 1000).",
119
+ },
120
+ endLine: {
121
+ type: "number",
122
+ description:
123
+ "1-based inclusive line at which the page ends; use the returned value to page backward.",
124
+ },
125
+ lineCharacterOffset: {
126
+ type: "number",
127
+ description: "Character offset within each source line.",
128
+ },
129
+ maxLineCharacters: {
130
+ type: "number",
131
+ description:
132
+ "Characters shown per line (default 4000, maximum 10000).",
133
+ },
134
+ characterOffset: {
135
+ type: "number",
136
+ description: "Character offset within the rendered tail page.",
137
+ },
138
+ maxCharacters: {
139
+ type: "number",
140
+ description: "Response size (default 20000, hard maximum 50000).",
73
141
  },
74
142
  },
75
143
  },
@@ -133,6 +133,11 @@ export class AgentSyncFs {
133
133
  * that call) to usage.json. This is intended for debugging cache-hit
134
134
  * issues, where comparing the message chains and usage values between
135
135
  * consecutive calls can reveal when a cache prefix was invalidated.
136
+ *
137
+ * To prevent exponential file growth, if the new entry's message thread
138
+ * fully contains the previous entry's thread as a prefix, the previous
139
+ * thread is replaced with { PREV_CACHE_HIT: true } so only the new
140
+ * messages (the delta) are stored alongside the new usage data.
136
141
  */
137
142
  private async appendUsageEntry(entry: any): Promise<void> {
138
143
  if (!this.taskPath) return;
@@ -148,6 +153,27 @@ export class AgentSyncFs {
148
153
  // File doesn't exist or is invalid, start fresh
149
154
  }
150
155
 
156
+ // If the new entry's messages are a superset of the last entry's
157
+ // messages (i.e. the last thread is a prefix of the new thread),
158
+ // collapse the last entry's messages to { PREV_CACHE_HIT: true }
159
+ // and store only the new delta — preventing exponential growth.
160
+ if (entries.length > 0) {
161
+ const lastEntry = entries[entries.length - 1];
162
+ const prevMessages: any[] = lastEntry?.messages;
163
+ const newMessages: any[] = entry?.messages;
164
+ if (
165
+ Array.isArray(prevMessages) &&
166
+ Array.isArray(newMessages) &&
167
+ prevMessages.length > 0 &&
168
+ newMessages.length >= prevMessages.length &&
169
+ JSON.stringify(newMessages.slice(0, prevMessages.length)) ===
170
+ JSON.stringify(prevMessages)
171
+ ) {
172
+ // Replace the previous full thread with a compact sentinel
173
+ lastEntry.messages = [{ PREV_CACHE_HIT: true }];
174
+ }
175
+ }
176
+
151
177
  entries.push(entry);
152
178
  await fs.writeFile(usagePath, JSON.stringify(entries, null, 2), "utf8");
153
179
  } catch (error) {
@@ -180,6 +206,17 @@ export class AgentSyncFs {
180
206
  metadata.totalCostUsd = agent.getTotalCostUsd();
181
207
  metadata.tokenUsage = agent.getTokenUsage();
182
208
  metadata.agentName = agent.name;
209
+ // Record the model/provider actually in use at this sync so task
210
+ // history reflects the real model (including mid-run fallbacks), not
211
+ // just the named agent.
212
+ metadata.model = agent.getModel();
213
+ metadata.provider = agent.getProvider();
214
+ // Persist reasoning settings so a resume can restore the exact
215
+ // reasoning effort / summarize behavior the run was using. (The
216
+ // encrypted reasoning items + summaries themselves ride along on the
217
+ // thread messages via `_reasoning_details` / `reasoning_summary`.)
218
+ metadata.reasoningEffort = agent.getReasoningEffort?.();
219
+ metadata.summarizeReasoning = agent.getSummarizeReasoning?.();
183
220
  metadata.inProgress = inProgress;
184
221
  metadata.lastUpdate = new Date().toISOString();
185
222