@ngockhoale/ukit 3.4.45 → 3.4.46
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/CHANGELOG.md +9 -0
- package/manifests/documentation.yaml +12 -0
- package/package.json +3 -1
- package/src/core/executionContracts.js +21 -7
- package/template_project/.claude/hooks/skill-router.sh +23 -9
- package/template_project/.claude/ukit/index/route-resolver.mjs +21 -7
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +1 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +56 -1
- package/template_project/.omp/hooks/pre/ukit-bridge.js +1 -1
- package/scripts/audit/decision-coverage.mjs +0 -322
- package/scripts/audit/subagent-cost/analyze.mjs +0 -107
- package/scripts/audit/subagent-cost/check-doc.mjs +0 -91
- package/scripts/audit/subagent-cost/duplicates.mjs +0 -195
- package/scripts/audit/subagent-cost/metrics.mjs +0 -193
- package/scripts/audit/subagent-cost/reader.mjs +0 -332
- package/scripts/audit/subagent-cost/render.mjs +0 -34
- package/scripts/audit/subagent-cost/usage-fields.mjs +0 -82
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import readline from 'node:readline';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
|
-
import { classifyCacheRead, classifyCacheWrite, cacheMetadataStatus, extractIdentity } from './usage-fields.mjs';
|
|
6
|
-
|
|
7
|
-
const sha1 = (value) => createHash('sha1').update(value).digest('hex').slice(0, 12);
|
|
8
|
-
const timestampMs = (value) => {
|
|
9
|
-
const result = typeof value === 'number' ? value : Date.parse(value);
|
|
10
|
-
return Number.isFinite(result) ? result : null;
|
|
11
|
-
};
|
|
12
|
-
const estimateTokens = (text) => (text ? Math.ceil(text.length / 4) : 0);
|
|
13
|
-
const contentText = (content) => {
|
|
14
|
-
if (typeof content === 'string') return content;
|
|
15
|
-
if (!Array.isArray(content)) return '';
|
|
16
|
-
return content.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
17
|
-
.map((part) => part.text).join('\n');
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
async function readJsonl(file, stats, consume) {
|
|
21
|
-
const stream = fs.createReadStream(file, { encoding: 'utf8' });
|
|
22
|
-
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
23
|
-
try {
|
|
24
|
-
for await (const line of lines) {
|
|
25
|
-
stats.lines += 1;
|
|
26
|
-
if (!line.trim()) continue;
|
|
27
|
-
let record;
|
|
28
|
-
try {
|
|
29
|
-
record = JSON.parse(line);
|
|
30
|
-
} catch {
|
|
31
|
-
stats.malformed += 1;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
consume(record);
|
|
35
|
-
}
|
|
36
|
-
} finally {
|
|
37
|
-
lines.close();
|
|
38
|
-
stream.destroy();
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function transcriptFiles(root) {
|
|
43
|
-
const files = [];
|
|
44
|
-
const rootEntries = fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
-
for (const entry of rootEntries) {
|
|
46
|
-
const entryPath = path.join(root, entry.name);
|
|
47
|
-
if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
48
|
-
files.push({ file: entryPath, sessionId: entry.name.slice(0, -'.jsonl'.length), isAgent: false });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
for (const entry of rootEntries) {
|
|
52
|
-
if (!entry.isDirectory()) continue;
|
|
53
|
-
const sessionDir = path.join(root, entry.name);
|
|
54
|
-
const subagentsDir = path.join(sessionDir, 'subagents');
|
|
55
|
-
let subagentEntries;
|
|
56
|
-
try {
|
|
57
|
-
subagentEntries = fs.readdirSync(subagentsDir, { withFileTypes: true });
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') continue;
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
for (const agentEntry of subagentEntries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
63
|
-
if (!agentEntry.isFile() || !agentEntry.name.endsWith('.jsonl')) continue;
|
|
64
|
-
const stem = agentEntry.name.slice(0, -'.jsonl'.length);
|
|
65
|
-
files.push({
|
|
66
|
-
file: path.join(subagentsDir, agentEntry.name),
|
|
67
|
-
metaFile: path.join(subagentsDir, `${stem}.meta.json`),
|
|
68
|
-
sessionId: entry.name,
|
|
69
|
-
agentId: stem.replace(/^agent-/, ''),
|
|
70
|
-
isAgent: true,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return files;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function readMeta(file) {
|
|
78
|
-
try {
|
|
79
|
-
const value = JSON.parse(await fs.promises.readFile(file, 'utf8'));
|
|
80
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
81
|
-
} catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function makeCall(record, sessionId, agentId, agentType, depth, ts, priorTs, meta = null) {
|
|
87
|
-
const message = record.message ?? {};
|
|
88
|
-
const usage = message.usage ?? {};
|
|
89
|
-
const input = Number(usage.input_tokens) || 0;
|
|
90
|
-
const readMetric = classifyCacheRead(usage);
|
|
91
|
-
const writeMetric = classifyCacheWrite(usage);
|
|
92
|
-
const cacheRead = readMetric.value;
|
|
93
|
-
const cacheWrite = writeMetric.value;
|
|
94
|
-
const identity = extractIdentity(record, meta);
|
|
95
|
-
const output = Number(usage.output_tokens) || 0;
|
|
96
|
-
const key = message.id ?? record.requestId ?? record.uuid;
|
|
97
|
-
return {
|
|
98
|
-
id: key == null ? null : String(key),
|
|
99
|
-
sessionId,
|
|
100
|
-
turnId: null,
|
|
101
|
-
agentId: agentId ?? null,
|
|
102
|
-
agentType: agentType ?? null,
|
|
103
|
-
model: message.model ?? record.model ?? null,
|
|
104
|
-
ts,
|
|
105
|
-
latencyMs: ts != null && priorTs != null && ts >= priorTs ? ts - priorTs : null,
|
|
106
|
-
input,
|
|
107
|
-
cacheRead,
|
|
108
|
-
cacheWrite,
|
|
109
|
-
cacheReadStatus: readMetric.status,
|
|
110
|
-
cacheWriteStatus: writeMetric.status,
|
|
111
|
-
cacheMetadataStatus: cacheMetadataStatus(readMetric, writeMetric),
|
|
112
|
-
providerId: identity.providerId,
|
|
113
|
-
gatewayId: identity.gatewayId,
|
|
114
|
-
taskId: identity.taskId,
|
|
115
|
-
spawnPrefixHash: identity.spawnPrefixHash,
|
|
116
|
-
output,
|
|
117
|
-
zero: input === 0 && cacheRead === 0 && cacheWrite === 0 && output === 0,
|
|
118
|
-
depth,
|
|
119
|
-
_dedupeKey: key == null ? null : String(key),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function hashFinalReferences(finalText, parentText) {
|
|
124
|
-
if (!finalText || !parentText) return null;
|
|
125
|
-
const words = (text) => text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
126
|
-
const finalWords = words(finalText);
|
|
127
|
-
const parentWords = words(parentText);
|
|
128
|
-
if (finalWords.length < 5 || parentWords.length < 5) return null;
|
|
129
|
-
const grams = new Set();
|
|
130
|
-
for (let i = 0; i <= parentWords.length - 5; i += 1) {
|
|
131
|
-
grams.add(sha1(parentWords.slice(i, i + 5).join(' ')));
|
|
132
|
-
}
|
|
133
|
-
let matches = 0;
|
|
134
|
-
let total = 0;
|
|
135
|
-
for (let i = 0; i <= finalWords.length - 5; i += 1) {
|
|
136
|
-
total += 1;
|
|
137
|
-
if (grams.has(sha1(finalWords.slice(i, i + 5).join(' ')))) matches += 1;
|
|
138
|
-
}
|
|
139
|
-
return total ? matches / total : null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function loadCorpus(root, { since = null, until = null } = {}) {
|
|
143
|
-
let rootStat;
|
|
144
|
-
try {
|
|
145
|
-
rootStat = fs.statSync(root);
|
|
146
|
-
} catch {
|
|
147
|
-
throw new Error(`root not found: ${root}`);
|
|
148
|
-
}
|
|
149
|
-
if (!rootStat.isDirectory()) throw new Error(`root not found: ${root}`);
|
|
150
|
-
|
|
151
|
-
const stats = {
|
|
152
|
-
files: 0, lines: 0, malformed: 0, zeroUsage: 0, dupIds: 0,
|
|
153
|
-
missingMeta: 0, noParent: 0, unattributed: 0,
|
|
154
|
-
};
|
|
155
|
-
const files = transcriptFiles(root);
|
|
156
|
-
const mainFilesBySession = new Map();
|
|
157
|
-
const agentFiles = [];
|
|
158
|
-
const promptRecordsBySession = new Map();
|
|
159
|
-
const toolUsesBySession = new Map();
|
|
160
|
-
const calls = [];
|
|
161
|
-
const agents = [];
|
|
162
|
-
const turns = [];
|
|
163
|
-
const sinceMs = since == null ? null : timestampMs(since);
|
|
164
|
-
const untilMs = until == null ? null : timestampMs(until);
|
|
165
|
-
|
|
166
|
-
for (const fileInfo of files) {
|
|
167
|
-
stats.files += 1;
|
|
168
|
-
const meta = fileInfo.isAgent ? await readMeta(fileInfo.metaFile) : null;
|
|
169
|
-
if (fileInfo.isAgent && !meta) stats.missingMeta += 1;
|
|
170
|
-
const agentType = fileInfo.isAgent ? (meta?.agentType ?? null) : null;
|
|
171
|
-
const depth = fileInfo.isAgent
|
|
172
|
-
? (Number.isFinite(meta?.spawnDepth) ? meta.spawnDepth : 1)
|
|
173
|
-
: 0;
|
|
174
|
-
const deduped = new Map();
|
|
175
|
-
const userRecords = [];
|
|
176
|
-
let previousTs = null;
|
|
177
|
-
let finalAssistantText = null;
|
|
178
|
-
|
|
179
|
-
await readJsonl(fileInfo.file, stats, (record) => {
|
|
180
|
-
const ts = timestampMs(record.timestamp);
|
|
181
|
-
if (!fileInfo.isAgent && record.type === 'user' && !record.isSidechain) {
|
|
182
|
-
const body = contentText(record.message?.content ?? record.content);
|
|
183
|
-
if (body.trim()) userRecords.push({ ts, body });
|
|
184
|
-
}
|
|
185
|
-
if (record.type !== 'assistant') return;
|
|
186
|
-
|
|
187
|
-
const call = makeCall(
|
|
188
|
-
record,
|
|
189
|
-
fileInfo.sessionId,
|
|
190
|
-
fileInfo.isAgent ? fileInfo.agentId : null,
|
|
191
|
-
fileInfo.isAgent ? (agentType ?? record.attributionAgent ?? 'unknown') : null,
|
|
192
|
-
depth,
|
|
193
|
-
ts,
|
|
194
|
-
previousTs,
|
|
195
|
-
meta,
|
|
196
|
-
);
|
|
197
|
-
previousTs = ts;
|
|
198
|
-
const key = call._dedupeKey ?? `line:${stats.lines}`;
|
|
199
|
-
if (deduped.has(key) && call._dedupeKey != null) stats.dupIds += 1;
|
|
200
|
-
deduped.set(key, { call, record });
|
|
201
|
-
|
|
202
|
-
if (fileInfo.isAgent) {
|
|
203
|
-
const text = contentText(record.message?.content);
|
|
204
|
-
if (text) finalAssistantText = text;
|
|
205
|
-
} else {
|
|
206
|
-
for (const part of record.message?.content ?? []) {
|
|
207
|
-
if (part?.type !== 'tool_use' || typeof part.id !== 'string') continue;
|
|
208
|
-
if (!toolUsesBySession.has(fileInfo.sessionId)) toolUsesBySession.set(fileInfo.sessionId, new Map());
|
|
209
|
-
toolUsesBySession.get(fileInfo.sessionId).set(part.id, { ts, record, tool: part });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const records = [...deduped.values()];
|
|
215
|
-
if (fileInfo.isAgent) {
|
|
216
|
-
agentFiles.push({ fileInfo, meta, agentType, depth, records, finalAssistantText });
|
|
217
|
-
} else {
|
|
218
|
-
mainFilesBySession.set(fileInfo.sessionId, records);
|
|
219
|
-
promptRecordsBySession.set(fileInfo.sessionId, userRecords);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const turnsBySession = new Map();
|
|
224
|
-
for (const [sessionId, prompts] of promptRecordsBySession) {
|
|
225
|
-
prompts.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
226
|
-
const sessionTurns = prompts.map((prompt, index) => ({
|
|
227
|
-
turnId: `${sessionId}#${index + 1}`,
|
|
228
|
-
sessionId,
|
|
229
|
-
ts: prompt.ts,
|
|
230
|
-
promptHash: sha1(prompt.body),
|
|
231
|
-
promptTokensEst: estimateTokens(prompt.body),
|
|
232
|
-
}));
|
|
233
|
-
turnsBySession.set(sessionId, sessionTurns);
|
|
234
|
-
turns.push(...sessionTurns);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function turnForTime(sessionId, ts) {
|
|
238
|
-
const sessionTurns = turnsBySession.get(sessionId) ?? [];
|
|
239
|
-
let selected = null;
|
|
240
|
-
for (const turn of sessionTurns) {
|
|
241
|
-
if (ts == null || turn.ts == null || turn.ts <= ts) selected = turn;
|
|
242
|
-
else break;
|
|
243
|
-
}
|
|
244
|
-
return selected;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const parentTextBySessionTurn = new Map();
|
|
248
|
-
for (const [sessionId, records] of mainFilesBySession) {
|
|
249
|
-
let latestTurn = null;
|
|
250
|
-
const assistantTextsByTurn = new Map();
|
|
251
|
-
for (const { call, record } of records) {
|
|
252
|
-
const turn = turnForTime(sessionId, call.ts);
|
|
253
|
-
if (turn) latestTurn = turn;
|
|
254
|
-
call.turnId = latestTurn?.turnId ?? `${sessionId}#unattributed`;
|
|
255
|
-
if (record.message?.content) {
|
|
256
|
-
const text = contentText(record.message.content);
|
|
257
|
-
if (text) assistantTextsByTurn.set(call.turnId, text);
|
|
258
|
-
}
|
|
259
|
-
calls.push(call);
|
|
260
|
-
}
|
|
261
|
-
for (const [turnId, text] of assistantTextsByTurn) {
|
|
262
|
-
parentTextBySessionTurn.set(`${sessionId}\u0000${turnId}`, text);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
agentFiles.sort((a, b) => {
|
|
267
|
-
const aTs = a.records[0]?.call.ts ?? 0;
|
|
268
|
-
const bTs = b.records[0]?.call.ts ?? 0;
|
|
269
|
-
return aTs - bTs || a.fileInfo.agentId.localeCompare(b.fileInfo.agentId);
|
|
270
|
-
});
|
|
271
|
-
const agentsById = new Map();
|
|
272
|
-
for (const { fileInfo, meta, agentType, depth, records, finalAssistantText } of agentFiles) {
|
|
273
|
-
const toolUseId = meta?.toolUseId ?? null;
|
|
274
|
-
const parent = toolUseId == null ? null : toolUsesBySession.get(fileInfo.sessionId)?.get(toolUseId) ?? null;
|
|
275
|
-
if (!parent) stats.noParent += 1;
|
|
276
|
-
const turn = parent ? turnForTime(fileInfo.sessionId, parent.ts) : null;
|
|
277
|
-
const turnId = turn?.turnId ?? `${fileInfo.sessionId}#unattributed`;
|
|
278
|
-
if (!turn) stats.unattributed += 1;
|
|
279
|
-
|
|
280
|
-
const delegatedPrompt = typeof parent?.tool?.input?.prompt === 'string' ? parent.tool.input.prompt : '';
|
|
281
|
-
const description = typeof meta?.description === 'string' ? meta.description : '';
|
|
282
|
-
const segments = delegatedPrompt.split(/\n\s*\n/)
|
|
283
|
-
.map((segment) => segment.trim())
|
|
284
|
-
.filter((segment) => segment.length >= 200)
|
|
285
|
-
.map((segment) => ({ h: sha1(segment), t: estimateTokens(segment) }));
|
|
286
|
-
const firstTs = records.reduce((value, item) => item.call.ts == null ? value : value == null ? item.call.ts : Math.min(value, item.call.ts), null);
|
|
287
|
-
const lastTs = records.reduce((value, item) => item.call.ts == null ? value : value == null ? item.call.ts : Math.max(value, item.call.ts), null);
|
|
288
|
-
const parentText = parentTextBySessionTurn.get(`${fileInfo.sessionId}\u0000${turnId}`) ?? '';
|
|
289
|
-
const agent = {
|
|
290
|
-
agentId: fileInfo.agentId,
|
|
291
|
-
sessionId: fileInfo.sessionId,
|
|
292
|
-
turnId,
|
|
293
|
-
agentType: agentType ?? records.find((item) => item.record.attributionAgent)?.record.attributionAgent ?? 'unknown',
|
|
294
|
-
descriptionHash: description ? sha1(description) : null,
|
|
295
|
-
toolUseId,
|
|
296
|
-
depth,
|
|
297
|
-
parentAgentId: meta?.parentAgentId ?? null,
|
|
298
|
-
metaModel: meta?.model ?? null,
|
|
299
|
-
hasMeta: Boolean(meta),
|
|
300
|
-
promptHash: delegatedPrompt ? sha1(delegatedPrompt) : null,
|
|
301
|
-
promptTokensEst: delegatedPrompt ? estimateTokens(delegatedPrompt) : null,
|
|
302
|
-
segments,
|
|
303
|
-
finalTokensEst: finalAssistantText ? estimateTokens(finalAssistantText) : null,
|
|
304
|
-
finalRefRatio: hashFinalReferences(finalAssistantText, parentText),
|
|
305
|
-
firstTs,
|
|
306
|
-
lastTs,
|
|
307
|
-
};
|
|
308
|
-
agents.push(agent);
|
|
309
|
-
agentsById.set(`${fileInfo.sessionId}\u0000${fileInfo.agentId}`, agent);
|
|
310
|
-
for (const { call } of records) {
|
|
311
|
-
call.turnId = turnId;
|
|
312
|
-
calls.push(call);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Resolve explicit parent-agent links when the parent transcript is present; retain null for unknown links.
|
|
317
|
-
for (const agent of agents) {
|
|
318
|
-
if (agent.parentAgentId != null) {
|
|
319
|
-
const parent = agentsById.get(`${agent.sessionId}\u0000${agent.parentAgentId}`);
|
|
320
|
-
if (parent && agent.turnId.endsWith('#unattributed')) agent.turnId = parent.turnId;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
stats.zeroUsage = calls.reduce((count, call) => count + (call.zero ? 1 : 0), 0);
|
|
325
|
-
const filteredCalls = calls.filter((call) =>
|
|
326
|
-
(sinceMs == null || (call.ts != null && call.ts >= sinceMs)) &&
|
|
327
|
-
(untilMs == null || (call.ts != null && call.ts <= untilMs)));
|
|
328
|
-
for (const call of filteredCalls) {
|
|
329
|
-
delete call._dedupeKey;
|
|
330
|
-
}
|
|
331
|
-
return { calls: filteredCalls, turns, agents, stats };
|
|
332
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const fmt = (value) => value == null || !Number.isFinite(value) ? 'N/A' : Number(value).toLocaleString('en-US', { maximumFractionDigits: 3 });
|
|
2
|
-
const table = (headers, rows) => [`| ${headers.join(' | ')} |`, `| ${headers.map(() => '---').join(' | ')} |`, ...rows.map((row) => `| ${row.map((cell) => String(cell ?? 'N/A').replaceAll('|', '\\|').replaceAll('\n', ' ')).join(' | ')} |`)].join('\n');
|
|
3
|
-
|
|
4
|
-
export function renderMarkdown(report) {
|
|
5
|
-
const m = report.metrics ?? {};
|
|
6
|
-
const d = report.duplicates ?? {};
|
|
7
|
-
const totals = m.totals ?? {};
|
|
8
|
-
const typeRows = Object.entries(m.byType ?? {}).sort(([a], [b]) => a.localeCompare(b))
|
|
9
|
-
.map(([type, value]) => [type, value.agents, value.calls, fmt(value.inputSide), fmt(value.uncached), fmt(value.output), fmt(value.latencyP50)]);
|
|
10
|
-
const topRows = (items, key) => (items ?? []).map((item) => [item[key], item.agentType ?? 'N/A', item.calls, fmt(item.uncached), fmt(item.inputSide), fmt(item.output)]);
|
|
11
|
-
const flowRows = (m.tokenFlow ?? []).flatMap((turn) => turn.calls.map((call) => [turn.turnId, new Date(call.ts).toISOString(), call.agentType ?? 'main', call.depth, fmt(call.input), fmt(call.cacheRead), fmt(call.cacheWrite), fmt(call.output)]));
|
|
12
|
-
const flags = (value) => (value?.turnIds ?? []).map((turnId) => [turnId]);
|
|
13
|
-
return [
|
|
14
|
-
'# Sub-agent cost audit report',
|
|
15
|
-
'',
|
|
16
|
-
`Generated: ${report.generatedAt}`, `Root: ${report.root}`, `Window: ${report.window?.since ?? 'all'} — ${report.window?.until ?? 'all'}`,
|
|
17
|
-
'', '## Totals', '',
|
|
18
|
-
table(['Group', 'Calls', 'Input', 'Cache read', 'Cache write', 'Output', 'Uncached', 'Input-side'], ['main', 'sub'].map((key) => { const t = totals[key] ?? {}; return [key, t.calls, fmt(t.input), fmt(t.cacheRead), fmt(t.cacheWrite), fmt(t.output), fmt(t.uncached), fmt(t.inputSide)]; })),
|
|
19
|
-
'', '## Main vs sub-agent', '', table(['Metric', 'Value'], Object.entries(m.share ?? {}).map(([key, value]) => [key, fmt(value)])),
|
|
20
|
-
'', '## By type', '', table(['Type', 'Agents', 'Calls', 'Input-side', 'Uncached', 'Output', 'Latency p50 ms'], typeRows),
|
|
21
|
-
'', '## Fan-out', '', table(['Metric', 'Value'], [['Maximum', m.fanout?.max], ['p90', fmt(m.fanout?.p90)], ...Object.entries(m.fanout?.histogram ?? {}).map(([count, turns]) => [`${count} agents`, turns])]),
|
|
22
|
-
'', '## Cache', '', table(['Group', 'Hit rate', 'Cache read', 'Input-side'], ['main', 'sub'].map((key) => { const c = m.cache?.[key] ?? {}; return [key, fmt(c.hitRate), fmt(c.cacheRead), fmt(c.inputSide)]; })),
|
|
23
|
-
'', '## Top agents (20)', '', table(['Agent', 'Type', 'Calls', 'Uncached', 'Input-side', 'Output'], topRows(m.topAgents, 'agentId')),
|
|
24
|
-
'', '## Top turns (20)', '', table(['Turn', 'Type', 'Calls', 'Uncached', 'Input-side', 'Output'], topRows(m.topTurns, 'turnId')),
|
|
25
|
-
'', '## Token flow (5 costliest turns)', '', table(['Turn', 'Timestamp', 'Type', 'Depth', 'Input', 'Cache read', 'Cache write', 'Output'], flowRows),
|
|
26
|
-
'', '## N+1 flags', '', table(['Turn'], flags(m.nPlusOne)),
|
|
27
|
-
'', '## Retry flags', '', table(['Turn'], flags(m.retryLoops)),
|
|
28
|
-
'', '## Repeated-loop flags', '', table(['Turn'], flags(m.loops)),
|
|
29
|
-
'', '## Heavy calls and replay hypothesis', '', table(['Metric', 'Value'], [['Heavy calls', m.heavyCalls?.count], ['Heavy share', fmt(m.heavyCalls?.share)], ['Replay verdict', d.replayHypothesis?.verdict], ['Replay share', fmt(d.replayHypothesis?.share)]]),
|
|
30
|
-
'', '## Context duplication', '', table(['Metric', 'Value'], [['Amplification', fmt(d.amplification)], ['Duplicate context ratio', fmt(d.duplicateRatio)], ['Within-turn ratio', fmt(d.withinTurn?.ratio)], ['Cross-turn ratio', fmt(d.crossTurn?.ratio)]]),
|
|
31
|
-
'', '## Missing telemetry', '', ...(report.missing ?? []).map((item) => `- ${item}`),
|
|
32
|
-
'',
|
|
33
|
-
].join('\n');
|
|
34
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// MB-1 (C100) — telemetry correctness helpers shared by the sub-agent cost reader.
|
|
2
|
-
// Pure, local, no LLM calls. Rules: a missing/invalid usage field is NEVER a measured zero;
|
|
3
|
-
// an explicit 0 stays 0; provider/gateway/task/prefix identity is read only from explicit
|
|
4
|
-
// allowlisted keys (the model name is never a signal) and unknown stays null.
|
|
5
|
-
|
|
6
|
-
export const CACHE_STATUS = Object.freeze({
|
|
7
|
-
PRESENT_ZERO: 'present_zero',
|
|
8
|
-
PRESENT_POSITIVE: 'present_positive',
|
|
9
|
-
ABSENT: 'absent',
|
|
10
|
-
INVALID: 'invalid',
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const READ_KEYS = ['cache_read_input_tokens', 'cache_read_tokens'];
|
|
14
|
-
const WRITE_KEYS = ['cache_creation_input_tokens', 'cache_creation_tokens'];
|
|
15
|
-
const PROVIDER_KEYS = ['providerId', 'provider_id', 'provider', 'apiProvider'];
|
|
16
|
-
const GATEWAY_KEYS = ['gatewayId', 'gateway_id', 'gatewayName', 'gateway'];
|
|
17
|
-
const MAX_ID_LENGTH = 128;
|
|
18
|
-
const SAFE_ID = /^[A-Za-z0-9._:/@-]{1,128}$/;
|
|
19
|
-
const SECRET_LIKE = /sk-[A-Za-z0-9]{8,}|bearer|secret|passw|token=|key=|api[_-]?key/i;
|
|
20
|
-
|
|
21
|
-
function numericOrNull(raw) {
|
|
22
|
-
if (typeof raw === 'number') return Number.isFinite(raw) ? raw : null;
|
|
23
|
-
if (typeof raw === 'string' && raw.trim() !== '') {
|
|
24
|
-
const value = Number(raw);
|
|
25
|
-
return Number.isFinite(value) ? value : null;
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Classify one cache metric. `value` is only the measured number; 0 for absent/invalid is a
|
|
31
|
-
* summing convenience — consumers must read `status`, never infer a miss from value 0. */
|
|
32
|
-
export function classifyCacheMetric(usage, keys) {
|
|
33
|
-
if (!usage || typeof usage !== 'object') return { status: CACHE_STATUS.ABSENT, value: 0 };
|
|
34
|
-
for (const key of keys) {
|
|
35
|
-
const raw = usage[key];
|
|
36
|
-
if (raw === undefined || raw === null) continue;
|
|
37
|
-
const value = numericOrNull(raw);
|
|
38
|
-
if (value === null || value < 0) return { status: CACHE_STATUS.INVALID, value: 0 };
|
|
39
|
-
return { status: value === 0 ? CACHE_STATUS.PRESENT_ZERO : CACHE_STATUS.PRESENT_POSITIVE, value };
|
|
40
|
-
}
|
|
41
|
-
return { status: CACHE_STATUS.ABSENT, value: 0 };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const classifyCacheRead = (usage) => classifyCacheMetric(usage, READ_KEYS);
|
|
45
|
-
export const classifyCacheWrite = (usage) => classifyCacheMetric(usage, WRITE_KEYS);
|
|
46
|
-
|
|
47
|
-
/** Combined status for a call: a known read status wins; otherwise the worse of the pair. */
|
|
48
|
-
export function cacheMetadataStatus(read, write) {
|
|
49
|
-
if (read.status === CACHE_STATUS.INVALID || write.status === CACHE_STATUS.INVALID) return CACHE_STATUS.INVALID;
|
|
50
|
-
if (read.status === CACHE_STATUS.ABSENT) return CACHE_STATUS.ABSENT;
|
|
51
|
-
return read.status;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function safeId(raw) {
|
|
55
|
-
if (typeof raw !== 'string') return null;
|
|
56
|
-
const value = raw.trim();
|
|
57
|
-
if (value.length === 0 || value.length > MAX_ID_LENGTH) return null;
|
|
58
|
-
if (!SAFE_ID.test(value) || SECRET_LIKE.test(value)) return null;
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function firstId(sources, keys) {
|
|
63
|
-
for (const source of sources) {
|
|
64
|
-
if (!source || typeof source !== 'object') continue;
|
|
65
|
-
for (const key of keys) {
|
|
66
|
-
const id = safeId(source[key]);
|
|
67
|
-
if (id) return id;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Identity metadata from explicit keys only; null means UNKNOWN (never a guessed value). */
|
|
74
|
-
export function extractIdentity(record, meta = null) {
|
|
75
|
-
const sources = [record, record?.message, meta];
|
|
76
|
-
return {
|
|
77
|
-
providerId: firstId(sources, PROVIDER_KEYS),
|
|
78
|
-
gatewayId: firstId(sources, GATEWAY_KEYS),
|
|
79
|
-
taskId: firstId(sources, ['taskId', 'task_id']),
|
|
80
|
-
spawnPrefixHash: firstId(sources, ['spawnPrefixHash', 'spawn_prefix_hash']),
|
|
81
|
-
};
|
|
82
|
-
}
|