@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.
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 +26 -6
  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 +18 -5
  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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.128",
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",
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env npx ts-node
2
+ /**
3
+ * cache-miss-debug.ts
4
+ *
5
+ * Debug helper for analyzing usage.json files written by AgentSyncFs
6
+ * (`.knowhow/processes/agents/<taskId>/usage.json`).
7
+ *
8
+ * Each entry in usage.json corresponds to one AI completion call and looks
9
+ * like:
10
+ * {
11
+ * timestamp, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
12
+ * totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens,
13
+ * messages: [...] // exact message chain sent to the model for that call
14
+ * }
15
+ *
16
+ * This script helps answer: "did a particular call miss the prompt cache?"
17
+ * and "what changed in the message chain between call N and call N+1 that
18
+ * broke the cache prefix?"
19
+ *
20
+ * Usage:
21
+ * npx ts-node scripts/cache-miss-debug.ts <path-to-usage.json> [options]
22
+ *
23
+ * Options:
24
+ * --summary Print a per-call table of token/cache stats + hit/miss verdict (default if no other option given)
25
+ * --diff <a> <b> Print a diff of the `messages` array between call index a and call index b (0-based)
26
+ * --dump <n> Dump the full `messages` array for call index n as JSON
27
+ * --dump-message <n> <m> Dump a single message (index m) from call n's messages array
28
+ * --json Output the summary as JSON instead of a table
29
+ *
30
+ * Examples:
31
+ * npx ts-node scripts/cache-miss-debug.ts .knowhow/processes/agents/123-foo/usage.json
32
+ * npx ts-node scripts/cache-miss-debug.ts .knowhow/processes/agents/123-foo/usage.json --diff 1 2
33
+ * npx ts-node scripts/cache-miss-debug.ts .knowhow/processes/agents/123-foo/usage.json --dump 2
34
+ */
35
+
36
+ import * as fs from "fs";
37
+ import * as path from "path";
38
+ import { diffJson } from "diff";
39
+
40
+ interface UsageEntry {
41
+ timestamp?: string;
42
+ inputTokens: number;
43
+ outputTokens: number;
44
+ cacheReadTokens: number;
45
+ cacheWriteTokens: number;
46
+ totalInputTokens?: number;
47
+ totalOutputTokens?: number;
48
+ totalCacheReadTokens?: number;
49
+ totalCacheWriteTokens?: number;
50
+ messages?: any[];
51
+ [key: string]: any;
52
+ }
53
+
54
+ interface Verdict {
55
+ index: number;
56
+ timestamp: string;
57
+ inputTokens: number;
58
+ cacheReadTokens: number;
59
+ cacheWriteTokens: number;
60
+ cumulativeWritesBefore: number;
61
+ status: "cold-start" | "hit" | "partial-hit" | "miss";
62
+ note: string;
63
+ }
64
+
65
+ function loadUsage(filePath: string): UsageEntry[] {
66
+ const resolved = path.resolve(filePath);
67
+ if (!fs.existsSync(resolved)) {
68
+ throw new Error(`File not found: ${resolved}`);
69
+ }
70
+ const raw = fs.readFileSync(resolved, "utf8");
71
+ const parsed = JSON.parse(raw);
72
+ if (!Array.isArray(parsed)) {
73
+ throw new Error(
74
+ `Expected usage.json to contain a JSON array of call entries, got: ${typeof parsed}`
75
+ );
76
+ }
77
+ return parsed as UsageEntry[];
78
+ }
79
+
80
+ /**
81
+ * Compute a hit/miss verdict for each call by comparing cacheReadTokens
82
+ * against the cumulative cacheWriteTokens available from all prior calls.
83
+ *
84
+ * - cold-start: first call, no cache could exist yet (cacheReadTokens ~ 0 expected)
85
+ * - hit: cacheReadTokens roughly matches (or exceeds a large portion of) prior cumulative writes
86
+ * - partial-hit: cacheReadTokens is nonzero but noticeably less than what was available
87
+ * - miss: cacheReadTokens is 0 (or near 0) despite prior writes being available
88
+ */
89
+ function computeVerdicts(entries: UsageEntry[]): Verdict[] {
90
+ const verdicts: Verdict[] = [];
91
+ let cumulativeWrites = 0;
92
+
93
+ entries.forEach((entry, index) => {
94
+ const cacheReadTokens = entry.cacheReadTokens ?? 0;
95
+ const cacheWriteTokens = entry.cacheWriteTokens ?? 0;
96
+ const cumulativeWritesBefore = cumulativeWrites;
97
+
98
+ let status: Verdict["status"];
99
+ let note = "";
100
+
101
+ if (index === 0) {
102
+ status = "cold-start";
103
+ note = "First call — no prior cache expected.";
104
+ } else if (cumulativeWritesBefore === 0) {
105
+ status = cacheReadTokens > 0 ? "hit" : "cold-start";
106
+ note = "No prior writes recorded to compare against.";
107
+ } else if (cacheReadTokens === 0) {
108
+ status = "miss";
109
+ note = `Expected up to ${cumulativeWritesBefore} cached tokens available, got 0 reads — full cache miss.`;
110
+ } else if (cacheReadTokens < cumulativeWritesBefore * 0.5) {
111
+ status = "partial-hit";
112
+ note = `Read ${cacheReadTokens} of up to ${cumulativeWritesBefore} available cached tokens — partial miss, prefix likely diverged partway through.`;
113
+ } else {
114
+ status = "hit";
115
+ note = `Read ${cacheReadTokens} tokens, consistent with prior cumulative writes (${cumulativeWritesBefore}).`;
116
+ }
117
+
118
+ verdicts.push({
119
+ index,
120
+ timestamp: entry.timestamp || "unknown",
121
+ inputTokens: entry.inputTokens ?? 0,
122
+ cacheReadTokens,
123
+ cacheWriteTokens,
124
+ cumulativeWritesBefore,
125
+ status,
126
+ note,
127
+ });
128
+
129
+ cumulativeWrites += cacheWriteTokens;
130
+ });
131
+
132
+ return verdicts;
133
+ }
134
+
135
+
136
+ function fmtNum(n: number): string {
137
+ return n.toLocaleString();
138
+ }
139
+
140
+ function statusEmoji(status: Verdict["status"]): string {
141
+ switch (status) {
142
+ case "cold-start":
143
+ return "🧊";
144
+ case "hit":
145
+ return "✅";
146
+ case "partial-hit":
147
+ return "🟡";
148
+ case "miss":
149
+ return "🚨";
150
+ }
151
+ }
152
+
153
+ function printSummaryTable(entries: UsageEntry[], verdicts: Verdict[]) {
154
+ const header = [
155
+ "#",
156
+ "Time",
157
+ "Input",
158
+ "Output",
159
+ "CacheRead",
160
+ "CacheWrite",
161
+ "Status",
162
+ ];
163
+ const rows = verdicts.map((v, i) => [
164
+ String(v.index),
165
+ v.timestamp,
166
+ fmtNum(v.inputTokens),
167
+ fmtNum(entries[i].outputTokens ?? 0),
168
+ fmtNum(v.cacheReadTokens),
169
+ fmtNum(v.cacheWriteTokens),
170
+ `${statusEmoji(v.status)} ${v.status}`,
171
+ ]);
172
+
173
+ const widths = header.map((h, i) =>
174
+ Math.max(h.length, ...rows.map((r) => r[i].length))
175
+ );
176
+
177
+ const printRow = (cols: string[]) =>
178
+ console.log(
179
+ "| " + cols.map((c, i) => c.padEnd(widths[i])).join(" | ") + " |"
180
+ );
181
+
182
+ printRow(header);
183
+ printRow(widths.map((w) => "-".repeat(w)));
184
+ rows.forEach(printRow);
185
+
186
+ console.log("");
187
+ const misses = verdicts.filter((v) => v.status === "miss");
188
+ const partials = verdicts.filter((v) => v.status === "partial-hit");
189
+ if (misses.length === 0 && partials.length === 0) {
190
+ console.log("✅ No cache misses detected — caching looks healthy.");
191
+ } else {
192
+ console.log(
193
+ `🚨 Found ${misses.length} full miss(es) and ${partials.length} partial-hit(s):`
194
+ );
195
+ [...misses, ...partials].forEach((v) => {
196
+ console.log(` - Call #${v.index} (${v.timestamp}): ${v.note}`);
197
+ });
198
+ console.log(
199
+ "\nTip: use --diff <a> <b> to compare the message chains between two calls, e.g.:"
200
+ );
201
+ [...misses, ...partials].forEach((v) => {
202
+ if (v.index > 0) {
203
+ console.log(
204
+ ` npx ts-node scripts/cache-miss-debug.ts <file> --diff ${
205
+ v.index - 1
206
+ } ${v.index}`
207
+ );
208
+ }
209
+ });
210
+ }
211
+ }
212
+
213
+ function printJsonSummary(verdicts: Verdict[]) {
214
+ console.log(JSON.stringify(verdicts, null, 2));
215
+ }
216
+
217
+ /**
218
+ * Diff the messages array of two calls. Uses a structural JSON diff so
219
+ * additions/removals/reordering of individual messages (and fields within
220
+ * them, like tool_call ids or content) are all visible.
221
+ */
222
+ function printMessagesDiff(entries: UsageEntry[], aIdx: number, bIdx: number) {
223
+ if (!entries[aIdx] || !entries[bIdx]) {
224
+ throw new Error(
225
+ `Invalid indices: file has ${entries.length} entries (valid range 0-${
226
+ entries.length - 1
227
+ })`
228
+ );
229
+ }
230
+
231
+ const a = entries[aIdx].messages ?? [];
232
+ const b = entries[bIdx].messages ?? [];
233
+
234
+ console.log(
235
+ `\nDiffing messages: call #${aIdx} (${entries[aIdx].timestamp}) -> call #${bIdx} (${entries[bIdx].timestamp})`
236
+ );
237
+ console.log(` call #${aIdx}: ${a.length} messages`);
238
+ console.log(` call #${bIdx}: ${b.length} messages\n`);
239
+
240
+ const diff = diffJson(a, b);
241
+ diff.forEach((part) => {
242
+ const color = part.added ? "\x1b[32m" : part.removed ? "\x1b[31m" : "";
243
+ const reset = part.added || part.removed ? "\x1b[0m" : "";
244
+ const prefix = part.added ? "+ " : part.removed ? "- " : " ";
245
+ const lines = part.value.split("\n");
246
+ lines.forEach((line, i) => {
247
+ if (line === "" && i === lines.length - 1) return; // trailing empty line from split
248
+ console.log(`${color}${prefix}${line}${reset}`);
249
+ });
250
+ });
251
+ }
252
+
253
+ function dumpMessages(entries: UsageEntry[], idx: number) {
254
+ if (!entries[idx]) {
255
+ throw new Error(
256
+ `Invalid index ${idx}: file has ${entries.length} entries (valid range 0-${
257
+ entries.length - 1
258
+ })`
259
+ );
260
+ }
261
+ console.log(JSON.stringify(entries[idx].messages ?? [], null, 2));
262
+ }
263
+
264
+ function dumpMessage(entries: UsageEntry[], callIdx: number, msgIdx: number) {
265
+ if (!entries[callIdx]) {
266
+ throw new Error(
267
+ `Invalid call index ${callIdx}: file has ${entries.length} entries`
268
+ );
269
+ }
270
+ const messages = entries[callIdx].messages ?? [];
271
+ if (!messages[msgIdx]) {
272
+ throw new Error(
273
+ `Invalid message index ${msgIdx}: call #${callIdx} has ${messages.length} messages`
274
+ );
275
+ }
276
+ console.log(JSON.stringify(messages[msgIdx], null, 2));
277
+ }
278
+
279
+
280
+ function printUsage() {
281
+ console.log(`
282
+ cache-miss-debug.ts — inspect usage.json files for prompt-cache issues
283
+
284
+ Usage:
285
+ npx ts-node scripts/cache-miss-debug.ts <path-to-usage.json> [options]
286
+
287
+ Options:
288
+ --summary Print a per-call table of token/cache stats + hit/miss verdict (default)
289
+ --json Output the summary as JSON instead of a table
290
+ --diff <a> <b> Diff the messages array between call index a and call index b (0-based)
291
+ --dump <n> Dump the full messages array for call index n as JSON
292
+ --dump-message <n> <m> Dump a single message (index m) from call n's messages array
293
+
294
+ Examples:
295
+ npx ts-node scripts/cache-miss-debug.ts .knowhow/processes/agents/123-foo/usage.json
296
+ npx ts-node scripts/cache-miss-debug.ts .knowhow/processes/agents/123-foo/usage.json --diff 1 2
297
+ npx ts-node scripts/cache-miss-debug.ts .knowhow/processes/agents/123-foo/usage.json --dump 2
298
+ npx ts-node scripts/cache-miss-debug.ts .knowhow/processes/agents/123-foo/usage.json --dump-message 2 0
299
+ `);
300
+ }
301
+
302
+ function main() {
303
+ const args = process.argv.slice(2);
304
+ const filePath = args.find((a) => !a.startsWith("--"));
305
+
306
+ if (!filePath || args.includes("--help") || args.includes("-h")) {
307
+ printUsage();
308
+ process.exit(filePath ? 0 : 1);
309
+ }
310
+
311
+ const entries = loadUsage(filePath);
312
+
313
+ if (entries.length === 0) {
314
+ console.log("usage.json contains no entries.");
315
+ return;
316
+ }
317
+
318
+ const diffFlagIdx = args.indexOf("--diff");
319
+ const dumpFlagIdx = args.indexOf("--dump");
320
+ const dumpMessageFlagIdx = args.indexOf("--dump-message");
321
+
322
+ if (diffFlagIdx !== -1) {
323
+ const a = parseInt(args[diffFlagIdx + 1], 10);
324
+ const b = parseInt(args[diffFlagIdx + 2], 10);
325
+ if (Number.isNaN(a) || Number.isNaN(b)) {
326
+ console.error("--diff requires two numeric indices, e.g. --diff 1 2");
327
+ process.exit(1);
328
+ }
329
+ printMessagesDiff(entries, a, b);
330
+ return;
331
+ }
332
+
333
+ if (dumpMessageFlagIdx !== -1) {
334
+ const callIdx = parseInt(args[dumpMessageFlagIdx + 1], 10);
335
+ const msgIdx = parseInt(args[dumpMessageFlagIdx + 2], 10);
336
+ if (Number.isNaN(callIdx) || Number.isNaN(msgIdx)) {
337
+ console.error(
338
+ "--dump-message requires two numeric indices, e.g. --dump-message 2 0"
339
+ );
340
+ process.exit(1);
341
+ }
342
+ dumpMessage(entries, callIdx, msgIdx);
343
+ return;
344
+ }
345
+
346
+ if (dumpFlagIdx !== -1) {
347
+ const idx = parseInt(args[dumpFlagIdx + 1], 10);
348
+ if (Number.isNaN(idx)) {
349
+ console.error("--dump requires a numeric index, e.g. --dump 2");
350
+ process.exit(1);
351
+ }
352
+ dumpMessages(entries, idx);
353
+ return;
354
+ }
355
+
356
+ // Default: summary
357
+ const verdicts = computeVerdicts(entries);
358
+ if (args.includes("--json")) {
359
+ printJsonSummary(verdicts);
360
+ } else {
361
+ printSummaryTable(entries, verdicts);
362
+ }
363
+ }
364
+
365
+ main();
@@ -305,14 +305,16 @@ export abstract class BaseAgent implements IAgent {
305
305
  // Short response (≤ 3 words) that matches a termination word/phrase exactly
306
306
  const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
307
307
  if (wordCount <= 3) {
308
- const terminationPattern = /^(done|complete|completed|finished|final\s*answer|task\s*complete|all\s*done|that'?s\s*(all|it)|ok(ay)?|yes)[.!]*$/i;
308
+ const terminationPattern =
309
+ /^(done|complete|completed|finished|final\s*answer|task\s*complete|all\s*done|that'?s\s*(all|it)|ok(ay)?|yes)[.!]*$/i;
309
310
  if (terminationPattern.test(trimmed)) return true;
310
311
  }
311
312
 
312
313
  // Check if the first 1-3 words indicate task completion (for longer responses)
313
314
  // e.g. "Task complete: ...", "All done.", "No further changes needed.", "Confirmed complete."
314
315
  const firstWords = trimmed.split(/\s+/).slice(0, 3).join(" ");
315
- const firstWordPattern = /^(task\s*(complete|completed|done|finished)|all\s*done|no\s*(further|more|additional|changes|action)|confirmed?\s*(complete|done|finished|one\s*last)|nothing\s*(more|further|else)|standing\s*by|everything\s*is|still\s*confirmed|acknowledged|done\s*and|complete\s*(and|\.)|completed\s*successfully|no\s*additional|verified\s*and)/i;
316
+ const firstWordPattern =
317
+ /^(task\s*(complete|completed|done|finished)|all\s*done|no\s*(further|more|additional|changes|action)|confirmed?\s*(complete|done|finished|one\s*last)|nothing\s*(more|further|else)|standing\s*by|everything\s*is|still\s*confirmed|acknowledged|done\s*and|complete\s*(and|\.)|completed\s*successfully|no\s*additional|verified\s*and)/i;
316
318
  if (firstWordPattern.test(firstWords)) return true;
317
319
 
318
320
  // If easyFinalAnswer mode is on, also match response starting with "✅" or numbered confirmation lists
@@ -418,15 +420,17 @@ export abstract class BaseAgent implements IAgent {
418
420
  return this.totalCostUsd;
419
421
  }
420
422
 
421
- adjustTokenUsage(usage: any) {
423
+ adjustTokenUsage(usage: any, messages?: Message[]) {
422
424
  if (!usage) return;
423
425
  // Support both OpenAI-style (prompt_tokens/completion_tokens) and Anthropic-style (input_tokens/output_tokens)
424
426
  const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
425
427
  const outputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0;
426
428
 
427
429
  const cacheReadTokens =
428
- usage.cache_read_input_tokens ?? usage.cache_read_tokens ??
429
- usage.prompt_tokens_details?.cached_tokens ?? 0;
430
+ usage.cache_read_input_tokens ??
431
+ usage.cache_read_tokens ??
432
+ usage.prompt_tokens_details?.cached_tokens ??
433
+ 0;
430
434
  const cacheWriteTokens =
431
435
  usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
432
436
 
@@ -436,6 +440,7 @@ export abstract class BaseAgent implements IAgent {
436
440
  this.totalCacheWriteTokens += cacheWriteTokens;
437
441
 
438
442
  this.agentEvents.emit(this.eventTypes.tokenUsage, {
443
+ timestamp: new Date().toISOString(),
439
444
  inputTokens,
440
445
  outputTokens,
441
446
  cacheReadTokens,
@@ -444,6 +449,7 @@ export abstract class BaseAgent implements IAgent {
444
449
  totalOutputTokens: this.totalOutputTokens,
445
450
  totalCacheReadTokens: this.totalCacheReadTokens,
446
451
  totalCacheWriteTokens: this.totalCacheWriteTokens,
452
+ messages,
447
453
  });
448
454
  }
449
455
 
@@ -456,6 +462,37 @@ export abstract class BaseAgent implements IAgent {
456
462
  };
457
463
  }
458
464
 
465
+ /**
466
+ * Centralized helper for making an AI completion call while ensuring
467
+ * cost (totalCostUsd) and token usage (input/output/cache) are always
468
+ * updated consistently, regardless of which part of the agent triggers
469
+ * the completion (main call loop, task breakdown, compression, etc).
470
+ *
471
+ * Optionally supports being interrupted via makeInterruptible by passing
472
+ * an `interruptValue` in options - if the operation is interrupted, the
473
+ * interrupt value is returned and used for cost/usage tracking (which,
474
+ * for a typical interrupt stub with no usage/cost, is effectively a
475
+ * no-op).
476
+ */
477
+ protected async createAgentCompletion(
478
+ params: Parameters<GenericClient["createChatCompletion"]>[0],
479
+ options: {
480
+ interruptValue?: CompletionResponse;
481
+ } = {}
482
+ ): Promise<CompletionResponse> {
483
+ const { interruptValue } = options;
484
+
485
+ const callPromise = this.getClient().createChatCompletion(params);
486
+ const response = interruptValue
487
+ ? await this.makeInterruptible(callPromise, interruptValue)
488
+ : await callPromise;
489
+
490
+ this.adjustTotalCostUsd(response?.usd_cost);
491
+ this.adjustTokenUsage(response?.usage, params.messages);
492
+
493
+ return response;
494
+ }
495
+
459
496
  startNewThread(messages: Message[]) {
460
497
  this.currentThread++;
461
498
  this.agentEvents.emit(this.eventTypes.newThread, messages);
@@ -488,7 +525,14 @@ export abstract class BaseAgent implements IAgent {
488
525
  toolCallId: toolCall.id,
489
526
  functionName: toolCall.function?.name,
490
527
  functionArgs: {},
491
- toolMessages: [{ role: "tool", tool_call_id: toolCall.id, name: toolCall.function?.name, content: interruptMsg }],
528
+ toolMessages: [
529
+ {
530
+ role: "tool",
531
+ tool_call_id: toolCall.id,
532
+ name: toolCall.function?.name,
533
+ content: interruptMsg,
534
+ },
535
+ ],
492
536
  };
493
537
 
494
538
  const { functionResp, toolMessages } = await this.makeInterruptible(
@@ -677,23 +721,25 @@ export abstract class BaseAgent implements IAgent {
677
721
  resolve(value);
678
722
  };
679
723
 
680
- promise.then((result) => {
681
- if (this._interruptResolve) {
682
- this._interruptResolve = null;
683
- resolve(result);
684
- }
685
- }).catch((err) => {
686
- if (this._interruptResolve) {
687
- this._interruptResolve = null;
688
- }
689
- // Only swallow the error if interrupt() was explicitly called.
690
- // Otherwise re-throw so callers see the real error.
691
- if (interrupted) {
692
- resolve(interruptValue);
693
- } else {
694
- reject(err);
695
- }
696
- });
724
+ promise
725
+ .then((result) => {
726
+ if (this._interruptResolve) {
727
+ this._interruptResolve = null;
728
+ resolve(result);
729
+ }
730
+ })
731
+ .catch((err) => {
732
+ if (this._interruptResolve) {
733
+ this._interruptResolve = null;
734
+ }
735
+ // Only swallow the error if interrupt() was explicitly called.
736
+ // Otherwise re-throw so callers see the real error.
737
+ if (interrupted) {
738
+ resolve(interruptValue);
739
+ } else {
740
+ reject(err);
741
+ }
742
+ });
697
743
  });
698
744
  }
699
745
 
@@ -782,20 +828,28 @@ export abstract class BaseAgent implements IAgent {
782
828
  );
783
829
 
784
830
  const interruptResponse: CompletionResponse = {
785
- choices: [{ message: { role: "assistant", content: "User interrupted this AI completion. Please continue." } }],
831
+ choices: [
832
+ {
833
+ message: {
834
+ role: "assistant",
835
+ content: "User interrupted this AI completion. Please continue.",
836
+ },
837
+ },
838
+ ],
786
839
  model,
787
840
  usage: undefined,
788
841
  usd_cost: 0,
789
842
  };
790
- const response = await this.makeInterruptible(
791
- this.getClient().createChatCompletion({
843
+
844
+ const response = await this.createAgentCompletion(
845
+ {
792
846
  model,
793
847
  messages,
794
848
  tools: this.getEnabledTools(),
795
849
  tool_choice: "auto",
796
850
  long_ttl_cache: this.runTime() > 300_000,
797
- }),
798
- interruptResponse
851
+ },
852
+ { interruptValue: interruptResponse }
799
853
  );
800
854
 
801
855
  // If the agent was paused while the completion was in-flight, wait here
@@ -829,8 +883,6 @@ export abstract class BaseAgent implements IAgent {
829
883
  }
830
884
  }
831
885
 
832
- this.adjustTotalCostUsd(response?.usd_cost);
833
- this.adjustTokenUsage(response?.usage);
834
886
  this.log("agent response cost: " + response?.usd_cost);
835
887
 
836
888
  // Typically, there's only one choice in the array, but you could have many
@@ -847,7 +899,6 @@ export abstract class BaseAgent implements IAgent {
847
899
 
848
900
  this.logMessages([lastMessage]);
849
901
 
850
- const toolCalls = lastMessage.tool_calls;
851
902
  if (lastMessage.tool_calls) {
852
903
  // About to call a tool, process the messages
853
904
  // We could add all the tool calls, and do this once
@@ -856,6 +907,11 @@ export abstract class BaseAgent implements IAgent {
856
907
  "pre_tools"
857
908
  );
858
909
 
910
+ // Re-derive toolCalls from the freshly processed messages, since
911
+ // pre_tools processors (e.g. MinimalToolsMessageProcessor) may have
912
+ // rewritten the tool_calls on a deep-copied message array.
913
+ const toolCalls = messages[messages.length - 1].tool_calls;
914
+
859
915
  this.updateCurrentThread(messages);
860
916
 
861
917
  const truncationWarning = this.detectTruncatedToolCalls(
@@ -942,7 +998,9 @@ export abstract class BaseAgent implements IAgent {
942
998
  firstMessage.content &&
943
999
  this.isTerminationResponse(firstMessage.content)
944
1000
  ) {
945
- this.log(`Termination word detected: "${firstMessage.content.trim()}", treating as finalAnswer`);
1001
+ this.log(
1002
+ `Termination word detected: "${firstMessage.content.trim()}", treating as finalAnswer`
1003
+ );
946
1004
  this.status = this.eventTypes.done;
947
1005
  this.agentEvents.emit(this.eventTypes.done, firstMessage.content);
948
1006
  return firstMessage.content;
@@ -1001,7 +1059,9 @@ export abstract class BaseAgent implements IAgent {
1001
1059
  this.logStatus();
1002
1060
 
1003
1061
  const continuation = `<Workflow>
1004
- Task terminates after you call on of these tools: ${JSON.stringify(this.requiredToolNames)}.\n
1062
+ Task terminates after you call on of these tools: ${JSON.stringify(
1063
+ this.requiredToolNames
1064
+ )}.\n
1005
1065
  User likely only sees output from the required tool call.
1006
1066
  <TaskStatus>${statusMessage}</TaskStatus>
1007
1067
  </Workflow>`;
@@ -1048,7 +1108,12 @@ export abstract class BaseAgent implements IAgent {
1048
1108
 
1049
1109
  this.log(`Agent failed: ${e}`, "error");
1050
1110
 
1051
- if (e != null && typeof e === "object" && "response" in e && "data" in (e as any).response) {
1111
+ if (
1112
+ e != null &&
1113
+ typeof e === "object" &&
1114
+ "response" in e &&
1115
+ "data" in (e as any).response
1116
+ ) {
1052
1117
  this.log(
1053
1118
  `Error response data: ${JSON.stringify(e.response.data, null, 2)}`,
1054
1119
  "error"
@@ -1223,20 +1288,20 @@ export abstract class BaseAgent implements IAgent {
1223
1288
 
1224
1289
  const model = this.getModel();
1225
1290
 
1226
- const response = await this.getClient().createChatCompletion({
1291
+ const taskBreakdownMessages = [
1292
+ ...messages,
1293
+ {
1294
+ role: "user",
1295
+ content: taskPrompt,
1296
+ },
1297
+ ] as Message[];
1298
+
1299
+ const response = await this.createAgentCompletion({
1227
1300
  model,
1228
- messages: [
1229
- ...messages,
1230
- {
1231
- role: "user",
1232
- content: taskPrompt,
1233
- },
1234
- ],
1301
+ messages: taskBreakdownMessages,
1235
1302
  max_tokens: 2000,
1236
1303
  });
1237
1304
 
1238
- this.adjustTotalCostUsd(response.usd_cost);
1239
-
1240
1305
  this.taskBreakdown = response.choices[0].message.content;
1241
1306
  this.log(`task breakdown cost: ${response.usd_cost}`);
1242
1307
  return this.taskBreakdown;
@@ -1268,19 +1333,19 @@ export abstract class BaseAgent implements IAgent {
1268
1333
 
1269
1334
  const model = this.getModel();
1270
1335
 
1271
- const response = await this.getClient().createChatCompletion({
1336
+ const compressMessagesPayload = [
1337
+ ...messages,
1338
+ {
1339
+ role: "user",
1340
+ content: toCompressPrompt,
1341
+ },
1342
+ ] as Message[];
1343
+
1344
+ const response = await this.createAgentCompletion({
1272
1345
  model,
1273
- messages: [
1274
- ...messages,
1275
- {
1276
- role: "user",
1277
- content: toCompressPrompt,
1278
- },
1279
- ],
1346
+ messages: compressMessagesPayload,
1280
1347
  });
1281
1348
 
1282
- this.adjustTotalCostUsd(response.usd_cost);
1283
-
1284
1349
  const summaries = response.choices.map((c) => c.message.content);
1285
1350
  this.summaries.push(...summaries);
1286
1351