@tyvm/knowhow 0.0.128 → 0.0.130

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 (183) hide show
  1. package/package.json +5 -4
  2. package/scripts/cache-miss-debug.ts +365 -0
  3. package/src/agents/base/base.ts +159 -59
  4. package/src/agents/tools/execCommand.ts +26 -6
  5. package/src/agents/tools/index.ts +1 -0
  6. package/src/agents/tools/list.ts +2 -0
  7. package/src/agents/tools/minimal/callTool.ts +45 -0
  8. package/src/agents/tools/minimal/definitions.ts +50 -0
  9. package/src/agents/tools/minimal/index.ts +3 -0
  10. package/src/agents/tools/minimal/inspectTools.ts +27 -0
  11. package/src/agents/tools/skills.ts +79 -0
  12. package/src/chat/modules/AgentModule.ts +111 -9
  13. package/src/cli.ts +4 -0
  14. package/src/clients/anthropic.ts +59 -1
  15. package/src/clients/contextLimits.ts +9 -0
  16. package/src/clients/index.ts +6 -0
  17. package/src/clients/meta.ts +18 -0
  18. package/src/clients/openai.ts +28 -5
  19. package/src/clients/pricing/anthropic.ts +13 -0
  20. package/src/clients/pricing/fireworks.ts +18 -12
  21. package/src/clients/pricing/index.ts +1 -0
  22. package/src/clients/pricing/meta.ts +12 -0
  23. package/src/clients/pricing/openai.ts +16 -0
  24. package/src/clients/pricing/xai.ts +3 -0
  25. package/src/clients/types.ts +27 -4
  26. package/src/clients/xai.ts +2 -2
  27. package/src/commands/agent.ts +19 -0
  28. package/src/commands/behaviors.ts +47 -0
  29. package/src/commands/services.ts +7 -3
  30. package/src/commands/skills.ts +71 -0
  31. package/src/index.ts +2 -0
  32. package/src/plugins/PluginBase.ts +0 -2
  33. package/src/plugins/SkillsPlugin.ts +11 -124
  34. package/src/plugins/language.ts +0 -1
  35. package/src/plugins/plugins.ts +24 -13
  36. package/src/plugins/types.ts +8 -2
  37. package/src/processors/MinimalToolsMessageProcessor.ts +113 -0
  38. package/src/processors/TokenCompressor.ts +21 -4
  39. package/src/processors/index.ts +7 -1
  40. package/src/services/AgentSyncFs.ts +49 -0
  41. package/src/services/AgentSyncKnowhowWeb.ts +1 -1
  42. package/src/services/BehaviorsService.ts +432 -0
  43. package/src/services/KnowhowClient.ts +62 -1
  44. package/src/services/LazyToolsService.ts +12 -0
  45. package/src/services/MinimalToolsService.ts +167 -0
  46. package/src/services/SkillsService.ts +622 -0
  47. package/src/services/Tools.ts +11 -0
  48. package/src/services/index.ts +9 -2
  49. package/src/services/modules/types.ts +2 -0
  50. package/src/worker.ts +7 -0
  51. package/tests/processors/TokenCompressor.test.ts +45 -0
  52. package/tests/unit/services/MinimalToolsService.test.ts +424 -0
  53. package/ts_build/package.json +5 -4
  54. package/ts_build/src/agents/base/base.d.ts +13 -2
  55. package/ts_build/src/agents/base/base.js +91 -35
  56. package/ts_build/src/agents/base/base.js.map +1 -1
  57. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  58. package/ts_build/src/agents/tools/execCommand.js +18 -5
  59. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  60. package/ts_build/src/agents/tools/index.d.ts +1 -0
  61. package/ts_build/src/agents/tools/index.js +1 -0
  62. package/ts_build/src/agents/tools/index.js.map +1 -1
  63. package/ts_build/src/agents/tools/list.js +2 -0
  64. package/ts_build/src/agents/tools/list.js.map +1 -1
  65. package/ts_build/src/agents/tools/minimal/callTool.d.ts +5 -0
  66. package/ts_build/src/agents/tools/minimal/callTool.js +28 -0
  67. package/ts_build/src/agents/tools/minimal/callTool.js.map +1 -0
  68. package/ts_build/src/agents/tools/minimal/definitions.d.ts +4 -0
  69. package/ts_build/src/agents/tools/minimal/definitions.js +46 -0
  70. package/ts_build/src/agents/tools/minimal/definitions.js.map +1 -0
  71. package/ts_build/src/agents/tools/minimal/index.d.ts +3 -0
  72. package/ts_build/src/agents/tools/minimal/index.js +20 -0
  73. package/ts_build/src/agents/tools/minimal/index.js.map +1 -0
  74. package/ts_build/src/agents/tools/minimal/inspectTools.d.ts +2 -0
  75. package/ts_build/src/agents/tools/minimal/inspectTools.js +15 -0
  76. package/ts_build/src/agents/tools/minimal/inspectTools.js.map +1 -0
  77. package/ts_build/src/agents/tools/skills.d.ts +33 -0
  78. package/ts_build/src/agents/tools/skills.js +62 -0
  79. package/ts_build/src/agents/tools/skills.js.map +1 -0
  80. package/ts_build/src/chat/modules/AgentModule.d.ts +2 -0
  81. package/ts_build/src/chat/modules/AgentModule.js +77 -2
  82. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  83. package/ts_build/src/chat/modules/CustomCommandsModule.d.ts +1 -1
  84. package/ts_build/src/cli.js +4 -0
  85. package/ts_build/src/cli.js.map +1 -1
  86. package/ts_build/src/clients/anthropic.d.ts +2 -0
  87. package/ts_build/src/clients/anthropic.js +35 -0
  88. package/ts_build/src/clients/anthropic.js.map +1 -1
  89. package/ts_build/src/clients/contextLimits.js +8 -0
  90. package/ts_build/src/clients/contextLimits.js.map +1 -1
  91. package/ts_build/src/clients/index.d.ts +1 -0
  92. package/ts_build/src/clients/index.js +6 -0
  93. package/ts_build/src/clients/index.js.map +1 -1
  94. package/ts_build/src/clients/meta.d.ts +4 -0
  95. package/ts_build/src/clients/meta.js +15 -0
  96. package/ts_build/src/clients/meta.js.map +1 -0
  97. package/ts_build/src/clients/openai.d.ts +1 -0
  98. package/ts_build/src/clients/openai.js +25 -5
  99. package/ts_build/src/clients/openai.js.map +1 -1
  100. package/ts_build/src/clients/pricing/anthropic.d.ts +4 -0
  101. package/ts_build/src/clients/pricing/anthropic.js +12 -0
  102. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  103. package/ts_build/src/clients/pricing/fireworks.d.ts +2 -5
  104. package/ts_build/src/clients/pricing/fireworks.js +13 -11
  105. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  106. package/ts_build/src/clients/pricing/index.d.ts +1 -0
  107. package/ts_build/src/clients/pricing/index.js +5 -2
  108. package/ts_build/src/clients/pricing/index.js.map +1 -1
  109. package/ts_build/src/clients/pricing/meta.d.ts +6 -0
  110. package/ts_build/src/clients/pricing/meta.js +8 -0
  111. package/ts_build/src/clients/pricing/meta.js.map +1 -0
  112. package/ts_build/src/clients/pricing/openai.d.ts +3 -0
  113. package/ts_build/src/clients/pricing/openai.js +15 -0
  114. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  115. package/ts_build/src/clients/pricing/xai.d.ts +1 -0
  116. package/ts_build/src/clients/pricing/xai.js +3 -0
  117. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  118. package/ts_build/src/clients/types.d.ts +6 -1
  119. package/ts_build/src/clients/xai.js +1 -1
  120. package/ts_build/src/clients/xai.js.map +1 -1
  121. package/ts_build/src/commands/agent.js +16 -0
  122. package/ts_build/src/commands/agent.js.map +1 -1
  123. package/ts_build/src/commands/behaviors.d.ts +2 -0
  124. package/ts_build/src/commands/behaviors.js +43 -0
  125. package/ts_build/src/commands/behaviors.js.map +1 -0
  126. package/ts_build/src/commands/services.js +4 -2
  127. package/ts_build/src/commands/services.js.map +1 -1
  128. package/ts_build/src/commands/skills.d.ts +2 -0
  129. package/ts_build/src/commands/skills.js +61 -0
  130. package/ts_build/src/commands/skills.js.map +1 -0
  131. package/ts_build/src/index.d.ts +2 -0
  132. package/ts_build/src/index.js +5 -1
  133. package/ts_build/src/index.js.map +1 -1
  134. package/ts_build/src/plugins/PluginBase.d.ts +0 -2
  135. package/ts_build/src/plugins/PluginBase.js.map +1 -1
  136. package/ts_build/src/plugins/SkillsPlugin.d.ts +1 -6
  137. package/ts_build/src/plugins/SkillsPlugin.js +6 -135
  138. package/ts_build/src/plugins/SkillsPlugin.js.map +1 -1
  139. package/ts_build/src/plugins/language.js.map +1 -1
  140. package/ts_build/src/plugins/plugins.d.ts +4 -1
  141. package/ts_build/src/plugins/plugins.js +17 -11
  142. package/ts_build/src/plugins/plugins.js.map +1 -1
  143. package/ts_build/src/plugins/types.d.ts +3 -2
  144. package/ts_build/src/processors/MinimalToolsMessageProcessor.d.ts +7 -0
  145. package/ts_build/src/processors/MinimalToolsMessageProcessor.js +61 -0
  146. package/ts_build/src/processors/MinimalToolsMessageProcessor.js.map +1 -0
  147. package/ts_build/src/processors/TokenCompressor.js +5 -2
  148. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  149. package/ts_build/src/processors/index.d.ts +2 -1
  150. package/ts_build/src/processors/index.js +3 -1
  151. package/ts_build/src/processors/index.js.map +1 -1
  152. package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
  153. package/ts_build/src/services/AgentSyncFs.js +30 -0
  154. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  155. package/ts_build/src/services/AgentSyncKnowhowWeb.js +1 -1
  156. package/ts_build/src/services/BehaviorsService.d.ts +46 -0
  157. package/ts_build/src/services/BehaviorsService.js +358 -0
  158. package/ts_build/src/services/BehaviorsService.js.map +1 -0
  159. package/ts_build/src/services/KnowhowClient.d.ts +9 -1
  160. package/ts_build/src/services/KnowhowClient.js +20 -0
  161. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  162. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  163. package/ts_build/src/services/MinimalToolsService.d.ts +14 -0
  164. package/ts_build/src/services/MinimalToolsService.js +80 -0
  165. package/ts_build/src/services/MinimalToolsService.js.map +1 -0
  166. package/ts_build/src/services/SkillsService.d.ts +53 -0
  167. package/ts_build/src/services/SkillsService.js +478 -0
  168. package/ts_build/src/services/SkillsService.js.map +1 -0
  169. package/ts_build/src/services/Tools.d.ts +2 -0
  170. package/ts_build/src/services/Tools.js.map +1 -1
  171. package/ts_build/src/services/index.d.ts +4 -0
  172. package/ts_build/src/services/index.js +9 -2
  173. package/ts_build/src/services/index.js.map +1 -1
  174. package/ts_build/src/services/modules/index.d.ts +4 -2
  175. package/ts_build/src/services/modules/types.d.ts +2 -0
  176. package/ts_build/src/types.d.ts +8 -0
  177. package/ts_build/src/worker.js +5 -0
  178. package/ts_build/src/worker.js.map +1 -1
  179. package/ts_build/tests/processors/TokenCompressor.test.js +27 -0
  180. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  181. package/ts_build/tests/unit/services/MinimalToolsService.test.d.ts +1 -0
  182. package/ts_build/tests/unit/services/MinimalToolsService.test.js +320 -0
  183. package/ts_build/tests/unit/services/MinimalToolsService.test.js.map +1 -0
@@ -13,6 +13,7 @@ import { MediaProcessorService } from "../MediaProcessorService";
13
13
  import { TunnelHandler } from "@tyvm/knowhow-tunnel";
14
14
  import { EventService } from "../EventService";
15
15
  import { ConversionService } from "../conversion/ConversionService";
16
+ import { BehaviorsService } from "../BehaviorsService";
16
17
 
17
18
  /*
18
19
  *
@@ -61,6 +62,7 @@ export interface ModuleContext {
61
62
  Conversion?: ConversionService;
62
63
  Tunnel?: TunnelHandler;
63
64
  Program?: Command;
65
+ Behaviors?: BehaviorsService
64
66
  }
65
67
 
66
68
  export interface KnowhowModule {
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", () => {
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Tests for MinimalToolsService and MinimalToolsMessageProcessor.
3
+ *
4
+ * Key scenarios:
5
+ * 1. Base tools are always in the visible tools array (cache-stable).
6
+ * 2. Extended tools added via addTools() are NOT in the visible array.
7
+ * 3. callTool dispatches to any tool in the catalog by name.
8
+ * 4. inspectTools returns schemas from the full catalog.
9
+ * 5. MinimalToolsMessageProcessor unwraps callTool(name, args) in the last
10
+ * assistant message's tool_calls.
11
+ * 6. Unwrapping callTool("finalAnswer", ...) → finalAnswer so the agent
12
+ * terminates correctly.
13
+ * 7. Unwrapping callTool("testTool123", ...) → testTool123 so a custom
14
+ * required tool is satisfied.
15
+ * 8. Multiple callTool entries in one message are all unwrapped.
16
+ * 9. Only the last assistant message is rewritten (older messages untouched).
17
+ */
18
+
19
+ // ── Mocks (must come before any imports that touch config/clients) ──────────
20
+ jest.mock("../../../src/config", () => ({
21
+ getConfig: jest.fn(),
22
+ getGlobalConfig: jest.fn(),
23
+ getConfigSync: jest.fn().mockReturnValue({}),
24
+ }));
25
+
26
+ jest.mock("../../../src/clients", () => ({
27
+ AIClient: jest.fn(),
28
+ Clients: jest.fn(),
29
+ }));
30
+
31
+ jest.mock("../../../src/services", () => ({
32
+ services: jest.fn().mockReturnValue({
33
+ Clients: {},
34
+ Plugins: { listPlugins: jest.fn().mockReturnValue([]) },
35
+ Agents: {},
36
+ Tools: {},
37
+ }),
38
+ }));
39
+
40
+ // ── Imports ──────────────────────────────────────────────────────────────────
41
+ import { MinimalToolsService } from "../../../src/services/MinimalToolsService";
42
+ import { MinimalToolsMessageProcessor } from "../../../src/processors/MinimalToolsMessageProcessor";
43
+ import { Tool } from "../../../src/clients/types";
44
+ import { Message } from "../../../src/clients/types";
45
+
46
+ // ── Helpers ──────────────────────────────────────────────────────────────────
47
+
48
+ function makeTool(name: string): Tool {
49
+ return {
50
+ type: "function",
51
+ function: {
52
+ name,
53
+ description: `${name} tool`,
54
+ parameters: {
55
+ type: "object",
56
+ positional: false,
57
+ properties: {
58
+ input: { type: "string", description: "input" },
59
+ },
60
+ required: [],
61
+ },
62
+ },
63
+ };
64
+ }
65
+
66
+ function makeCallToolMessage(
67
+ toolName: string,
68
+ args: Record<string, any>,
69
+ id = "tc_1"
70
+ ): Message {
71
+ return {
72
+ role: "assistant",
73
+ content: null,
74
+ tool_calls: [
75
+ {
76
+ id,
77
+ type: "function",
78
+ function: {
79
+ name: "callTool",
80
+ arguments: JSON.stringify({ name: toolName, args }),
81
+ },
82
+ },
83
+ ],
84
+ } as unknown as Message;
85
+ }
86
+
87
+ // ── MinimalToolsService ───────────────────────────────────────────────────────
88
+
89
+ describe("MinimalToolsService", () => {
90
+ let service: MinimalToolsService;
91
+
92
+ beforeEach(() => {
93
+ service = new MinimalToolsService();
94
+ });
95
+
96
+ test("base tools are always visible", () => {
97
+ const names = service.getTools().map((t) => t.function.name);
98
+ expect(names).toContain("finalAnswer");
99
+ expect(names).toContain("callTool");
100
+ expect(names).toContain("inspectTools");
101
+ });
102
+
103
+ test("extended tools added via addTools() are NOT in visible tools", () => {
104
+ const extended = makeTool("readFile");
105
+ service.addTools([extended]);
106
+
107
+ const visibleNames = service.getTools().map((t) => t.function.name);
108
+ expect(visibleNames).not.toContain("readFile");
109
+ });
110
+
111
+ test("extended tools appear in getAllTools()", () => {
112
+ const extended = makeTool("readFile");
113
+ service.addTools([extended]);
114
+
115
+ const allNames = service.getAllTools().map((t) => t.function.name);
116
+ expect(allNames).toContain("readFile");
117
+ expect(allNames).toContain("finalAnswer");
118
+ });
119
+
120
+ test("visible tools array is stable after adding extended tools", () => {
121
+ const before = service.getTools().map((t) => t.function.name);
122
+ service.addTools([makeTool("writeFile"), makeTool("patchFile")]);
123
+ const after = service.getTools().map((t) => t.function.name);
124
+ expect(before).toEqual(after);
125
+ });
126
+
127
+ test("duplicate extended tools are not added twice", () => {
128
+ service.addTools([makeTool("readFile")]);
129
+ service.addTools([makeTool("readFile")]);
130
+ const allNames = service.getAllTools().map((t) => t.function.name);
131
+ const count = allNames.filter((n) => n === "readFile").length;
132
+ expect(count).toBe(1);
133
+ });
134
+
135
+ test("inspectTools returns schema for all tools when no patterns given", () => {
136
+ service.addTools([makeTool("readFile"), makeTool("writeFile")]);
137
+ const result = service.callTool(
138
+ {
139
+ id: "tc_inspect",
140
+ type: "function",
141
+ function: { name: "inspectTools", arguments: JSON.stringify({}) },
142
+ },
143
+ service.getToolNames()
144
+ );
145
+ // inspectTools is synchronous inside callTool, check via direct function call
146
+ const fn = service.getFunction("inspectTools");
147
+ expect(fn).toBeDefined();
148
+ const schemas = fn.call(service) as any[];
149
+ const names = schemas.map((s: any) => s.name);
150
+ expect(names).toContain("readFile");
151
+ expect(names).toContain("writeFile");
152
+ expect(names).toContain("finalAnswer");
153
+ });
154
+
155
+ test("inspectTools filters by glob pattern", () => {
156
+ service.addTools([makeTool("readFile"), makeTool("writeFile"), makeTool("patchFile")]);
157
+ const fn = service.getFunction("inspectTools");
158
+ const schemas = fn.call(service, ["*File"]) as any[];
159
+ const names = schemas.map((s: any) => s.name);
160
+ expect(names).toContain("readFile");
161
+ expect(names).toContain("writeFile");
162
+ expect(names).toContain("patchFile");
163
+ expect(names).not.toContain("finalAnswer");
164
+ expect(names).not.toContain("callTool");
165
+ });
166
+
167
+ test("callToolByName dispatches to an extended tool and returns response", async () => {
168
+ const mockFn = jest.fn().mockResolvedValue("file contents");
169
+ service.addTools([makeTool("readFile")]);
170
+ service.addFunctions({ readFile: mockFn });
171
+
172
+ const result = await service.callToolByName(
173
+ {
174
+ id: "tc_read",
175
+ type: "function",
176
+ function: { name: "readFile", arguments: JSON.stringify({ input: "path/to/file" }) },
177
+ },
178
+ "readFile"
179
+ );
180
+
181
+ expect(mockFn).toHaveBeenCalled();
182
+ expect(result).toBe("file contents");
183
+ });
184
+
185
+ test("callTool() dispatches an extended tool called directly by its real name (not via the callTool meta-tool wrapper)", async () => {
186
+ // This covers the case where an agent calls a registered tool directly
187
+ // (toolCall.function.name === "execCommand") instead of routing through
188
+ // the callTool({name, args}) meta-tool. Previously this threw
189
+ // "Tool execCommand not found" because extended tools only live in
190
+ // allToolsCatalog, not in the frozen visible `tools` array that the base
191
+ // ToolsService.getTool() searches.
192
+ const mockFn = jest.fn().mockResolvedValue("command output");
193
+ service.addTools([makeTool("execCommand")]);
194
+ service.addFunctions({ execCommand: mockFn });
195
+
196
+ const result = await service.callTool({
197
+ id: "tc_direct_exec",
198
+ type: "function",
199
+ function: {
200
+ name: "execCommand",
201
+ arguments: JSON.stringify({ input: "ls" }),
202
+ },
203
+ });
204
+
205
+ expect(mockFn).toHaveBeenCalled();
206
+ expect(result.functionResp).toBe("command output");
207
+ expect(result.toolMessages[0].content).toBe("command output");
208
+
209
+ // Visible tools array must remain stable (cache-stable guarantee) after
210
+ // dispatching a direct call to an extended tool.
211
+ const visibleNames = service.getTools().map((t) => t.function.name);
212
+ expect(visibleNames).not.toContain("execCommand");
213
+ });
214
+
215
+ test("callTool() returns a standard error for an unknown tool name", async () => {
216
+ const result = await service.callTool({
217
+ id: "tc_unknown",
218
+ type: "function",
219
+ function: { name: "totallyUnknownTool", arguments: JSON.stringify({}) },
220
+ });
221
+
222
+ expect(result.toolMessages[0].name).toBe("error");
223
+ expect(result.toolMessages[0].content).toMatch(/not found|not enabled/);
224
+ });
225
+ });
226
+
227
+ // ── MinimalToolsMessageProcessor ─────────────────────────────────────────────
228
+
229
+ describe("MinimalToolsMessageProcessor", () => {
230
+ let processor: MinimalToolsMessageProcessor;
231
+ let processorFn: ReturnType<MinimalToolsMessageProcessor["createProcessor"]>;
232
+
233
+ beforeEach(() => {
234
+ processor = new MinimalToolsMessageProcessor();
235
+ processorFn = processor.createProcessor();
236
+ });
237
+
238
+ function applyProcessor(messages: Message[]): Message[] {
239
+ const original = JSON.parse(JSON.stringify(messages)) as Message[];
240
+ const modified = JSON.parse(JSON.stringify(messages)) as Message[];
241
+ processorFn(original, modified);
242
+ return modified;
243
+ }
244
+
245
+ test("unwraps callTool(name, args) to the real tool call", () => {
246
+ const messages: Message[] = [
247
+ makeCallToolMessage("readFile", { filePath: "foo.ts" }, "tc_1"),
248
+ ];
249
+
250
+ const result = applyProcessor(messages);
251
+ const lastMsg = result[result.length - 1];
252
+
253
+ expect(lastMsg.tool_calls).toHaveLength(1);
254
+ expect(lastMsg.tool_calls[0].function.name).toBe("readFile");
255
+ expect(JSON.parse(lastMsg.tool_calls[0].function.arguments)).toEqual({
256
+ filePath: "foo.ts",
257
+ });
258
+ // ID preserved
259
+ expect(lastMsg.tool_calls[0].id).toBe("tc_1");
260
+ });
261
+
262
+ test("unwraps callTool('finalAnswer', ...) so agent terminates", () => {
263
+ const messages: Message[] = [
264
+ makeCallToolMessage("finalAnswer", { answer: "All done!" }, "tc_fa"),
265
+ ];
266
+
267
+ const result = applyProcessor(messages);
268
+ const lastMsg = result[result.length - 1];
269
+
270
+ expect(lastMsg.tool_calls[0].function.name).toBe("finalAnswer");
271
+ expect(JSON.parse(lastMsg.tool_calls[0].function.arguments)).toEqual({
272
+ answer: "All done!",
273
+ });
274
+ });
275
+
276
+ test("unwraps callTool('testTool123', ...) so custom required tool is satisfied", () => {
277
+ const messages: Message[] = [
278
+ makeCallToolMessage("testTool123", { data: "value" }, "tc_custom"),
279
+ ];
280
+
281
+ const result = applyProcessor(messages);
282
+ const lastMsg = result[result.length - 1];
283
+
284
+ expect(lastMsg.tool_calls[0].function.name).toBe("testTool123");
285
+ expect(JSON.parse(lastMsg.tool_calls[0].function.arguments)).toEqual({
286
+ data: "value",
287
+ });
288
+ });
289
+
290
+ test("unwraps multiple callTool entries in a single assistant message", () => {
291
+ const messages: Message[] = [
292
+ {
293
+ role: "assistant",
294
+ content: null,
295
+ tool_calls: [
296
+ {
297
+ id: "tc_a",
298
+ type: "function",
299
+ function: {
300
+ name: "callTool",
301
+ arguments: JSON.stringify({ name: "readFile", args: { filePath: "a.ts" } }),
302
+ },
303
+ },
304
+ {
305
+ id: "tc_b",
306
+ type: "function",
307
+ function: {
308
+ name: "callTool",
309
+ arguments: JSON.stringify({ name: "writeFile", args: { filePath: "b.ts", content: "hi" } }),
310
+ },
311
+ },
312
+ ],
313
+ } as unknown as Message,
314
+ ];
315
+
316
+ const result = applyProcessor(messages);
317
+ const lastMsg = result[result.length - 1];
318
+
319
+ expect(lastMsg.tool_calls).toHaveLength(2);
320
+ expect(lastMsg.tool_calls[0].function.name).toBe("readFile");
321
+ expect(lastMsg.tool_calls[1].function.name).toBe("writeFile");
322
+ expect(JSON.parse(lastMsg.tool_calls[0].function.arguments)).toEqual({ filePath: "a.ts" });
323
+ expect(JSON.parse(lastMsg.tool_calls[1].function.arguments)).toEqual({
324
+ filePath: "b.ts",
325
+ content: "hi",
326
+ });
327
+ });
328
+
329
+ test("does NOT rewrite older assistant messages (only the last one)", () => {
330
+ const messages: Message[] = [
331
+ // older assistant message with callTool — should NOT be rewritten
332
+ makeCallToolMessage("oldTool", { x: 1 }, "tc_old"),
333
+ // tool result for that call
334
+ {
335
+ role: "tool",
336
+ tool_call_id: "tc_old",
337
+ name: "callTool",
338
+ content: "some result",
339
+ } as unknown as Message,
340
+ // newest assistant message — should be rewritten
341
+ makeCallToolMessage("finalAnswer", { answer: "done" }, "tc_new"),
342
+ ];
343
+
344
+ const result = applyProcessor(messages);
345
+
346
+ // First assistant message should be unchanged (still "callTool")
347
+ const firstAssistant = result[0];
348
+ expect(firstAssistant.tool_calls[0].function.name).toBe("callTool");
349
+
350
+ // Last assistant message should be rewritten
351
+ const lastAssistant = result[result.length - 1];
352
+ expect(lastAssistant.tool_calls[0].function.name).toBe("finalAnswer");
353
+ });
354
+
355
+ test("leaves non-callTool tool_calls unchanged", () => {
356
+ const messages: Message[] = [
357
+ {
358
+ role: "assistant",
359
+ content: null,
360
+ tool_calls: [
361
+ {
362
+ id: "tc_direct",
363
+ type: "function",
364
+ function: {
365
+ name: "inspectTools",
366
+ arguments: JSON.stringify({ patterns: ["*"] }),
367
+ },
368
+ },
369
+ ],
370
+ } as unknown as Message,
371
+ ];
372
+
373
+ const result = applyProcessor(messages);
374
+ const lastMsg = result[result.length - 1];
375
+
376
+ expect(lastMsg.tool_calls[0].function.name).toBe("inspectTools");
377
+ });
378
+
379
+ test("skips messages with no tool_calls", () => {
380
+ const messages: Message[] = [
381
+ { role: "user", content: "hello" } as Message,
382
+ { role: "assistant", content: "I will help" } as Message,
383
+ ];
384
+
385
+ const result = applyProcessor(messages);
386
+ expect(result[1].tool_calls).toBeUndefined();
387
+ });
388
+
389
+ test("mixed: callTool entries alongside direct tool calls are selectively unwrapped", () => {
390
+ const messages: Message[] = [
391
+ {
392
+ role: "assistant",
393
+ content: null,
394
+ tool_calls: [
395
+ {
396
+ id: "tc_direct",
397
+ type: "function",
398
+ function: {
399
+ name: "inspectTools",
400
+ arguments: JSON.stringify({}),
401
+ },
402
+ },
403
+ {
404
+ id: "tc_wrapped",
405
+ type: "function",
406
+ function: {
407
+ name: "callTool",
408
+ arguments: JSON.stringify({ name: "readFile", args: { filePath: "x.ts" } }),
409
+ },
410
+ },
411
+ ],
412
+ } as unknown as Message,
413
+ ];
414
+
415
+ const result = applyProcessor(messages);
416
+ const lastMsg = result[result.length - 1];
417
+
418
+ expect(lastMsg.tool_calls).toHaveLength(2);
419
+ // Direct call preserved
420
+ expect(lastMsg.tool_calls[0].function.name).toBe("inspectTools");
421
+ // Wrapped call unwrapped
422
+ expect(lastMsg.tool_calls[1].function.name).toBe("readFile");
423
+ });
424
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.128",
3
+ "version": "0.0.130",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "lint:deps": "depcheck --config=.depcheckrc",
23
23
  "lint:all": "npm run lint && npm run lint:deps",
24
24
  "check:model-pricing": "ts-node scripts/check-model-pricing.ts",
25
+ "debug:cache-miss": "ts-node scripts/cache-miss-debug.ts",
25
26
  "version:nightly": "bash scripts/publish.sh version:nightly",
26
27
  "version:dev": "bash scripts/publish.sh version:dev",
27
28
  "publish:nightly": "bash scripts/publish.sh nightly",
@@ -34,7 +35,6 @@
34
35
  "license": "MIT",
35
36
  "devDependencies": {
36
37
  "@babel/preset-typescript": "^7.23.3",
37
- "typescript": "^5.0.0",
38
38
  "@types/diff": "^5.2.1",
39
39
  "@types/express": "^4.17.13",
40
40
  "@types/jest": "^29.5.13",
@@ -46,10 +46,11 @@
46
46
  "ts-jest": "^29.1.1",
47
47
  "ts-node": "^10.7.0",
48
48
  "tslint": "^6.1.3",
49
- "tslint-config-prettier": "^1.18.0"
49
+ "tslint-config-prettier": "^1.18.0",
50
+ "typescript": "^5.0.0"
50
51
  },
51
52
  "dependencies": {
52
- "@anthropic-ai/sdk": "^0.39.0",
53
+ "@anthropic-ai/sdk": "^0.111.0",
53
54
  "@google/genai": "^0.14.1",
54
55
  "@inquirer/editor": "^4.2.18",
55
56
  "@modelcontextprotocol/sdk": "^1.13.3",
@@ -1,4 +1,4 @@
1
- import { CompletionResponse, GenericClient, Message, MessageContent, Tool, ToolCall } from "../../clients/types";
1
+ import { CompletionResponse, CompletionOptions, GenericClient, Message, MessageContent, Tool, ToolCall } from "../../clients/types";
2
2
  import { IAgent } from "../interface";
3
3
  import { ToolsService } from "../../services/Tools";
4
4
  import { EventService } from "../../services/EventService";
@@ -8,6 +8,8 @@ export { Message, Tool, ToolCall };
8
8
  export interface ModelPreference {
9
9
  model: string;
10
10
  provider: keyof typeof Clients.clients;
11
+ reasoning_effort?: CompletionOptions["reasoning_effort"];
12
+ reasoning_summary?: boolean;
11
13
  }
12
14
  export interface AgentContext {
13
15
  Tools?: ToolsService;
@@ -38,6 +40,8 @@ export declare abstract class BaseAgent implements IAgent {
38
40
  protected turnCount: number;
39
41
  protected totalCostUsd: number;
40
42
  protected currentThread: number;
43
+ protected reasoningEffort: CompletionOptions["reasoning_effort"] | undefined;
44
+ protected summarizeReasoning: boolean | undefined;
41
45
  protected totalInputTokens: number;
42
46
  protected totalOutputTokens: number;
43
47
  protected totalCacheReadTokens: number;
@@ -94,6 +98,10 @@ export declare abstract class BaseAgent implements IAgent {
94
98
  setModel(value: string): void;
95
99
  getProvider(): string;
96
100
  setProvider(value: keyof typeof Clients.clients): void;
101
+ setReasoningEffort(effort: CompletionOptions["reasoning_effort"]): void;
102
+ getReasoningEffort(): CompletionOptions["reasoning_effort"];
103
+ setSummarizeReasoning(value: boolean): void;
104
+ getSummarizeReasoning(): boolean | undefined;
97
105
  getClient(): GenericClient;
98
106
  setClient(client: GenericClient): void;
99
107
  setEasyFinalAnswer(value: boolean): void;
@@ -109,13 +117,16 @@ export declare abstract class BaseAgent implements IAgent {
109
117
  getTurnCount(): number;
110
118
  adjustTotalCostUsd(cost: number): void;
111
119
  getTotalCostUsd(): number;
112
- adjustTokenUsage(usage: any): void;
120
+ adjustTokenUsage(usage: any, messages?: Message[]): void;
113
121
  getTokenUsage(): {
114
122
  totalInputTokens: number;
115
123
  totalOutputTokens: number;
116
124
  totalCacheReadTokens: number;
117
125
  totalCacheWriteTokens: number;
118
126
  };
127
+ protected createAgentCompletion(params: Parameters<GenericClient["createChatCompletion"]>[0], options?: {
128
+ interruptValue?: CompletionResponse;
129
+ }): Promise<CompletionResponse>;
119
130
  startNewThread(messages: Message[]): void;
120
131
  updateCurrentThread(messages: Message[]): void;
121
132
  getThreads(): Message[][];