@tyvm/knowhow 0.0.127 → 0.0.129

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 (109) hide show
  1. package/package.json +2 -1
  2. package/scripts/cache-miss-debug.ts +365 -0
  3. package/src/agents/base/base.ts +119 -54
  4. package/src/agents/tools/execCommand.ts +38 -4
  5. package/src/agents/tools/minimal/callTool.ts +45 -0
  6. package/src/agents/tools/minimal/definitions.ts +50 -0
  7. package/src/agents/tools/minimal/index.ts +3 -0
  8. package/src/agents/tools/minimal/inspectTools.ts +27 -0
  9. package/src/chat/modules/AgentModule.ts +27 -8
  10. package/src/clients/contextLimits.ts +9 -0
  11. package/src/clients/index.ts +6 -0
  12. package/src/clients/meta.ts +18 -0
  13. package/src/clients/pricing/anthropic.ts +13 -0
  14. package/src/clients/pricing/fireworks.ts +18 -12
  15. package/src/clients/pricing/index.ts +1 -0
  16. package/src/clients/pricing/meta.ts +12 -0
  17. package/src/clients/pricing/openai.ts +13 -0
  18. package/src/clients/pricing/xai.ts +3 -0
  19. package/src/commands/services.ts +1 -1
  20. package/src/processors/MinimalToolsMessageProcessor.ts +113 -0
  21. package/src/processors/TokenCompressor.ts +21 -4
  22. package/src/processors/index.ts +7 -1
  23. package/src/services/AgentSyncFs.ts +49 -0
  24. package/src/services/AgentSyncKnowhowWeb.ts +1 -1
  25. package/src/services/KnowhowClient.ts +1 -1
  26. package/src/services/LazyToolsService.ts +12 -0
  27. package/src/services/MinimalToolsService.ts +167 -0
  28. package/src/services/Tools.ts +9 -0
  29. package/src/services/index.ts +1 -0
  30. package/src/worker.ts +7 -0
  31. package/tests/processors/TokenCompressor.test.ts +45 -0
  32. package/tests/unit/services/MinimalToolsService.test.ts +424 -0
  33. package/ts_build/package.json +2 -1
  34. package/ts_build/src/agents/base/base.d.ts +4 -1
  35. package/ts_build/src/agents/base/base.js +62 -31
  36. package/ts_build/src/agents/base/base.js.map +1 -1
  37. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  38. package/ts_build/src/agents/tools/execCommand.js +24 -3
  39. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  40. package/ts_build/src/agents/tools/minimal/callTool.d.ts +5 -0
  41. package/ts_build/src/agents/tools/minimal/callTool.js +28 -0
  42. package/ts_build/src/agents/tools/minimal/callTool.js.map +1 -0
  43. package/ts_build/src/agents/tools/minimal/definitions.d.ts +4 -0
  44. package/ts_build/src/agents/tools/minimal/definitions.js +46 -0
  45. package/ts_build/src/agents/tools/minimal/definitions.js.map +1 -0
  46. package/ts_build/src/agents/tools/minimal/index.d.ts +3 -0
  47. package/ts_build/src/agents/tools/minimal/index.js +20 -0
  48. package/ts_build/src/agents/tools/minimal/index.js.map +1 -0
  49. package/ts_build/src/agents/tools/minimal/inspectTools.d.ts +2 -0
  50. package/ts_build/src/agents/tools/minimal/inspectTools.js +15 -0
  51. package/ts_build/src/agents/tools/minimal/inspectTools.js.map +1 -0
  52. package/ts_build/src/chat/modules/AgentModule.js +8 -2
  53. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  54. package/ts_build/src/clients/contextLimits.js +8 -0
  55. package/ts_build/src/clients/contextLimits.js.map +1 -1
  56. package/ts_build/src/clients/index.d.ts +1 -0
  57. package/ts_build/src/clients/index.js +6 -0
  58. package/ts_build/src/clients/index.js.map +1 -1
  59. package/ts_build/src/clients/meta.d.ts +4 -0
  60. package/ts_build/src/clients/meta.js +15 -0
  61. package/ts_build/src/clients/meta.js.map +1 -0
  62. package/ts_build/src/clients/pricing/anthropic.d.ts +4 -0
  63. package/ts_build/src/clients/pricing/anthropic.js +12 -0
  64. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  65. package/ts_build/src/clients/pricing/fireworks.d.ts +2 -5
  66. package/ts_build/src/clients/pricing/fireworks.js +13 -11
  67. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  68. package/ts_build/src/clients/pricing/index.d.ts +1 -0
  69. package/ts_build/src/clients/pricing/index.js +5 -2
  70. package/ts_build/src/clients/pricing/index.js.map +1 -1
  71. package/ts_build/src/clients/pricing/meta.d.ts +6 -0
  72. package/ts_build/src/clients/pricing/meta.js +8 -0
  73. package/ts_build/src/clients/pricing/meta.js.map +1 -0
  74. package/ts_build/src/clients/pricing/openai.d.ts +3 -0
  75. package/ts_build/src/clients/pricing/openai.js +12 -0
  76. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  77. package/ts_build/src/clients/pricing/xai.d.ts +1 -0
  78. package/ts_build/src/clients/pricing/xai.js +3 -0
  79. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  80. package/ts_build/src/commands/services.js.map +1 -1
  81. package/ts_build/src/processors/MinimalToolsMessageProcessor.d.ts +7 -0
  82. package/ts_build/src/processors/MinimalToolsMessageProcessor.js +61 -0
  83. package/ts_build/src/processors/MinimalToolsMessageProcessor.js.map +1 -0
  84. package/ts_build/src/processors/TokenCompressor.js +5 -2
  85. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  86. package/ts_build/src/processors/index.d.ts +2 -1
  87. package/ts_build/src/processors/index.js +3 -1
  88. package/ts_build/src/processors/index.js.map +1 -1
  89. package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
  90. package/ts_build/src/services/AgentSyncFs.js +30 -0
  91. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  92. package/ts_build/src/services/AgentSyncKnowhowWeb.js +1 -1
  93. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  94. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  95. package/ts_build/src/services/MinimalToolsService.d.ts +14 -0
  96. package/ts_build/src/services/MinimalToolsService.js +80 -0
  97. package/ts_build/src/services/MinimalToolsService.js.map +1 -0
  98. package/ts_build/src/services/Tools.js.map +1 -1
  99. package/ts_build/src/services/index.d.ts +1 -0
  100. package/ts_build/src/services/index.js +1 -0
  101. package/ts_build/src/services/index.js.map +1 -1
  102. package/ts_build/src/types.d.ts +8 -0
  103. package/ts_build/src/worker.js +5 -0
  104. package/ts_build/src/worker.js.map +1 -1
  105. package/ts_build/tests/processors/TokenCompressor.test.js +27 -0
  106. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  107. package/ts_build/tests/unit/services/MinimalToolsService.test.d.ts +1 -0
  108. package/ts_build/tests/unit/services/MinimalToolsService.test.js +320 -0
  109. package/ts_build/tests/unit/services/MinimalToolsService.test.js.map +1 -0
@@ -0,0 +1,113 @@
1
+ import { Message, ToolCall } from "../clients/types";
2
+ import { MessageProcessorFunction } from "../services/MessageProcessor";
3
+
4
+ /**
5
+ * MinimalToolsMessageProcessor
6
+ *
7
+ * Works with MinimalToolsService to unwrap `callTool(name, args)` invocations
8
+ * in the message history, rewriting them as if the real tool had been called
9
+ * directly.
10
+ *
11
+ * This is registered on the `pre_tools` lifecycle so it runs right before tool
12
+ * calls are dispatched. It rewrites the last assistant message's tool_calls so
13
+ * that:
14
+ * callTool({ name: "readFile", args: { filePath: "..." } })
15
+ * becomes:
16
+ * readFile({ filePath: "..." })
17
+ *
18
+ * This means:
19
+ * 1. The `tools:` array sent to the AI never needs to change (cache stable).
20
+ * 2. The agent terminates correctly when callTool("finalAnswer", ...) is called,
21
+ * because after transformation the required tool name matches.
22
+ * 3. The conversation history looks clean — the model sees native tool calls in
23
+ * subsequent turns rather than nested callTool wrappers.
24
+ */
25
+ export class MinimalToolsMessageProcessor {
26
+ /**
27
+ * Unwraps a single callTool ToolCall into the real underlying ToolCall.
28
+ * Returns null if this is not a callTool invocation.
29
+ */
30
+ private unwrapCallTool(toolCall: ToolCall): ToolCall | null {
31
+ if (toolCall.function.name !== "callTool") {
32
+ return null;
33
+ }
34
+
35
+ let parsed: { name?: string; args?: Record<string, any> };
36
+ try {
37
+ parsed =
38
+ typeof toolCall.function.arguments === "string"
39
+ ? JSON.parse(toolCall.function.arguments)
40
+ : toolCall.function.arguments;
41
+ } catch {
42
+ return null;
43
+ }
44
+
45
+ const { name, args } = parsed;
46
+ if (!name || typeof name !== "string") {
47
+ return null;
48
+ }
49
+
50
+ return {
51
+ id: toolCall.id,
52
+ type: "function",
53
+ function: {
54
+ name,
55
+ arguments: JSON.stringify(args ?? {}),
56
+ },
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Processes the modified messages array in-place:
62
+ * - Finds assistant messages whose tool_calls contain callTool invocations.
63
+ * - Replaces those callTool entries with the real underlying tool calls.
64
+ *
65
+ * Only the most-recent assistant message (the one just added by the AI) is
66
+ * rewritten — older messages are left untouched so caching is not affected.
67
+ */
68
+ private processMessages(
69
+ _originalMessages: Message[],
70
+ modifiedMessages: Message[]
71
+ ): void {
72
+ // Walk backwards to find the last assistant message with tool_calls
73
+ for (let i = modifiedMessages.length - 1; i >= 0; i--) {
74
+ const msg = modifiedMessages[i];
75
+ if (msg.role !== "assistant" || !msg.tool_calls?.length) {
76
+ continue;
77
+ }
78
+
79
+ let didUnwrap = false;
80
+ const newToolCalls: ToolCall[] = msg.tool_calls.map((tc) => {
81
+ const unwrapped = this.unwrapCallTool(tc);
82
+ if (unwrapped) {
83
+ didUnwrap = true;
84
+ return unwrapped;
85
+ }
86
+ return tc;
87
+ });
88
+
89
+ if (didUnwrap) {
90
+ msg.tool_calls = newToolCalls;
91
+ }
92
+
93
+ // Only process the last assistant message
94
+ break;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Returns a MessageProcessorFunction suitable for registering on the
100
+ * `pre_tools` lifecycle of a MessageProcessor.
101
+ */
102
+ createProcessor(): MessageProcessorFunction {
103
+ return (
104
+ originalMessages: Message[],
105
+ modifiedMessages: Message[]
106
+ ): void => {
107
+ this.processMessages(originalMessages, modifiedMessages);
108
+ };
109
+ }
110
+ }
111
+
112
+ export const globalMinimalToolsMessageProcessor =
113
+ new MinimalToolsMessageProcessor();
@@ -155,14 +155,26 @@ export class TokenCompressor implements JsonCompressorStorage {
155
155
  * Check if content is already compressed
156
156
  */
157
157
  private isAlreadyCompressed(content: string): boolean {
158
- // Check for compressed string markers
159
- if (content.includes("[COMPRESSED_STRING")) {
158
+ // Check for a compressed-string marker, but ONLY when the content *starts*
159
+ // with it. Compressor output always begins with "[COMPRESSED_STRING ...".
160
+ // A naive `.includes()` produced false positives for large payloads (e.g. a
161
+ // usage.json / message-history dump) that merely CONTAIN that substring deep
162
+ // inside their nested data — those files were left completely uncompressed
163
+ // because this guard incorrectly reported them as already compressed.
164
+ if (content.trimStart().startsWith("[COMPRESSED_STRING")) {
160
165
  return true;
161
166
  }
162
167
 
163
168
  // Check for compressed JSON structure with schema key
169
+ // Only treat this as compressed when `_schema_key` is an actual TOP-LEVEL
170
+ // key of the parsed object — not merely present somewhere in the string.
164
171
  const parsed = this.tryParseJson(content);
165
- if (parsed && parsed._schema_key && typeof parsed._schema_key === "string") {
172
+ if (
173
+ parsed &&
174
+ typeof parsed === "object" &&
175
+ !Array.isArray(parsed) &&
176
+ typeof parsed._schema_key === "string"
177
+ ) {
166
178
  return true;
167
179
  }
168
180
 
@@ -296,7 +308,12 @@ export class TokenCompressor implements JsonCompressorStorage {
296
308
  return async (originalMessages: Message[], modifiedMessages: Message[]) => {
297
309
  // Find the index of the last assistant message so we only compress
298
310
  // the most recent batch of tool responses (after the last agent output).
299
- // This prevents re-compressing already-cached historical messages.
311
+ // This prevents re-compressing already-cached historical messages, which
312
+ // would break prompt caching. Newly-added tool responses (the "current
313
+ // batch") are the ones that need compressing before they reach the model,
314
+ // and they appear after the last assistant message at the moment this
315
+ // processor runs (post_tools / pre_call), when the fresh tool response is
316
+ // still the tail of the message list.
300
317
  let lastAssistantIndex = -1;
301
318
  for (let i = modifiedMessages.length - 1; i >= 0; i--) {
302
319
  if (modifiedMessages[i].role === "assistant") {
@@ -1,7 +1,13 @@
1
1
  export { Base64ImageProcessor } from "./Base64ImageDetector";
2
2
  export { CustomVariables } from "./CustomVariables";
3
3
  export { TokenCompressor } from "./TokenCompressor";
4
- export { JsonCompressor, JsonSchema, CompressionMetadata, JsonCompressorStorage } from "./JsonCompressor";
4
+ export {
5
+ JsonCompressor,
6
+ JsonSchema,
7
+ CompressionMetadata,
8
+ JsonCompressorStorage,
9
+ } from "./JsonCompressor";
5
10
  export { ToolResponseCache } from "./ToolResponseCache";
6
11
  export { XmlToolCallProcessor } from "./XmlToolCallProcessor";
7
12
  export { HarmonyToolProcessor } from "./HarmonyToolProcessor";
13
+ export { MinimalToolsMessageProcessor } from "./MinimalToolsMessageProcessor";
@@ -33,6 +33,7 @@ export class AgentSyncFs {
33
33
  private agent: BaseAgent | undefined;
34
34
  private threadUpdateHandler: ((...args: any[]) => void) | undefined;
35
35
  private doneHandler: ((...args: any[]) => void) | undefined;
36
+ private tokenUsageHandler: ((...args: any[]) => void) | undefined;
36
37
  /**
37
38
  * Tracks the most recent in-flight filesystem metadata update.
38
39
  * The done handler awaits this before finalizing, preventing a race where
@@ -118,6 +119,33 @@ export class AgentSyncFs {
118
119
  }
119
120
  }
120
121
 
122
+ /**
123
+ * Append a token usage entry (including the full message chain sent for
124
+ * that call) to usage.json. This is intended for debugging cache-hit
125
+ * issues, where comparing the message chains and usage values between
126
+ * consecutive calls can reveal when a cache prefix was invalidated.
127
+ */
128
+ private async appendUsageEntry(entry: any): Promise<void> {
129
+ if (!this.taskPath) return;
130
+
131
+ try {
132
+ const usagePath = path.join(this.taskPath, "usage.json");
133
+ let entries: any[] = [];
134
+
135
+ try {
136
+ const existingData = await fs.readFile(usagePath, "utf8");
137
+ entries = JSON.parse(existingData);
138
+ } catch {
139
+ // File doesn't exist or is invalid, start fresh
140
+ }
141
+
142
+ entries.push(entry);
143
+ await fs.writeFile(usagePath, JSON.stringify(entries, null, 2), "utf8");
144
+ } catch (error) {
145
+ console.error(`❌ Failed to append usage entry:`, error);
146
+ }
147
+ }
148
+
121
149
  /**
122
150
  * Update metadata file with current agent state
123
151
  */
@@ -352,6 +380,20 @@ export class AgentSyncFs {
352
380
  this.threadUpdateHandler
353
381
  );
354
382
 
383
+ // Listen to token usage events to record per-call usage + message chain
384
+ // (store reference for cleanup). This is written to usage.json so that
385
+ // cache-hit/cache-miss issues can be debugged by comparing the message
386
+ // chain and usage values sent/received on consecutive AI calls.
387
+ this.tokenUsageHandler = (usage: any) => {
388
+ this.appendUsageEntry(usage).catch((error) => {
389
+ console.error(`❌ Error recording token usage:`, error);
390
+ });
391
+ };
392
+ agent.agentEvents.on(
393
+ agent.eventTypes.tokenUsage,
394
+ this.tokenUsageHandler
395
+ );
396
+
355
397
  // Listen to completion event to finalize task (store reference for cleanup)
356
398
  this.doneHandler = (result: string) => {
357
399
  if (!this.taskId) {
@@ -495,6 +537,13 @@ export class AgentSyncFs {
495
537
  );
496
538
  this.threadUpdateHandler = undefined;
497
539
  }
540
+ if (this.tokenUsageHandler) {
541
+ this.agent.agentEvents.removeListener(
542
+ this.agent.eventTypes.tokenUsage,
543
+ this.tokenUsageHandler
544
+ );
545
+ this.tokenUsageHandler = undefined;
546
+ }
498
547
  if (this.doneHandler) {
499
548
  this.agent.agentEvents.removeListener(
500
549
  this.agent.eventTypes.done,
@@ -89,7 +89,7 @@ export class AgentSyncKnowhowWeb {
89
89
  try {
90
90
  await this.client.updateChatTask(knowhowTaskId, {
91
91
  threads: agent.getThreads(),
92
- totalCostUsd: agent.getTotalCostUsd(),
92
+ totalUsdCost: agent.getTotalCostUsd(),
93
93
  inProgress,
94
94
  ...(result ? { result } : {}),
95
95
  });
@@ -61,7 +61,7 @@ export interface CreateMessageTaskResponse {
61
61
 
62
62
  export interface UpdateOrgTaskRequest {
63
63
  threads: Message[][];
64
- totalCostUsd: number;
64
+ totalUsdCost: number;
65
65
  inProgress: boolean;
66
66
  result?: string;
67
67
  }
@@ -9,6 +9,18 @@ import {
9
9
  } from "../agents/tools/lazy";
10
10
  import { definitions } from "../agents/tools/lazy/definitions";
11
11
 
12
+ /*
13
+ *
14
+ * This ToolService design allows an agent to start with a few enabled tools,
15
+ * search and enable additional tools
16
+ *
17
+ * One drawback: enabling a tool will bust the cache, causing you to have to pay extra after enabling new tools
18
+ * Benefit: for providers with limits on how many tools you can support, this tool service allows you to have the minimal set of tools you need to use in the tools array. Smaller context usage
19
+ *
20
+ * In a longer running session, where the agent's tools accumulate over multiple interactions, the cache busting is minimized
21
+ * In web sessions where you get a new ToolService each interaction, then the cache busting is going to happen with each enable
22
+ *
23
+ */
12
24
  export class LazyToolsService extends ToolsService {
13
25
  private allTools: Tool[] = [];
14
26
  // Start with lazy tools enabled by default
@@ -0,0 +1,167 @@
1
+ import { ToolsService, ToolContext } from "./Tools";
2
+ import { Tool, ToolCall } from "../clients/types";
3
+ import { callTool } from "../agents/tools/minimal/callTool";
4
+ import { inspectTools } from "../agents/tools/minimal/inspectTools";
5
+ import { minimalDefinitions } from "../agents/tools/minimal/definitions";
6
+ import { finalAnswer } from "../agents/tools/finalAnswer";
7
+ import { includedTools } from "../agents/tools/list";
8
+
9
+ /*
10
+ * MinimalToolsService
11
+ *
12
+ * A cache-stable ToolService: the `tools:` array sent to the AI is always the
13
+ * same small set of base tools (callTool, inspectTools, finalAnswer + any
14
+ * explicitly registered base tools). All other tools live in `allTools` and
15
+ * are callable via callTool(name, args).
16
+ *
17
+ * Benefits over LazyToolsService:
18
+ * - The `tools:` array NEVER changes during a session → zero cache busts from tool changes.
19
+ * - Works on all providers and older models (no provider-specific defer_loading needed).
20
+ * - Agents discover tools via inspectTools() and call them via callTool().
21
+ */
22
+ export class MinimalToolsService extends ToolsService {
23
+ /** Full catalog of every registered tool (base + extended). */
24
+ private allToolsCatalog: Tool[] = [];
25
+
26
+ constructor(context?: ToolContext) {
27
+ super(context);
28
+ this.registerMinimalTools();
29
+ }
30
+
31
+ /**
32
+ * Registers the immutable base tools.
33
+ * These are the only tools that ever appear in the `tools:` array.
34
+ */
35
+ private registerMinimalTools(): void {
36
+ // finalAnswer definition is sourced from the included tools list
37
+ const finalAnswerDef = includedTools.find(
38
+ (t) => t.function.name === "finalAnswer"
39
+ );
40
+
41
+ const baseDefs: Tool[] = [
42
+ ...(finalAnswerDef ? [finalAnswerDef as Tool] : []),
43
+ ...minimalDefinitions,
44
+ ];
45
+
46
+ // Add base tool defs to both catalogs
47
+ this.allToolsCatalog.push(...baseDefs);
48
+ super.addTools(baseDefs);
49
+
50
+ // Register implementations
51
+ this.addFunctions({
52
+ finalAnswer: finalAnswer.bind(this),
53
+ callTool: callTool.bind(this),
54
+ inspectTools: inspectTools.bind(this),
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Override addTools so that newly-registered tools go into the full catalog
60
+ * but NOT into the visible `tools` array (which stays stable).
61
+ */
62
+ addTools(tools: Tool[]): void {
63
+ const existingNames = this.allToolsCatalog.map((t) => t.function.name);
64
+ const newTools = tools.filter(
65
+ (t) => !existingNames.includes(t.function.name)
66
+ );
67
+ this.allToolsCatalog.push(...newTools);
68
+ // Do NOT call super.addTools() — base tools array stays frozen.
69
+ }
70
+
71
+ addTool(tool: Tool): void {
72
+ this.addTools([tool]);
73
+ }
74
+
75
+ /**
76
+ * Returns only the stable base tools (what gets sent in `tools:` to the AI).
77
+ */
78
+ getTools(): Tool[] {
79
+ return super.getTools();
80
+ }
81
+
82
+ /**
83
+ * Returns every tool in the catalog (base + extended). Used by callTool /
84
+ * inspectTools for dispatching and schema lookup.
85
+ */
86
+ getAllTools(): Tool[] {
87
+ return this.allToolsCatalog;
88
+ }
89
+
90
+ /**
91
+ * Calls a tool by its resolved name, bypassing the enabled-tools check that
92
+ * the base class applies. Used internally by the callTool dispatch function.
93
+ * Returns the raw functionResp (unwrapped), used by the callTool meta-tool.
94
+ */
95
+ async callToolByName(
96
+ toolCall: ToolCall,
97
+ resolvedName: string
98
+ ): Promise<any> {
99
+ const result = await this.dispatchToTool(toolCall, resolvedName);
100
+ return result.functionResp;
101
+ }
102
+
103
+ /**
104
+ * Shared dispatch logic: temporarily surfaces the target tool (if it's not
105
+ * already visible) so the base ToolsService.callTool can find its
106
+ * definition and implementation, then restores the visible set.
107
+ * Used both by direct calls (agent calls the tool by its real name) and by
108
+ * the callTool meta-tool (agent calls callTool({ name, args })).
109
+ */
110
+ private async dispatchToTool(toolCall: ToolCall, resolvedName: string) {
111
+ // Temporarily surface the target tool in the visible list so base
112
+ // ToolsService.callTool can find its definition, then restore state.
113
+ const alreadyVisible = super
114
+ .getTools()
115
+ .some((t) => t.function.name === resolvedName);
116
+
117
+ if (!alreadyVisible) {
118
+ const toolDef = this.allToolsCatalog.find(
119
+ (t) => t.function.name === resolvedName
120
+ );
121
+ if (toolDef) {
122
+ super.addTools([toolDef]);
123
+ }
124
+ }
125
+
126
+ const result = await super.callTool(toolCall, [resolvedName]);
127
+
128
+ // Remove the temporarily-added tool to keep the visible set stable
129
+ if (!alreadyVisible) {
130
+ this.tools = this.tools.filter((t) => t.function.name !== resolvedName);
131
+ }
132
+
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * Delegates to the full catalog for resolution, then runs via callToolByName.
138
+ */
139
+ async callTool(toolCall: ToolCall, enabledTools?: string[]): Promise<any> {
140
+ const functionName = toolCall.function.name;
141
+
142
+ // Base tools (callTool, inspectTools, finalAnswer) → standard dispatch
143
+ const isBaseTool = super
144
+ .getTools()
145
+ .some((t) => t.function.name === functionName);
146
+
147
+ if (isBaseTool) {
148
+ return super.callTool(toolCall, this.getToolNames());
149
+ }
150
+
151
+ // Extended tools: called directly by name (not via the callTool meta-tool).
152
+ // These live only in allToolsCatalog, not in the frozen visible `tools`
153
+ // array, so we must temporarily surface them the same way callToolByName
154
+ // does, otherwise base ToolsService.getTool() won't find the definition
155
+ // and will throw "Tool ... not found" even though it's a registered tool.
156
+ const existsInCatalog = this.allToolsCatalog.some(
157
+ (t) => t.function.name === functionName
158
+ );
159
+
160
+ if (existsInCatalog) {
161
+ return this.dispatchToTool(toolCall, functionName);
162
+ }
163
+
164
+ // Not found anywhere - let the base class produce the standard error.
165
+ return super.callTool(toolCall, [functionName]);
166
+ }
167
+ }
@@ -25,6 +25,15 @@ export interface ToolContext {
25
25
  metadata?: { [key: string]: any };
26
26
  }
27
27
 
28
+ /*
29
+ *
30
+ * This ToolService design allows an agent to start with every tool
31
+ *
32
+ * Drawbacks:
33
+ * some providers have limits on how many tools you can use
34
+ * larger context bloat from having every tool defined in system prompt
35
+ *
36
+ */
28
37
  export class ToolsService {
29
38
  private context: ToolContext = {};
30
39
 
@@ -24,6 +24,7 @@ export * from "./EmbeddingsService";
24
24
  export * from "./S3";
25
25
  export * from "./Tools";
26
26
  export * from "./LazyToolsService";
27
+ export * from "./MinimalToolsService";
27
28
  export * as MCP from "./Mcp";
28
29
  export * from "./EmbeddingService";
29
30
  export * from "./DockerService";
package/src/worker.ts CHANGED
@@ -225,9 +225,16 @@ export async function worker(options?: {
225
225
  console.log(
226
226
  "Worker tools configured! Update knowhow.json to adjust which tools are allowed by the worker."
227
227
  );
228
+ console.log(
229
+ "Tunnel is disabled by default. Set worker.tunnel.enabled = true in knowhow.json to enable it."
230
+ );
228
231
  config.worker = {
229
232
  ...config.worker,
230
233
  allowedTools: Tools.getToolNames(),
234
+ tunnel: {
235
+ enabled: false,
236
+ ...config.worker?.tunnel,
237
+ },
231
238
  };
232
239
  await updateConfig(config);
233
240
  return;
@@ -83,6 +83,51 @@ describe("TokenCompressor", () => {
83
83
  expect(parsed.smallProp).toBe("small"); // Should remain unchanged
84
84
  expect(parsed.largeProp).toContain("[COMPRESSED_JSON_PROPERTY"); // Should be compressed
85
85
  });
86
+
87
+ it("should compress large content that merely CONTAINS a compression marker deep inside (regression)", () => {
88
+ // Regression for the false-positive in isAlreadyCompressed(): a large
89
+ // payload (e.g. a usage.json / message-history dump) whose body contains
90
+ // the substring "[COMPRESSED_STRING" deep inside its nested data was
91
+ // incorrectly treated as already-compressed and returned uncompressed,
92
+ // letting 1MB+ raw tool responses flow into createCompletion untouched.
93
+ const nestedMarker =
94
+ '[COMPRESSED_STRING - 10155 tokens in 3 chunks]\\nKey: compressed_abc123';
95
+ // Build a big JSON array whose entries embed the marker far from the start.
96
+ const entry = JSON.stringify({
97
+ role: "tool",
98
+ name: "readFile",
99
+ content: nestedMarker,
100
+ filler: "z".repeat(500),
101
+ });
102
+ const bigContent = "[" + new Array(200).fill(entry).join(",") + "]";
103
+
104
+ // Sanity: the raw content contains the marker but is NOT at the start.
105
+ expect(bigContent.includes("[COMPRESSED_STRING")).toBe(true);
106
+ expect(bigContent.trimStart().startsWith("[COMPRESSED_STRING")).toBe(false);
107
+
108
+ const result = tokenCompressor.compressContent(bigContent);
109
+
110
+ // It MUST actually get compressed, not returned untouched.
111
+ expect(result.length).toBeLessThan(bigContent.length);
112
+ });
113
+
114
+ it("should NOT recompress content that genuinely starts with a compression marker", () => {
115
+ // Idempotency: real compressor output begins with "[COMPRESSED_STRING".
116
+ const alreadyCompressed =
117
+ "[COMPRESSED_STRING - 999 tokens in 2 chunks]\nKey: compressed_xyz\nPath: \nPreview: ...";
118
+ const result = tokenCompressor.compressContent(alreadyCompressed);
119
+ expect(result).toBe(alreadyCompressed);
120
+ });
121
+
122
+ it("should NOT recompress JSON whose top-level _schema_key marks it compressed", () => {
123
+ // Idempotency: JSON-compressed output carries a top-level _schema_key.
124
+ const compressedJson = JSON.stringify({
125
+ _schema_key: "compressed_schema_1",
126
+ data: "x".repeat(40000),
127
+ });
128
+ const result = tokenCompressor.compressContent(compressedJson);
129
+ expect(result).toBe(compressedJson);
130
+ });
86
131
  });
87
132
 
88
133
  describe("compressStringInChunks", () => {