@migor/cli 0.10.2 → 0.10.3
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 +552 -36
- package/dist/AGENTS.md +95 -25
- package/dist/export.js +2 -2
- package/dist/harvest.js +4 -2
- package/dist/inspector/summary.js +52 -5
- package/dist/report.js +141 -5
- package/dist/templates/package.json +2 -2
- package/dist/templates/tsconfig.json +2 -0
- package/dist/templates.js +1 -1
- package/package.json +1 -1
package/dist/report.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* - 裸 Trace(含 `spans`)—— 如 `createOtlpExporter` 之外的本地落盘、或自己 writeFile 的记录;
|
|
9
9
|
* - TaskRecord(含 `result.trace` 或 `trace`)—— 如 `FileTaskStore` / `SqliteTaskStore` 的导出。
|
|
10
10
|
*
|
|
11
|
+
* ⚠️ TaskRecord 导出是 **append-only**(FileTaskStore 每次 save 追加一行):审批流里同一棵
|
|
12
|
+
* 挂起段 trace 会被写多次(挂起 save / approve 落库 / 接管再 save,各行的 `result.trace`
|
|
13
|
+
* 是同一份)⇒ 按行计会把 runs / 成功率 / 成本全部翻倍。因此解析后按 **`traceId` 去重,
|
|
14
|
+
* 同一 traceId 只留最后一行**(append-only 里后面的是新状态),能力表与会话表共用这份输入。
|
|
15
|
+
*
|
|
11
16
|
* 聚合实现**不在这里** —— 复用 `@migor/trace-view` 的 `summarizeTrace`
|
|
12
17
|
* (构建期由 scripts/copy-assets.mjs 拷进 dist/inspector/)。CLI 因此保持零 npm 运行时依赖,
|
|
13
18
|
* 且与 inspector 面板/官网 playground 共用同一份聚合口径(避免两处漂移)。
|
|
@@ -29,6 +34,51 @@ export function extractTrace(v) {
|
|
|
29
34
|
return trace;
|
|
30
35
|
return null;
|
|
31
36
|
}
|
|
37
|
+
/** TaskRecord 的任务键(`FileTaskStore` / `SqliteTaskStore` 导出的顶层 `taskId`)。裸 Trace 没有它。 */
|
|
38
|
+
function taskKeyOf(v) {
|
|
39
|
+
if (!v || typeof v !== 'object')
|
|
40
|
+
return null;
|
|
41
|
+
const id = v.taskId;
|
|
42
|
+
return typeof id === 'string' && id.length > 0 ? id : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* run 根 span —— 优先 `kind === 'run'`,跑挂的 trace 没有就退到第一个 span。
|
|
46
|
+
* 与 `inspector-routes.ts` 的 `summarize` 同款取法(两处都在读 run 根的 attribute,口径要一致)。
|
|
47
|
+
*/
|
|
48
|
+
function rootSpanOf(t) {
|
|
49
|
+
const spans = (t.spans ?? []);
|
|
50
|
+
return spans.find((s) => s?.kind === 'run') ?? spans[0] ?? null;
|
|
51
|
+
}
|
|
52
|
+
/** 「没有会话」桶的名字 —— 常量而非字面量:断言与渲染必须指同一个东西 */
|
|
53
|
+
export const NO_SESSION = '(无会话)';
|
|
54
|
+
/** 分组键:会话优先,其次任务,最后落「无会话」桶 */
|
|
55
|
+
function groupKeyOf(t, taskKey) {
|
|
56
|
+
const attrs = rootSpanOf(t)?.attributes;
|
|
57
|
+
const sid = attrs?.['session.id'];
|
|
58
|
+
if (typeof sid === 'string' && sid.length > 0)
|
|
59
|
+
return sid;
|
|
60
|
+
return taskKey ?? NO_SESSION;
|
|
61
|
+
}
|
|
62
|
+
/** 单条 run 的根 span 耗时(`endedAt - startedAt`);读不出计 0 */
|
|
63
|
+
function runMs(t) {
|
|
64
|
+
const root = rootSpanOf(t);
|
|
65
|
+
const s = Number(root?.startedAt);
|
|
66
|
+
const e = Number(root?.endedAt);
|
|
67
|
+
return Number.isFinite(s) && Number.isFinite(e) && e >= s ? e - s : 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 单条 run 的成本(美元)。
|
|
71
|
+
*
|
|
72
|
+
* ⚠️ **刻意不从 `summarizeTrace` 的能力行反加**:能力行只覆盖进了 capability 的
|
|
73
|
+
* `llm.turn`,run 根直属的回合(主 agent 自己问的)不在任何能力行里 ⇒ 反加会**漏**。
|
|
74
|
+
* `Trace.totalUsage` 才是权威口径(各 `llm.turn` 之和,刻意不含 capability 聚合以免
|
|
75
|
+
* 重复计数,`src/core/trace.ts`)。读不出(手工写的 trace / 未定价模型)⇒ null
|
|
76
|
+
* (渲染成 `-`),**不伪装成 0**。
|
|
77
|
+
*/
|
|
78
|
+
function runCostUsd(t) {
|
|
79
|
+
const c = t.totalUsage?.costEstimate;
|
|
80
|
+
return typeof c === 'number' && Number.isFinite(c) ? c : null;
|
|
81
|
+
}
|
|
32
82
|
/** 打印一张按总耗时降序的能力排行(跨多行记录时按能力合并) */
|
|
33
83
|
/** 用法串(cli.ts 的子命令 `--help` 也从这里取,避免两处各写一份) */
|
|
34
84
|
export const USAGE = '用法:agentia report <trace.jsonl> [--json]';
|
|
@@ -50,16 +100,21 @@ export async function reportCommand(args) {
|
|
|
50
100
|
catch (e) {
|
|
51
101
|
throw new Error(`读不到文件 ${file}(${e.message})`);
|
|
52
102
|
}
|
|
53
|
-
|
|
103
|
+
// 解析时同时留下**外层记录**的任务键(TaskRecord 的 `taskId`)—— 它与 trace 本体不同源
|
|
104
|
+
// (在 wrapper 上,不在 `spans` 里),故在解析处一并取出。
|
|
105
|
+
// ⚠️ 「一行 = 一条 run」对 append-only 导出不成立(同一棵 trace 可能占多行),
|
|
106
|
+
// 去重在解析**之后**统一做(见下),别在循环里顺手去重 —— 坏行计数会被带歪。
|
|
107
|
+
const entries = [];
|
|
54
108
|
let badLines = 0;
|
|
55
109
|
for (const line of raw.split('\n')) {
|
|
56
110
|
const s = line.trim();
|
|
57
111
|
if (!s)
|
|
58
112
|
continue;
|
|
59
113
|
try {
|
|
60
|
-
const
|
|
114
|
+
const v = JSON.parse(s);
|
|
115
|
+
const t = extractTrace(v);
|
|
61
116
|
if (t)
|
|
62
|
-
|
|
117
|
+
entries.push({ trace: t, taskKey: taskKeyOf(v) });
|
|
63
118
|
else
|
|
64
119
|
badLines += 1;
|
|
65
120
|
}
|
|
@@ -67,10 +122,30 @@ export async function reportCommand(args) {
|
|
|
67
122
|
badLines += 1;
|
|
68
123
|
}
|
|
69
124
|
}
|
|
70
|
-
if (
|
|
125
|
+
if (entries.length === 0) {
|
|
71
126
|
throw new Error(`${file} 里没有可识别的 trace(支持裸 Trace 或含 result.trace / trace 的记录)` +
|
|
72
127
|
(badLines > 0 ? `;另有 ${badLines} 行无法解析` : ''));
|
|
73
128
|
}
|
|
129
|
+
// **按 traceId 去重,同一 traceId 只留最后一行**。FileTaskStore 是 append-only:审批流里
|
|
130
|
+
// 同一棵挂起段 trace 会被写多次(挂起 save / approve 落库 / 接管再 save),各行的
|
|
131
|
+
// `result.trace` 是同一份 —— 不去重则 runs / 成功率 / 成本按行数翻倍。
|
|
132
|
+
// - 取**最后**一行:append-only 里后面的是新状态(接管后的记录才是终态);
|
|
133
|
+
// - 键是 traceId 而不是 taskId:一个任务可以跨**多段** run(挂起 → 恢复 → 再挂起),
|
|
134
|
+
// 每段一棵新 trace(traceId == runId),按 taskId 去重会把不同段错并成一条;
|
|
135
|
+
// - 无 traceId 的行(手写 trace)无从判同,全部保留;
|
|
136
|
+
// - 过滤保序:留下的是各 traceId 在文件里**最后一次出现的位置**,输出顺序与直觉一致。
|
|
137
|
+
// 能力表、runs 总计、会话表**共用**这份去重后的输入(两处各算各的必然漂移)。
|
|
138
|
+
const lastByTraceId = new Map();
|
|
139
|
+
entries.forEach((e, i) => {
|
|
140
|
+
const id = e.trace.traceId;
|
|
141
|
+
if (typeof id === 'string' && id.length > 0)
|
|
142
|
+
lastByTraceId.set(id, i);
|
|
143
|
+
});
|
|
144
|
+
const deduped = entries.filter((e, i) => {
|
|
145
|
+
const id = e.trace.traceId;
|
|
146
|
+
return typeof id !== 'string' || id.length === 0 || lastByTraceId.get(id) === i;
|
|
147
|
+
});
|
|
148
|
+
const traces = deduped.map((e) => e.trace);
|
|
74
149
|
// 聚合口径与 inspector 面板 / 官网 playground 同源(构建期拷进来的 trace-view 产物)
|
|
75
150
|
// 这份聚合实现由 packages/cli/scripts/copy-assets.mjs 在构建期从 trace-view 产物拷进来。
|
|
76
151
|
// 缺它只可能是「装坏了」或「直接跑未构建的源码」—— 原始 ERR_MODULE_NOT_FOUND 里全是
|
|
@@ -105,6 +180,42 @@ export async function reportCommand(args) {
|
|
|
105
180
|
}
|
|
106
181
|
}
|
|
107
182
|
const rows = [...merged.values()].sort((a, b) => b.totalMs - a.totalMs || b.calls - a.calls);
|
|
183
|
+
// G6:会话 / 任务维度 —— 与上面的能力表**互补**:能力表看单个能力,这里看一整段会话/任务
|
|
184
|
+
// (多轮 run 的合计 + 成功率)。成功率**必须带分母**:只报百分比在小组上是噪声。
|
|
185
|
+
const byGroup = new Map();
|
|
186
|
+
for (const e of deduped) {
|
|
187
|
+
const key = groupKeyOf(e.trace, e.taskKey);
|
|
188
|
+
let row = byGroup.get(key);
|
|
189
|
+
if (!row) {
|
|
190
|
+
row = {
|
|
191
|
+
session: key,
|
|
192
|
+
runs: 0,
|
|
193
|
+
succeeded: 0,
|
|
194
|
+
failed: 0,
|
|
195
|
+
totalMs: 0,
|
|
196
|
+
costUsd: null,
|
|
197
|
+
costPerSuccess: null,
|
|
198
|
+
};
|
|
199
|
+
byGroup.set(key, row);
|
|
200
|
+
}
|
|
201
|
+
row.runs += 1;
|
|
202
|
+
// 成败判据与上面的 `okRuns` **同一条**(`status !== 'error'`)—— 两处各写一份会漂移。
|
|
203
|
+
if (e.trace.status !== 'error')
|
|
204
|
+
row.succeeded += 1;
|
|
205
|
+
else
|
|
206
|
+
row.failed += 1;
|
|
207
|
+
row.totalMs += runMs(e.trace);
|
|
208
|
+
const c = runCostUsd(e.trace);
|
|
209
|
+
if (c != null)
|
|
210
|
+
row.costUsd = (row.costUsd ?? 0) + c;
|
|
211
|
+
}
|
|
212
|
+
for (const row of byGroup.values()) {
|
|
213
|
+
// ⚠️ 成功数为 0 ⇒ null(渲染成 `-`)。写成 0 是把「除不了」伪装成「每个成功任务不花钱」,
|
|
214
|
+
// 正好说反 —— 这正是本项要防的那笔假账。
|
|
215
|
+
row.costPerSuccess =
|
|
216
|
+
row.costUsd != null && row.succeeded > 0 ? row.costUsd / row.succeeded : null;
|
|
217
|
+
}
|
|
218
|
+
const sessions = [...byGroup.values()].sort((a, b) => b.totalMs - a.totalMs || b.runs - a.runs || a.session.localeCompare(b.session));
|
|
108
219
|
const failRows = traces
|
|
109
220
|
.filter((t) => t.status === 'error')
|
|
110
221
|
.map((t) => {
|
|
@@ -124,12 +235,25 @@ export async function reportCommand(args) {
|
|
|
124
235
|
skippedLines: badLines,
|
|
125
236
|
failures: failRows,
|
|
126
237
|
capabilities: rows,
|
|
238
|
+
sessions,
|
|
127
239
|
totals: { totalMs, errors: errTotal, capabilities: rows.length },
|
|
128
240
|
};
|
|
129
241
|
console.log(JSON.stringify(payload, null, 2));
|
|
130
242
|
return 0;
|
|
131
243
|
}
|
|
132
|
-
|
|
244
|
+
// 按**显示宽度**补齐:`(无会话)` 里的汉字占 2 列而 `String.length` 只算 1 ⇒ 用字符数补齐
|
|
245
|
+
// 会把那一行整体右推 3 格。而它偏偏是「宿主没开会话」时的**唯一**一行,歪得很显眼。
|
|
246
|
+
// 判据是启发式(≥ U+2E80 记 2 列):覆盖 CJK 与全角标点;`…`(U+2026)等仍是 1 列。
|
|
247
|
+
const dispWidth = (s) => {
|
|
248
|
+
let w = 0;
|
|
249
|
+
for (const ch of s)
|
|
250
|
+
w += (ch.codePointAt(0) ?? 0) >= 0x2e80 ? 2 : 1;
|
|
251
|
+
return w;
|
|
252
|
+
};
|
|
253
|
+
const pad = (s, n) => {
|
|
254
|
+
const w = dispWidth(s);
|
|
255
|
+
return w >= n ? s : s + ' '.repeat(n - w);
|
|
256
|
+
};
|
|
133
257
|
const fmtMs = (ms) => (ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`);
|
|
134
258
|
console.log(`trace 文件 ${file}`);
|
|
135
259
|
console.log(`runs ${traces.length}(失败 ${traces.length - okRuns})${badLines > 0 ? ` · 跳过无法解析 ${badLines} 行` : ''}`);
|
|
@@ -141,6 +265,18 @@ export async function reportCommand(args) {
|
|
|
141
265
|
if (failRows.length > 3)
|
|
142
266
|
console.log(` …另有 ${failRows.length - 3} 条失败 run`);
|
|
143
267
|
console.log('');
|
|
268
|
+
// G6 区块:**无论有没有会话都打**。没配 `session: { store, id }` 时只会有一行
|
|
269
|
+
// `(无会话)` —— 那是**如实呈现**(说明宿主没开会话),不是缺陷(见设计稿 §6)。
|
|
270
|
+
console.log(`${pad('session', 34)} ${pad('runs', 6)} ${pad('ok', 5)} ${pad('fail', 6)} ${pad('total', 9)} ${pad('cost', 12)} ${pad('cost/ok', 12)}`);
|
|
271
|
+
for (const r of sessions) {
|
|
272
|
+
console.log(`${pad(r.session, 34)} ${pad(String(r.runs), 6)} ${pad(String(r.succeeded), 5)} ` +
|
|
273
|
+
`${pad(String(r.failed), 6)} ${pad(fmtMs(r.totalMs), 9)} ` +
|
|
274
|
+
`${pad(r.costUsd != null ? r.costUsd.toFixed(6) : '-', 12)} ` +
|
|
275
|
+
// `costPerSuccess` 为 null 有两种来路(没成本 / 没成功),两者都只能给 `-` ——
|
|
276
|
+
// 区别在 ok 列上写着(ok = 0 时就是后者)。
|
|
277
|
+
`${pad(r.costPerSuccess != null ? r.costPerSuccess.toFixed(6) : '-', 12)}`);
|
|
278
|
+
}
|
|
279
|
+
console.log('');
|
|
144
280
|
if (rows.length === 0) {
|
|
145
281
|
console.log('(没有可归因的能力:这些 trace 里既没有 capability span,也没有 tool.output 事件)');
|
|
146
282
|
return 0;
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@migor/agentia": "^0.10.
|
|
14
|
+
"@migor/agentia": "^0.10.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@migor/cli": "^0.10.
|
|
17
|
+
"@migor/cli": "^0.10.3",
|
|
18
18
|
"tsx": "^4.19.0",
|
|
19
19
|
"typescript": "^7.0.2",
|
|
20
20
|
"@types/node": "^22.0.0"
|
package/dist/templates.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 字符串不过编译器:模板代码对仓库自己的 typecheck/lint 不可见,缺陷要等用户
|
|
5
5
|
* npm install 后才暴露(discover 路径「生产必崩」就是这么漏出去的)。真文件化后
|
|
6
6
|
* 模板被仓库自己的工具链全程照看:`tsconfig.templates.json` 把它纳入 tsc
|
|
7
|
-
* ('@migor/agentia' 经 paths 映射到框架 src
|
|
7
|
+
* ('@migor/agentia' 经 paths 映射到框架 src);Biome 收 lint,formatter 对 templates/** 关闭(生成物字节是兼容契约,见 biome.jsonc)。
|
|
8
8
|
*
|
|
9
9
|
* 占位符纪律:token 只许出现在**字符串 / 注释 / 标识符**位置
|
|
10
10
|
* (`__PROJECT_NAME__` / `__NAME__` / `__CLASS_NAME__` / `__METHOD_NAME__`),
|