@tyvm/knowhow 0.0.128 → 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.
- package/package.json +2 -1
- package/scripts/cache-miss-debug.ts +365 -0
- package/src/agents/base/base.ts +119 -54
- package/src/agents/tools/execCommand.ts +26 -6
- package/src/agents/tools/minimal/callTool.ts +45 -0
- package/src/agents/tools/minimal/definitions.ts +50 -0
- package/src/agents/tools/minimal/index.ts +3 -0
- package/src/agents/tools/minimal/inspectTools.ts +27 -0
- package/src/chat/modules/AgentModule.ts +27 -8
- package/src/clients/contextLimits.ts +9 -0
- package/src/clients/index.ts +6 -0
- package/src/clients/meta.ts +18 -0
- package/src/clients/pricing/anthropic.ts +13 -0
- package/src/clients/pricing/fireworks.ts +18 -12
- package/src/clients/pricing/index.ts +1 -0
- package/src/clients/pricing/meta.ts +12 -0
- package/src/clients/pricing/openai.ts +13 -0
- package/src/clients/pricing/xai.ts +3 -0
- package/src/commands/services.ts +1 -1
- package/src/processors/MinimalToolsMessageProcessor.ts +113 -0
- package/src/processors/TokenCompressor.ts +21 -4
- package/src/processors/index.ts +7 -1
- package/src/services/AgentSyncFs.ts +49 -0
- package/src/services/AgentSyncKnowhowWeb.ts +1 -1
- package/src/services/KnowhowClient.ts +1 -1
- package/src/services/LazyToolsService.ts +12 -0
- package/src/services/MinimalToolsService.ts +167 -0
- package/src/services/Tools.ts +9 -0
- package/src/services/index.ts +1 -0
- package/src/worker.ts +7 -0
- package/tests/processors/TokenCompressor.test.ts +45 -0
- package/tests/unit/services/MinimalToolsService.test.ts +424 -0
- package/ts_build/package.json +2 -1
- package/ts_build/src/agents/base/base.d.ts +4 -1
- package/ts_build/src/agents/base/base.js +62 -31
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
- package/ts_build/src/agents/tools/execCommand.js +18 -5
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/minimal/callTool.d.ts +5 -0
- package/ts_build/src/agents/tools/minimal/callTool.js +28 -0
- package/ts_build/src/agents/tools/minimal/callTool.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/definitions.d.ts +4 -0
- package/ts_build/src/agents/tools/minimal/definitions.js +46 -0
- package/ts_build/src/agents/tools/minimal/definitions.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/index.d.ts +3 -0
- package/ts_build/src/agents/tools/minimal/index.js +20 -0
- package/ts_build/src/agents/tools/minimal/index.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.d.ts +2 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.js +15 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.js.map +1 -0
- package/ts_build/src/chat/modules/AgentModule.js +8 -2
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +8 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +1 -0
- package/ts_build/src/clients/index.js +6 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/meta.d.ts +4 -0
- package/ts_build/src/clients/meta.js +15 -0
- package/ts_build/src/clients/meta.js.map +1 -0
- package/ts_build/src/clients/pricing/anthropic.d.ts +4 -0
- package/ts_build/src/clients/pricing/anthropic.js +12 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.d.ts +2 -5
- package/ts_build/src/clients/pricing/fireworks.js +13 -11
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/clients/pricing/index.d.ts +1 -0
- package/ts_build/src/clients/pricing/index.js +5 -2
- package/ts_build/src/clients/pricing/index.js.map +1 -1
- package/ts_build/src/clients/pricing/meta.d.ts +6 -0
- package/ts_build/src/clients/pricing/meta.js +8 -0
- package/ts_build/src/clients/pricing/meta.js.map +1 -0
- package/ts_build/src/clients/pricing/openai.d.ts +3 -0
- package/ts_build/src/clients/pricing/openai.js +12 -0
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/xai.d.ts +1 -0
- package/ts_build/src/clients/pricing/xai.js +3 -0
- package/ts_build/src/clients/pricing/xai.js.map +1 -1
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/processors/MinimalToolsMessageProcessor.d.ts +7 -0
- package/ts_build/src/processors/MinimalToolsMessageProcessor.js +61 -0
- package/ts_build/src/processors/MinimalToolsMessageProcessor.js.map +1 -0
- package/ts_build/src/processors/TokenCompressor.js +5 -2
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/processors/index.d.ts +2 -1
- package/ts_build/src/processors/index.js +3 -1
- package/ts_build/src/processors/index.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
- package/ts_build/src/services/AgentSyncFs.js +30 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.d.ts +14 -0
- package/ts_build/src/services/MinimalToolsService.js +80 -0
- package/ts_build/src/services/MinimalToolsService.js.map +1 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +1 -0
- package/ts_build/src/services/index.js +1 -0
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/types.d.ts +8 -0
- package/ts_build/src/worker.js +5 -0
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +27 -0
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/unit/services/MinimalToolsService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/MinimalToolsService.test.js +320 -0
- 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
|
+
});
|
package/ts_build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
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 ??
|
|
293
|
-
usage.
|
|
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: [
|
|
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
|
|
494
|
+
promise
|
|
495
|
+
.then((result) => {
|
|
474
496
|
if (this._interruptResolve) {
|
|
475
497
|
this._interruptResolve = null;
|
|
476
498
|
resolve(result);
|
|
477
499
|
}
|
|
478
|
-
})
|
|
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: [
|
|
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.
|
|
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
|
-
}
|
|
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 &&
|
|
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
|
|
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
|
|
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 = [
|