@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
package/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",
|
|
@@ -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();
|
package/src/agents/base/base.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 ??
|
|
429
|
-
usage.
|
|
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: [
|
|
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
|
|
681
|
-
|
|
682
|
-
this._interruptResolve
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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: [
|
|
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
|
-
|
|
791
|
-
|
|
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(
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|