@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
@@ -1,6 +1,6 @@
1
1
  import { includedTools } from "../agents/tools/list";
2
2
  import * as allTools from "../agents/tools";
3
- import { LazyToolsService, services, MinimalToolsService } from "../services";
3
+ import { LazyToolsService, services, MinimalToolsService, TracingService } from "../services";
4
4
  import { agents } from "../agents";
5
5
  import { ModulesService } from "../services/modules";
6
6
  import { getConfig } from "../config";
@@ -19,8 +19,10 @@ export async function setupServices() {
19
19
  Events,
20
20
  MediaProcessor,
21
21
  Behaviors,
22
+ RuntimeReload,
22
23
  } = services();
23
24
 
25
+ await Plugins.refreshConfiguredState();
24
26
  // cli uses LazyTools to keep context slim
25
27
  const Tools = new LazyToolsService();
26
28
  await Behaviors.initFromConfig();
@@ -80,7 +82,7 @@ export async function setupServices() {
80
82
 
81
83
  console.log("📦 Loading modules from config...");
82
84
  const modulesService = new ModulesService();
83
- await modulesService.loadModulesFromConfig({
85
+ const moduleContext = {
84
86
  Agents,
85
87
  Embeddings,
86
88
  Plugins,
@@ -89,7 +91,51 @@ export async function setupServices() {
89
91
  MediaProcessor,
90
92
  Behaviors,
91
93
  Events,
94
+ Tracing: TracingService,
95
+ };
96
+
97
+ // Call destroy() on all modules when the process is shutting down so they
98
+ // can flush buffers (e.g. OTEL spans), close connections, etc.
99
+ const destroyAll = async () => {
100
+ await modulesService.destroyModules();
101
+ };
102
+ process.on("beforeExit", destroyAll);
103
+ process.on("SIGINT", async () => {
104
+ await destroyAll();
105
+ process.exit(0);
106
+ });
107
+ await modulesService.loadModulesFromConfig(moduleContext);
108
+
109
+ RuntimeReload.configure(async () => {
110
+ await modulesService.destroyModules();
111
+ await Plugins.refreshConfiguredState();
112
+ await Mcp.closeAll();
113
+
114
+ Tools.resetTools();
115
+ Tools.defineTools(includedTools, allTools);
116
+ try {
117
+ const agentCallDef = AllTools.getTool?.("agentCall");
118
+ const agentCallFn = AllTools.getFunction?.("agentCall");
119
+ if (agentCallDef) Tools.addTool(agentCallDef);
120
+ if (agentCallFn) Tools.setFunction("agentCall", agentCallFn);
121
+ } catch (_) {}
122
+
123
+ await Behaviors.initFromConfig();
124
+ await Mcp.connectToConfigured(Tools);
125
+ await Clients.registerConfiguredModels();
126
+ await modulesService.loadModulesFromConfig(moduleContext);
127
+
128
+ const config = await getConfig();
129
+ return {
130
+ tools: Tools.getTools().length,
131
+ mcps: (config.mcps || []).length,
132
+ modules: (config.modules || []).length,
133
+ };
134
+ });
135
+ process.on("SIGTERM", async () => {
136
+ await destroyAll();
137
+ process.exit(0);
92
138
  });
93
139
 
94
- return { Tools, Clients };
140
+ return { Tools, Clients, modulesService };
95
141
  }
package/src/index.ts CHANGED
@@ -51,6 +51,14 @@ export {
51
51
  } from "./generate";
52
52
 
53
53
  export * as clients from "./clients";
54
+ export type {
55
+ Tool,
56
+ ToolProp,
57
+ ToolCall,
58
+ Message,
59
+ CompletionOptions,
60
+ CompletionResponse,
61
+ } from "./clients/types";
54
62
  export * as agents from "./agents";
55
63
  export * as services from "./services";
56
64
  export * as embeddings from "./embeddings";
@@ -61,6 +69,7 @@ export * as ai from "./ai";
61
69
  // Export module system types for external modules
62
70
  export * from "./services/modules/types";
63
71
  export { ModulesService } from "./services/modules";
72
+ export * from "./services/modules/computerUse";
64
73
  // Export conversion types for external modules (e.g. knowhow-module-pdf)
65
74
  export * from "./services/conversion/types";
66
75
  export { ConversionService } from "./services/conversion/ConversionService";
@@ -52,6 +52,22 @@ export class PluginService {
52
52
  return true;
53
53
  }
54
54
 
55
+ /** Apply the enabled/disabled state from the current Knowhow config. */
56
+ async refreshConfiguredState(): Promise<void> {
57
+ const config = await getConfig();
58
+ const disabled = new Set(getDisabledPlugins(config.plugins));
59
+
60
+ for (const [key, plugin] of this.pluginMap) {
61
+ if (disabled.has(key)) {
62
+ plugin.disable();
63
+ } else {
64
+ // Configuration is authoritative when services start or reload. This
65
+ // also re-enables a plugin after it is removed from `disabled`.
66
+ plugin.enable();
67
+ }
68
+ }
69
+ }
70
+
55
71
  /* -------- existing public API (updated for compatibility) ---------------------- */
56
72
 
57
73
  listPlugins() {
@@ -257,11 +257,26 @@ export class Base64ImageProcessor {
257
257
  try {
258
258
  // Try to parse as JSON
259
259
  const parsed = JSON.parse(message.content);
260
-
261
- // Check if it's an image_url object
262
- if (parsed.type === "image_url" && parsed.image_url?.url) {
263
- // Convert the tool message content from JSON string to an array with the image
264
- message.content = [parsed];
260
+
261
+ // Normalize the parsed value into an array of candidate parts so we can
262
+ // handle BOTH shapes a tool might return:
263
+ // 1. a single image_url object: {type:"image_url", image_url:{url}}
264
+ // 2. an array of parts: [{type:"image_url", image_url:{url}}, {type:"text", ...}]
265
+ // (Computer-use screenshot tools and loadWebpage return the array form,
266
+ // which the single-object check above previously missed — leaving the
267
+ // model to see a raw base64 JSON string instead of an actual image.)
268
+ const candidates = Array.isArray(parsed) ? parsed : [parsed];
269
+ const isImagePart = (p: any) =>
270
+ p && p.type === "image_url" && p.image_url?.url;
271
+ const isTextPart = (p: any) =>
272
+ p && p.type === "text" && typeof p.text === "string";
273
+
274
+ // Only rewrite the content if it actually contains at least one image
275
+ // part; otherwise leave the original tool text untouched.
276
+ if (candidates.some(isImagePart)) {
277
+ message.content = candidates.filter(
278
+ (p) => isImagePart(p) || isTextPart(p)
279
+ );
265
280
  }
266
281
  } catch (e) {
267
282
  // Not JSON, check if it's a plain base64 string (only if still a string)
@@ -414,10 +429,34 @@ export class Base64ImageProcessor {
414
429
 
415
430
  // Read the file as base64
416
431
  const imageBuffer = fs.readFileSync(filePath);
432
+
433
+ // Detect actual MIME type from magic bytes (overrides extension-based detection)
434
+ // This handles cases where a file is saved with the wrong extension (e.g. JPEG saved as .png)
435
+ let detectedMimeType = mimeType;
436
+ if (imageBuffer.length >= 4) {
437
+ // JPEG: starts with FF D8 FF
438
+ if (imageBuffer[0] === 0xff && imageBuffer[1] === 0xd8 && imageBuffer[2] === 0xff) {
439
+ detectedMimeType = "image/jpeg";
440
+ }
441
+ // PNG: starts with 89 50 4E 47 0D 0A 1A 0A
442
+ else if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50 && imageBuffer[2] === 0x4e && imageBuffer[3] === 0x47) {
443
+ detectedMimeType = "image/png";
444
+ }
445
+ // GIF: starts with 47 49 46 38 (GIF8)
446
+ else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49 && imageBuffer[2] === 0x46 && imageBuffer[3] === 0x38) {
447
+ detectedMimeType = "image/gif";
448
+ }
449
+ // WebP: starts with 52 49 46 46 (RIFF) ... 57 45 42 50 (WEBP) at offset 8
450
+ else if (imageBuffer.length >= 12 && imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49 && imageBuffer[2] === 0x46 && imageBuffer[3] === 0x46 &&
451
+ imageBuffer[8] === 0x57 && imageBuffer[9] === 0x45 && imageBuffer[10] === 0x42 && imageBuffer[11] === 0x50) {
452
+ detectedMimeType = "image/webp";
453
+ }
454
+ }
455
+
417
456
  const base64Data = imageBuffer.toString("base64");
418
457
 
419
- // Create data URL
420
- const dataUrl = `data:${mimeType};base64,${base64Data}`;
458
+ // Create data URL using the actual detected MIME type
459
+ const dataUrl = `data:${detectedMimeType};base64,${base64Data}`;
421
460
 
422
461
  // Return in a format that indicates this is an image
423
462
  // The Base64ImageDetector will convert this to proper image content
@@ -0,0 +1,163 @@
1
+ import { Message, ToolCall } from "../clients/types";
2
+ import { MessageProcessorFunction } from "../services/MessageProcessor";
3
+
4
+ /**
5
+ * Gemma 4 Tool Call Processor
6
+ *
7
+ * Gemma 4 uses a non-standard tool-call format with its own string-quoting token:
8
+ *
9
+ * <|tool_call>call:toolName{key:<|"|>string value<|"|>,numKey:42,boolKey:true}<tool_call|>
10
+ *
11
+ * Strings are delimited by <|"|> instead of standard JSON quotes.
12
+ * Numbers and booleans are written bare (no quotes).
13
+ * Multiple tool calls may appear in a single assistant message.
14
+ *
15
+ * Reference: https://ai.google.dev/gemma/docs/capabilities/text/function-calling-gemma4
16
+ */
17
+ export class GemmaToolProcessor {
18
+ private static instance: GemmaToolProcessor;
19
+
20
+ static getInstance(): GemmaToolProcessor {
21
+ if (!GemmaToolProcessor.instance) {
22
+ GemmaToolProcessor.instance = new GemmaToolProcessor();
23
+ }
24
+ return GemmaToolProcessor.instance;
25
+ }
26
+
27
+ // ── Detection ─────────────────────────────────────────────────────────────
28
+
29
+ /** Returns true when the content looks like it contains Gemma tool-call tokens. */
30
+ private isGemmaFormat(content: string): boolean {
31
+ return content.includes("<|tool_call>") && content.includes("<tool_call|>");
32
+ }
33
+
34
+ // ── Parsing ───────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Cast a raw argument value string to a JS primitive.
38
+ * Mirrors the Python `cast()` helper from the official docs.
39
+ */
40
+ private castValue(raw: string): string | number | boolean {
41
+ const trimmed = raw.trim();
42
+
43
+ // Boolean
44
+ const lower = trimmed.toLowerCase();
45
+ if (lower === "true") return true;
46
+ if (lower === "false") return false;
47
+
48
+ // Integer
49
+ if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
50
+
51
+ // Float
52
+ if (/^-?\d+\.\d*$/.test(trimmed)) return parseFloat(trimmed);
53
+
54
+ // Strip surrounding single/double quotes that the model might emit
55
+ return trimmed.replace(/^['"]|['"]$/g, "");
56
+ }
57
+
58
+ /**
59
+ * Parse the argument body of a single Gemma tool call.
60
+ *
61
+ * The body looks like:
62
+ * key1:<|"|>string value<|"|>,key2:42,key3:true
63
+ *
64
+ * The regex (from the official docs) handles both quoted strings and bare values:
65
+ * (\w+):(?:<|"|>(.*?)<|"|>|([^,}]*))
66
+ */
67
+ private parseArgs(argsBody: string): Record<string, string | number | boolean> {
68
+ const result: Record<string, string | number | boolean> = {};
69
+
70
+ // Regex from the official Google Gemma 4 function-calling docs
71
+ const argPattern = /(\w+):(?:<\|"\|>(.*?)<\|"\|>|([^,}]*))/gs;
72
+ let m: RegExpExecArray | null;
73
+
74
+ while ((m = argPattern.exec(argsBody)) !== null) {
75
+ const key = m[1];
76
+ // v1: string inside <|"|>…<|"|> v2: bare value
77
+ const rawValue = m[2] !== undefined ? m[2] : m[3] ?? "";
78
+ result[key] = m[2] !== undefined ? rawValue : this.castValue(rawValue);
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * Extract all Gemma tool calls from an assistant message string.
86
+ * Returns an array of { toolName, arguments } objects.
87
+ */
88
+ private extractGemmaToolCalls(
89
+ content: string
90
+ ): { toolName: string; arguments: string }[] {
91
+ const results: { toolName: string; arguments: string }[] = [];
92
+
93
+ // Outer pattern: <|tool_call>call:name{args}<tool_call|>
94
+ // Using DOTALL-equivalent workaround: [\s\S] for multi-line args
95
+ const outerPattern = /<\|tool_call>call:(\w+)\{([\s\S]*?)\}<tool_call\|>/g;
96
+ let match: RegExpExecArray | null;
97
+
98
+ while ((match = outerPattern.exec(content)) !== null) {
99
+ const toolName = match[1];
100
+ const argsBody = match[2];
101
+
102
+ const parsed = this.parseArgs(argsBody);
103
+ results.push({
104
+ toolName,
105
+ arguments: JSON.stringify(parsed),
106
+ });
107
+ }
108
+
109
+ return results;
110
+ }
111
+
112
+ // ── Conversion ────────────────────────────────────────────────────────────
113
+
114
+ private generateToolCallId(): string {
115
+ return `gemma_call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
116
+ }
117
+
118
+ private toToolCalls(
119
+ extracted: { toolName: string; arguments: string }[]
120
+ ): ToolCall[] {
121
+ return extracted.map(({ toolName, arguments: args }) => ({
122
+ id: this.generateToolCallId(),
123
+ type: "function" as const,
124
+ function: {
125
+ name: toolName,
126
+ arguments: args,
127
+ },
128
+ }));
129
+ }
130
+
131
+ // ── Message processing ────────────────────────────────────────────────────
132
+
133
+ private processMessage(message: Message): void {
134
+ if (message.role !== "assistant") return;
135
+ if (typeof message.content !== "string") return;
136
+ if (message?.tool_calls?.length) return;
137
+
138
+ if (!this.isGemmaFormat(message.content)) return;
139
+
140
+ const extracted = this.extractGemmaToolCalls(message.content);
141
+ if (extracted.length === 0) return;
142
+
143
+ const toolCalls = this.toToolCalls(extracted);
144
+
145
+ if (message.tool_calls) {
146
+ message.tool_calls.push(...toolCalls);
147
+ } else {
148
+ message.tool_calls = toolCalls;
149
+ }
150
+ }
151
+
152
+ // ── Public API ────────────────────────────────────────────────────────────
153
+
154
+ createProcessor(): MessageProcessorFunction {
155
+ return (_originalMessages: Message[], modifiedMessages: Message[]): void => {
156
+ for (const message of modifiedMessages) {
157
+ this.processMessage(message);
158
+ }
159
+ };
160
+ }
161
+ }
162
+
163
+ export const globalGemmaToolProcessor = new GemmaToolProcessor();
@@ -7,6 +7,7 @@ import {
7
7
  CompressionMetadata,
8
8
  JsonCompressorStorage,
9
9
  } from "./JsonCompressor";
10
+ import { paginateToolResponse } from "./tools/boundedToolResponse";
10
11
 
11
12
  export interface KeyInfo {
12
13
  key: string;
@@ -419,15 +420,25 @@ export class TokenCompressor implements JsonCompressorStorage {
419
420
  if (toolsService) {
420
421
  toolsService.addTools([expandTokensDefinition]);
421
422
  toolsService.addFunctions({
422
- [this.toolName]: (key: string, fromLine?: number, toLine?: number) => {
423
+ [this.toolName]: (
424
+ key: string,
425
+ fromLine?: number,
426
+ toLine?: number,
427
+ characterOffset?: number,
428
+ maxCharacters?: number
429
+ ) => {
423
430
  // Auto-stitch: follow any NEXT_CHUNK_KEY chain and return the full,
424
431
  // reassembled content so the agent never has to chase nested keys.
425
432
  const data = this.retrieveFullString(key);
426
433
 
427
434
  if (data === null) {
428
- return `Error: No data found for key "${key}". Available keys: ${this.getStorageKeys().join(
429
- ", "
430
- )}`;
435
+ return paginateToolResponse(
436
+ `Error: No data found for key "${key}". Available keys: ${this.getStorageKeys().join(
437
+ ", "
438
+ )}`,
439
+ { characterOffset, maxCharacters },
440
+ "available-key list"
441
+ );
431
442
  }
432
443
 
433
444
  // Optional ranged read: return only the requested 1-based, inclusive
@@ -435,7 +446,11 @@ export class TokenCompressor implements JsonCompressorStorage {
435
446
  const hasRange =
436
447
  typeof fromLine === "number" || typeof toLine === "number";
437
448
  if (!hasRange) {
438
- return data;
449
+ return paginateToolResponse(
450
+ data,
451
+ { characterOffset, maxCharacters },
452
+ `expanded value for ${key}`
453
+ );
439
454
  }
440
455
 
441
456
  const lines = data.split("\n");
@@ -447,14 +462,22 @@ export class TokenCompressor implements JsonCompressorStorage {
447
462
  );
448
463
 
449
464
  if (start > end) {
450
- return `Error: Invalid line range for key "${key}": fromLine (${start}) is greater than toLine (${end}). Content has ${totalLines} lines.`;
465
+ return paginateToolResponse(
466
+ `Error: Invalid line range for key "${key}": fromLine (${start}) is greater than toLine (${end}). Content has ${totalLines} lines.`,
467
+ { characterOffset, maxCharacters },
468
+ "line-range error"
469
+ );
451
470
  }
452
471
 
453
472
  const numbered: string[] = [];
454
473
  for (let i = start; i <= end; i++) {
455
474
  numbered.push(`${i}: ${lines[i - 1]}`);
456
475
  }
457
- return numbered.join("\n");
476
+ return paginateToolResponse(
477
+ numbered.join("\n"),
478
+ { characterOffset, maxCharacters },
479
+ `expanded lines ${start}-${end} for ${key}`
480
+ );
458
481
  },
459
482
  });
460
483
  }
@@ -559,7 +582,7 @@ export const expandTokensDefinition: Tool = {
559
582
  function: {
560
583
  name: "expandTokens",
561
584
  description:
562
- "Retrieve compressed data that was stored during message processing. The full content is automatically reassembled (any chunk chain is followed for you, so you never need to chase NEXT_CHUNK_KEY references). Optionally pass fromLine/toLine (1-based, inclusive) to return just a range of lines, prefixed with real line numbers. NOTE: Can also use jqToolResponse, grepToolResponse, tailToolResponse to access/filter/search compressed content without needing to expand all tokens.",
585
+ "Retrieve a bounded page of compressed data. Chunk chains are automatically reassembled, but output is limited to 20,000 characters by default. Use characterOffset to continue, or fromLine/toLine for line-based selection. Prefer jqToolResponse, grepToolResponse, or tailToolResponse when possible.",
563
586
  parameters: {
564
587
  type: "object",
565
588
  positional: true,
@@ -578,6 +601,15 @@ export const expandTokensDefinition: Tool = {
578
601
  description:
579
602
  "Optional 1-based end line (inclusive). Defaults to the end of the content when omitted.",
580
603
  },
604
+ characterOffset: {
605
+ type: "number",
606
+ description:
607
+ "Optional zero-based character offset within the expanded value or selected line range.",
608
+ },
609
+ maxCharacters: {
610
+ type: "number",
611
+ description: "Response size (default 20000, hard maximum 50000).",
612
+ },
581
613
  },
582
614
  required: ["key"],
583
615
  },
@@ -5,6 +5,7 @@ import { JsonCompressor } from "./JsonCompressor";
5
5
  import {
6
6
  jqToolResponseDefinition,
7
7
  executeJqQuery,
8
+ JqOptions,
8
9
  grepToolResponseDefinition,
9
10
  executeGrep,
10
11
  GrepOptions,
@@ -13,6 +14,7 @@ import {
13
14
  TailOptions,
14
15
  listStoredToolResponsesDefinition,
15
16
  executeListStoredToolResponses,
17
+ ListStoredToolResponsesOptions,
16
18
  } from "./tools";
17
19
 
18
20
  interface ToolResponseStorage {
@@ -196,11 +198,12 @@ export class ToolResponseCache {
196
198
  */
197
199
  async queryToolResponse(
198
200
  toolCallId: string,
199
- jqQuery: string
201
+ jqQuery: string,
202
+ options?: JqOptions
200
203
  ): Promise<string> {
201
204
  const data = this.storage[toolCallId];
202
205
  const availableIds = Object.keys(this.storage);
203
- return executeJqQuery(data, toolCallId, jqQuery, availableIds, this.toolNameMap);
206
+ return executeJqQuery(data, toolCallId, jqQuery, availableIds, this.toolNameMap, options);
204
207
  }
205
208
 
206
209
  /**
@@ -231,11 +234,14 @@ export class ToolResponseCache {
231
234
  /**
232
235
  * List all stored tool responses with metadata
233
236
  */
234
- async listStoredToolResponses(): Promise<string> {
237
+ async listStoredToolResponses(
238
+ options?: ListStoredToolResponsesOptions
239
+ ): Promise<string> {
235
240
  return executeListStoredToolResponses(
236
241
  this.storage,
237
242
  this.metadataStorage,
238
- this.toolNameMap
243
+ this.toolNameMap,
244
+ options
239
245
  );
240
246
  }
241
247
 
@@ -281,9 +287,10 @@ export class ToolResponseCache {
281
287
  toolsService.addFunctions({
282
288
  [jqToolResponseDefinition.function.name]: async (
283
289
  toolCallId: string,
284
- jqQuery: string
290
+ jqQuery: string,
291
+ options?: JqOptions
285
292
  ) => {
286
- return await this.queryToolResponse(toolCallId, jqQuery);
293
+ return await this.queryToolResponse(toolCallId, jqQuery, options);
287
294
  },
288
295
  [grepToolResponseDefinition.function.name]: async (
289
296
  toolCallId: string,
@@ -298,8 +305,10 @@ export class ToolResponseCache {
298
305
  ) => {
299
306
  return await this.tailToolResponse(toolCallId, options);
300
307
  },
301
- [listStoredToolResponsesDefinition.function.name]: async () => {
302
- return await this.listStoredToolResponses();
308
+ [listStoredToolResponsesDefinition.function.name]: async (
309
+ options?: ListStoredToolResponsesOptions
310
+ ) => {
311
+ return await this.listStoredToolResponses(options);
303
312
  },
304
313
  });
305
314
  }
@@ -10,4 +10,5 @@ export {
10
10
  export { ToolResponseCache } from "./ToolResponseCache";
11
11
  export { XmlToolCallProcessor } from "./XmlToolCallProcessor";
12
12
  export { HarmonyToolProcessor } from "./HarmonyToolProcessor";
13
+ export { GemmaToolProcessor } from "./GemmaToolProcessor";
13
14
  export { MinimalToolsMessageProcessor } from "./MinimalToolsMessageProcessor";
@@ -0,0 +1,49 @@
1
+ export const DEFAULT_TOOL_RESPONSE_CHARACTERS = 20_000;
2
+ export const MAX_TOOL_RESPONSE_CHARACTERS = 50_000;
3
+
4
+ export function boundedInteger(
5
+ value: number | undefined,
6
+ fallback: number,
7
+ min: number,
8
+ max: number
9
+ ): number {
10
+ return Math.min(
11
+ max,
12
+ Math.max(min, Number.isFinite(value) ? Math.floor(value as number) : fallback)
13
+ );
14
+ }
15
+
16
+ /** Return a bounded character page with instructions for retrieving the next page. */
17
+ export function paginateToolResponse(
18
+ value: string,
19
+ options?: { characterOffset?: number; maxCharacters?: number },
20
+ label = "response"
21
+ ): string {
22
+ const characterOffset = boundedInteger(
23
+ options?.characterOffset,
24
+ 0,
25
+ 0,
26
+ Number.MAX_SAFE_INTEGER
27
+ );
28
+ const maxCharacters = boundedInteger(
29
+ options?.maxCharacters,
30
+ DEFAULT_TOOL_RESPONSE_CHARACTERS,
31
+ 1_000,
32
+ MAX_TOOL_RESPONSE_CHARACTERS
33
+ );
34
+ const footerReserve = 300;
35
+ const contentLimit = Math.max(1, maxCharacters - footerReserve);
36
+ const page = value.slice(characterOffset, characterOffset + contentLimit);
37
+ const end = characterOffset + page.length;
38
+ if (characterOffset === 0 && end >= value.length) {
39
+ return page;
40
+ }
41
+
42
+ const safeLabel = label.slice(0, 200);
43
+ const footer =
44
+ end < value.length
45
+ ? `\n\n[${safeLabel} characters ${characterOffset}-${Math.max(characterOffset, end - 1)} of ${value.length}. Repeat with characterOffset=${end} to continue; maxCharacters=${maxCharacters}.]`
46
+ : `\n\n[End of ${safeLabel}; characters ${characterOffset}-${Math.max(characterOffset, end - 1)} of ${value.length}.]`;
47
+
48
+ return (page + footer).slice(0, maxCharacters);
49
+ }