@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,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.127",
3
+ "version": "0.0.129",
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",
@@ -109,13 +109,16 @@ export declare abstract class BaseAgent implements IAgent {
109
109
  getTurnCount(): number;
110
110
  adjustTotalCostUsd(cost: number): void;
111
111
  getTotalCostUsd(): number;
112
- adjustTokenUsage(usage: any): void;
112
+ adjustTokenUsage(usage: any, messages?: Message[]): void;
113
113
  getTokenUsage(): {
114
114
  totalInputTokens: number;
115
115
  totalOutputTokens: number;
116
116
  totalCacheReadTokens: number;
117
117
  totalCacheWriteTokens: number;
118
118
  };
119
+ protected createAgentCompletion(params: Parameters<GenericClient["createChatCompletion"]>[0], options?: {
120
+ interruptValue?: CompletionResponse;
121
+ }): Promise<CompletionResponse>;
119
122
  startNewThread(messages: Message[]): void;
120
123
  updateCurrentThread(messages: Message[]): void;
121
124
  getThreads(): Message[][];
@@ -284,19 +284,22 @@ class BaseAgent {
284
284
  getTotalCostUsd() {
285
285
  return this.totalCostUsd;
286
286
  }
287
- adjustTokenUsage(usage) {
287
+ adjustTokenUsage(usage, messages) {
288
288
  if (!usage)
289
289
  return;
290
290
  const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
291
291
  const outputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0;
292
- const cacheReadTokens = usage.cache_read_input_tokens ?? usage.cache_read_tokens ??
293
- usage.prompt_tokens_details?.cached_tokens ?? 0;
292
+ const cacheReadTokens = usage.cache_read_input_tokens ??
293
+ usage.cache_read_tokens ??
294
+ usage.prompt_tokens_details?.cached_tokens ??
295
+ 0;
294
296
  const cacheWriteTokens = usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
295
297
  this.totalInputTokens += inputTokens;
296
298
  this.totalOutputTokens += outputTokens;
297
299
  this.totalCacheReadTokens += cacheReadTokens;
298
300
  this.totalCacheWriteTokens += cacheWriteTokens;
299
301
  this.agentEvents.emit(this.eventTypes.tokenUsage, {
302
+ timestamp: new Date().toISOString(),
300
303
  inputTokens,
301
304
  outputTokens,
302
305
  cacheReadTokens,
@@ -305,6 +308,7 @@ class BaseAgent {
305
308
  totalOutputTokens: this.totalOutputTokens,
306
309
  totalCacheReadTokens: this.totalCacheReadTokens,
307
310
  totalCacheWriteTokens: this.totalCacheWriteTokens,
311
+ messages,
308
312
  });
309
313
  }
310
314
  getTokenUsage() {
@@ -315,6 +319,16 @@ class BaseAgent {
315
319
  totalCacheWriteTokens: this.totalCacheWriteTokens,
316
320
  };
317
321
  }
322
+ async createAgentCompletion(params, options = {}) {
323
+ const { interruptValue } = options;
324
+ const callPromise = this.getClient().createChatCompletion(params);
325
+ const response = interruptValue
326
+ ? await this.makeInterruptible(callPromise, interruptValue)
327
+ : await callPromise;
328
+ this.adjustTotalCostUsd(response?.usd_cost);
329
+ this.adjustTokenUsage(response?.usage, params.messages);
330
+ return response;
331
+ }
318
332
  startNewThread(messages) {
319
333
  this.currentThread++;
320
334
  this.agentEvents.emit(this.eventTypes.newThread, messages);
@@ -338,7 +352,14 @@ class BaseAgent {
338
352
  toolCallId: toolCall.id,
339
353
  functionName: toolCall.function?.name,
340
354
  functionArgs: {},
341
- toolMessages: [{ role: "tool", tool_call_id: toolCall.id, name: toolCall.function?.name, content: interruptMsg }],
355
+ toolMessages: [
356
+ {
357
+ role: "tool",
358
+ tool_call_id: toolCall.id,
359
+ name: toolCall.function?.name,
360
+ content: interruptMsg,
361
+ },
362
+ ],
342
363
  };
343
364
  const { functionResp, toolMessages } = await this.makeInterruptible(this.tools.callTool(toolCall, this.getEnabledToolNames()), interruptResult);
344
365
  this.agentEvents.emit(this.eventTypes.toolUsed, {
@@ -470,12 +491,14 @@ class BaseAgent {
470
491
  this._interruptResolve = null;
471
492
  resolve(value);
472
493
  };
473
- promise.then((result) => {
494
+ promise
495
+ .then((result) => {
474
496
  if (this._interruptResolve) {
475
497
  this._interruptResolve = null;
476
498
  resolve(result);
477
499
  }
478
- }).catch((err) => {
500
+ })
501
+ .catch((err) => {
479
502
  if (this._interruptResolve) {
480
503
  this._interruptResolve = null;
481
504
  }
@@ -536,18 +559,25 @@ class BaseAgent {
536
559
  const endIndex = messages.length;
537
560
  messages = await this.messageProcessor.processMessages(messages, "pre_call");
538
561
  const interruptResponse = {
539
- choices: [{ message: { role: "assistant", content: "User interrupted this AI completion. Please continue." } }],
562
+ choices: [
563
+ {
564
+ message: {
565
+ role: "assistant",
566
+ content: "User interrupted this AI completion. Please continue.",
567
+ },
568
+ },
569
+ ],
540
570
  model,
541
571
  usage: undefined,
542
572
  usd_cost: 0,
543
573
  };
544
- const response = await this.makeInterruptible(this.getClient().createChatCompletion({
574
+ const response = await this.createAgentCompletion({
545
575
  model,
546
576
  messages,
547
577
  tools: this.getEnabledTools(),
548
578
  tool_choice: "auto",
549
579
  long_ttl_cache: this.runTime() > 300_000,
550
- }), interruptResponse);
580
+ }, { interruptValue: interruptResponse });
551
581
  if (this.status === this.eventTypes.pause) {
552
582
  this.log("Agent was paused after completion, waiting before processing tool calls");
553
583
  await this.unpaused();
@@ -564,17 +594,15 @@ class BaseAgent {
564
594
  throw new Error(`AI response error: ${errMsg}`);
565
595
  }
566
596
  }
567
- this.adjustTotalCostUsd(response?.usd_cost);
568
- this.adjustTokenUsage(response?.usage);
569
597
  this.log("agent response cost: " + response?.usd_cost);
570
598
  for (const choice of response.choices) {
571
599
  messages.push(choice.message);
572
600
  messages = await this.messageProcessor.processMessages(messages, "post_call");
573
601
  const lastMessage = messages[messages.length - 1];
574
602
  this.logMessages([lastMessage]);
575
- const toolCalls = lastMessage.tool_calls;
576
603
  if (lastMessage.tool_calls) {
577
604
  messages = await this.messageProcessor.processMessages(messages, "pre_tools");
605
+ const toolCalls = messages[messages.length - 1].tool_calls;
578
606
  this.updateCurrentThread(messages);
579
607
  const truncationWarning = this.detectTruncatedToolCalls(toolCalls, response);
580
608
  if (truncationWarning) {
@@ -695,7 +723,10 @@ class BaseAgent {
695
723
  return this.call(userInput, _messages, retryCount + 1);
696
724
  }
697
725
  this.log(`Agent failed: ${e}`, "error");
698
- if (e != null && typeof e === "object" && "response" in e && "data" in e.response) {
726
+ if (e != null &&
727
+ typeof e === "object" &&
728
+ "response" in e &&
729
+ "data" in e.response) {
699
730
  this.log(`Error response data: ${JSON.stringify(e.response.data, null, 2)}`, "error");
700
731
  }
701
732
  this.agentEvents.emit(this.eventTypes.done, e.message);
@@ -817,18 +848,18 @@ class BaseAgent {
817
848
  Your output will be used to guide the work of the agent, and determine when we've accomplished the goal
818
849
  `;
819
850
  const model = this.getModel();
820
- const response = await this.getClient().createChatCompletion({
851
+ const taskBreakdownMessages = [
852
+ ...messages,
853
+ {
854
+ role: "user",
855
+ content: taskPrompt,
856
+ },
857
+ ];
858
+ const response = await this.createAgentCompletion({
821
859
  model,
822
- messages: [
823
- ...messages,
824
- {
825
- role: "user",
826
- content: taskPrompt,
827
- },
828
- ],
860
+ messages: taskBreakdownMessages,
829
861
  max_tokens: 2000,
830
862
  });
831
- this.adjustTotalCostUsd(response.usd_cost);
832
863
  this.taskBreakdown = response.choices[0].message.content;
833
864
  this.log(`task breakdown cost: ${response.usd_cost}`);
834
865
  return this.taskBreakdown;
@@ -851,17 +882,17 @@ class BaseAgent {
851
882
 
852
883
  `;
853
884
  const model = this.getModel();
854
- const response = await this.getClient().createChatCompletion({
885
+ const compressMessagesPayload = [
886
+ ...messages,
887
+ {
888
+ role: "user",
889
+ content: toCompressPrompt,
890
+ },
891
+ ];
892
+ const response = await this.createAgentCompletion({
855
893
  model,
856
- messages: [
857
- ...messages,
858
- {
859
- role: "user",
860
- content: toCompressPrompt,
861
- },
862
- ],
894
+ messages: compressMessagesPayload,
863
895
  });
864
- this.adjustTotalCostUsd(response.usd_cost);
865
896
  const summaries = response.choices.map((c) => c.message.content);
866
897
  this.summaries.push(...summaries);
867
898
  const startMessages = [