@migor/cli 0.10.2 → 0.10.4
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 +969 -64
- package/LICENSE +1 -1
- package/README.md +33 -11
- package/dist/AGENTS.md +234 -41
- package/dist/cli.js +30 -22
- package/dist/create-minimal.js +46 -0
- package/dist/create.js +16 -6
- package/dist/doctor.js +40 -0
- package/dist/export.js +2 -2
- package/dist/harvest.js +32 -9
- package/dist/inspector/fromTrace.js +9 -1
- package/dist/inspector/summary.js +52 -5
- package/dist/inspector/trace-view.bundle.js +694 -0
- package/dist/inspector-routes.js +7 -0
- package/dist/redact.js +82 -0
- package/dist/report.js +141 -5
- package/dist/templates/README.md +55 -2
- package/dist/templates/gitignore +2 -1
- package/dist/templates/minimal/README.md +36 -0
- package/dist/templates/minimal/src/main.ts +35 -0
- package/dist/templates/package.json +2 -2
- package/dist/templates/src/app.ts +27 -12
- package/dist/templates/src/registry.ts +9 -7
- package/dist/templates/src/tools/read-file/index.ts +18 -7
- package/dist/templates/tsconfig.json +2 -0
- package/dist/templates.js +7 -1
- package/dist/version.js +31 -0
- package/dist/view.js +361 -0
- package/package.json +14 -6
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/* @migor/trace-view —— 单文件 bundle(由 scripts/bundle.mjs 从 src/ 自动生成,请勿手改)。
|
|
2
|
+
* 用途:`agentia view` 出的单文件自包含 HTML 内联本文件(无外链 / 离线可开 / 可转发)。
|
|
3
|
+
* 真源仍是 src/;改渲染逻辑请改那里再跑 build。零依赖、纯 ESM。 */
|
|
4
|
+
|
|
5
|
+
/* ===== src/view.js ===== */
|
|
6
|
+
/* Agentia trace 调用树渲染器 —— 数据无关、零依赖。
|
|
7
|
+
*
|
|
8
|
+
* 只认五个动作:reset 开树 / start 开 span / event 记事件 / end 收 span / finish 收 run。
|
|
9
|
+
* 谁喂它都行:官网 playground 的模拟回放脚本,或 fromTrace.js 的 playTrace(真实 Trace)。
|
|
10
|
+
*
|
|
11
|
+
* 视觉语言与语义(逐字取自 packages/website/src/scripts/playground.js,行为不变):
|
|
12
|
+
* - 事件行(tool.input / tool.output)没有状态圈、没有耗时,比 span 行更轻;
|
|
13
|
+
* - 事件与子 span 按发生顺序混排(input 先于它触发的 capability span、output 后于它)——
|
|
14
|
+
* 这个先后本身就是语义,不能拍平成一类;
|
|
15
|
+
* - 四类能力标识符(⚙ tool / ◆ skill / ¶ prompt / ⊕ subagent)只靠字形区分、不上类型色。
|
|
16
|
+
*
|
|
17
|
+
* 展开:折叠态**一字不变**(省略号收敛,事件行比 span 行更轻),点事件行可展开看**完整正文**。
|
|
18
|
+
* 展开状态存在渲染之外(每次事件都会全量重建 DOM),否则实时 run 里刚点开的行会在下一条事件
|
|
19
|
+
* 到来时自己合上。数据侧「行内摘要」与「展开全文」是两个字段:`text` 是摘要(入参经 fmtArg
|
|
20
|
+
* 砍到 62 字符),`full` 才是原文 —— 只有全文能撑起「调试时看工具结果」这件事。
|
|
21
|
+
*
|
|
22
|
+
* 宿主需提供的 CSS 变量:--ice --faint --text(见 trace-view.css)。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const CAP_ICO = { tool: '⚙', skill: '◆', prompt: '¶', subagent: '⊕' };
|
|
26
|
+
|
|
27
|
+
/** 能力类型取自名字前缀(tool: / skill: / prompt: / subagent:) */
|
|
28
|
+
function capabilityTypeOf(name) {
|
|
29
|
+
const t = String(name || '').split(':')[0];
|
|
30
|
+
return CAP_ICO[t] ? t : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fmtNum = (n) => n.toLocaleString('en-US');
|
|
34
|
+
const fmtMs = (ms) => (ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms');
|
|
35
|
+
|
|
36
|
+
/* span 入参摘要:写进 trace 行,否则同名 capability(如两次 tool:get_weather)无法区分 */
|
|
37
|
+
function fmtArg(v) {
|
|
38
|
+
if (v == null) return '';
|
|
39
|
+
let s;
|
|
40
|
+
if (typeof v === 'object') {
|
|
41
|
+
const keys = Object.keys(v);
|
|
42
|
+
if (!keys.length) return ''; // 无入参就不显示,避免行里多一个 "{}"
|
|
43
|
+
s =
|
|
44
|
+
'{ ' +
|
|
45
|
+
keys
|
|
46
|
+
.slice(0, 4)
|
|
47
|
+
.map((k) => {
|
|
48
|
+
let sv = typeof v[k] === 'object' ? JSON.stringify(v[k]) : String(v[k]);
|
|
49
|
+
if (sv.length > 22) sv = sv.slice(0, 21) + '…';
|
|
50
|
+
return k + ': ' + sv;
|
|
51
|
+
})
|
|
52
|
+
.join(', ') +
|
|
53
|
+
(keys.length > 4 ? ', …' : '') +
|
|
54
|
+
' }';
|
|
55
|
+
} else s = String(v);
|
|
56
|
+
return s.length > 62 ? s.slice(0, 61) + '…' : s;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* span 入参**原文** —— fmtArg 的对偶:摘要给折叠态,原文给展开态。
|
|
60
|
+
* 两者必须定义在同一处:哪个宿主自己搓一个「原文」,「展开」就会在那里退回摘要
|
|
61
|
+
* (点开什么都没多出来),而两个宿主共用这一份渲染器,正是为了不让这种漂移发生。 */
|
|
62
|
+
function rawArg(v) {
|
|
63
|
+
if (v == null) return '';
|
|
64
|
+
if (typeof v === 'string') return v;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(v) ?? String(v);
|
|
67
|
+
} catch {
|
|
68
|
+
return String(v);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function el(tag, cls, text) {
|
|
73
|
+
const node = document.createElement(tag);
|
|
74
|
+
if (cls) node.className = cls;
|
|
75
|
+
if (text !== undefined) node.textContent = text;
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 建一个绑定到 rootEl 的 trace 视图。
|
|
81
|
+
* opts.price = { input, output }($/M tokens)或 null(无公开单价则不估算成本)
|
|
82
|
+
* opts.usage = { in, out, cost } 三个 DOM 节点(可选;给了才更新计数器)
|
|
83
|
+
* opts.onUsage = (acc) => void(可选;每次累计后回调,便于宿主自定义展示)
|
|
84
|
+
*/
|
|
85
|
+
function createTraceView(rootEl, opts = {}) {
|
|
86
|
+
let price = opts.price ?? null;
|
|
87
|
+
const onUsage = opts.onUsage ?? null;
|
|
88
|
+
const usageEls = opts.usage ?? null;
|
|
89
|
+
|
|
90
|
+
let traceRoot = null;
|
|
91
|
+
const spanMap = new Map();
|
|
92
|
+
const usageAcc = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
93
|
+
/* 展开态的行 key 集合 + 事件 key 发号器。两者都必须活在 renderTrace 之外:
|
|
94
|
+
渲染是【全量重建】(rootEl.innerHTML = ''),状态放进 DOM 或渲染过程里就活不过下一次事件。 */
|
|
95
|
+
const expanded = new Set();
|
|
96
|
+
let evSeq = 0;
|
|
97
|
+
|
|
98
|
+
/** 正在选文本时不要收起/展开 —— 鼠标拖选到行外松手会补一次 click,否则选完就自己合上了 */
|
|
99
|
+
function hasSelection() {
|
|
100
|
+
try {
|
|
101
|
+
return String(globalThis.getSelection?.() ?? '') !== '';
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toggleExpand(key) {
|
|
108
|
+
if (expanded.has(key)) expanded.delete(key);
|
|
109
|
+
else expanded.add(key);
|
|
110
|
+
renderTrace();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderTrace() {
|
|
114
|
+
rootEl.innerHTML = '';
|
|
115
|
+
const rows = [];
|
|
116
|
+
/* 每个节点下【事件】与【子 span】按发生顺序混排:tool.input 先于它触发的 capability span、
|
|
117
|
+
tool.output 后于它,这个先后本身就是语义,不能拍平成一类。 */
|
|
118
|
+
(function walk(node, prefix, isLast, isRoot) {
|
|
119
|
+
rows.push({ node, prefix, isRoot, last: isLast, ev: null });
|
|
120
|
+
const items = node.order || [];
|
|
121
|
+
items.forEach((it, i) => {
|
|
122
|
+
const last = i === items.length - 1;
|
|
123
|
+
const next = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
|
|
124
|
+
if (it.t === 'span') walk(it.n, next, last, false);
|
|
125
|
+
else rows.push({ node, prefix: next, isRoot: false, last, ev: it.e });
|
|
126
|
+
});
|
|
127
|
+
})(traceRoot, '', true, true);
|
|
128
|
+
|
|
129
|
+
rows.forEach(({ node, prefix, isRoot, last, ev }) => {
|
|
130
|
+
const branch = isRoot ? '' : prefix + (last ? '└─ ' : '├─ ');
|
|
131
|
+
|
|
132
|
+
/* 事件行:没有 status 圈、没有耗时、不参与 usage 计数,只带入参/出参摘要 */
|
|
133
|
+
if (ev) {
|
|
134
|
+
const open = expanded.has(ev.key);
|
|
135
|
+
const row = el('div', 'tr-row tr-ev' + (ev.ok === false ? ' error' : ''));
|
|
136
|
+
row.dataset.ev = ev.type;
|
|
137
|
+
row.dataset.capability = capabilityTypeOf(ev.tool);
|
|
138
|
+
row.appendChild(el('span', 'tr-pre', branch));
|
|
139
|
+
row.appendChild(el('span', 'tr-evv', ev.type === 'tool.output' ? '◂' : '▸'));
|
|
140
|
+
row.appendChild(el('span', 'tr-evtype', ev.type));
|
|
141
|
+
// 非 tool.* 事件(usage.unpriced / llm.retry / budget.* / context.budget)没有工具名 ——
|
|
142
|
+
// 这一格整个不渲染,别把「无工具」显示成假工具名(.tr-name 是 flex:none,.tr-io 自然占满)
|
|
143
|
+
if (ev.tool) row.appendChild(el('span', 'tr-name', ev.tool));
|
|
144
|
+
// 展开态用全文(ev.full),折叠态用摘要(ev.text)—— 折叠态因此与加展开之前逐字一致
|
|
145
|
+
const io = el('span', 'tr-io', (open ? ev.full : ev.text) || '');
|
|
146
|
+
// 展开后正文已在行里,再挂一个占满整行的原生 tooltip 只会挡视线
|
|
147
|
+
io.title = open ? '' : ev.text || '';
|
|
148
|
+
row.appendChild(io);
|
|
149
|
+
if (ev.full) {
|
|
150
|
+
row.dataset.expandable = '1'; // 可展开标记:CSS 靠它出 caret / cursor,测试靠它找行
|
|
151
|
+
row.appendChild(el('span', 'tr-caret', open ? '▾' : '▸'));
|
|
152
|
+
if (open) row.className += ' tr-open';
|
|
153
|
+
row.addEventListener('click', () => {
|
|
154
|
+
if (!hasSelection()) toggleExpand(ev.key);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
rootEl.appendChild(row);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const bad = node.done && node.status === 'error';
|
|
162
|
+
const row = el('div', 'tr-row' + (node.done ? '' : ' running') + (bad ? ' error' : ''));
|
|
163
|
+
row.dataset.kind = node.kind;
|
|
164
|
+
const ut = node.kind === 'capability' ? capabilityTypeOf(node.name) : '';
|
|
165
|
+
if (ut) row.dataset.capability = ut;
|
|
166
|
+
row.appendChild(el('span', 'tr-pre', branch));
|
|
167
|
+
row.appendChild(el('span', 'tr-dot', node.done ? (bad ? '✕' : '●') : '◌'));
|
|
168
|
+
if (ut) row.appendChild(el('span', 'tr-ico', CAP_ICO[ut]));
|
|
169
|
+
row.appendChild(el('span', 'tr-name', node.name));
|
|
170
|
+
if (node.arg) {
|
|
171
|
+
const arg = el('span', 'tr-arg', node.arg);
|
|
172
|
+
arg.title = node.arg;
|
|
173
|
+
row.appendChild(arg);
|
|
174
|
+
}
|
|
175
|
+
let meta;
|
|
176
|
+
if (node.done) {
|
|
177
|
+
const parts = [fmtMs(node.ms)];
|
|
178
|
+
if (node.usage) {
|
|
179
|
+
parts.push(fmtNum(node.usage.input + node.usage.output) + ' tok');
|
|
180
|
+
const cr = node.usage.cacheRead || 0;
|
|
181
|
+
const cc = node.usage.cacheCreation || 0;
|
|
182
|
+
if (cr || cc) parts.push('cache ↑' + fmtNum(cr) + ' ↓' + fmtNum(cc));
|
|
183
|
+
}
|
|
184
|
+
if (bad) parts.push(node.error?.type || 'error');
|
|
185
|
+
meta = parts.join(' · ');
|
|
186
|
+
} else meta = '…';
|
|
187
|
+
const metaEl = el('span', 'tr-meta', meta);
|
|
188
|
+
if (bad && node.error?.message) metaEl.title = node.error.message;
|
|
189
|
+
row.appendChild(metaEl);
|
|
190
|
+
rootEl.appendChild(row);
|
|
191
|
+
// 失败原文必须**看得见** —— 只塞进 title(hover 才显)等于没显示。首次运行最常见的失败
|
|
192
|
+
// (没配 API key)就靠这一行定位;单起一行,不去挤占 nowrap 的 span 行。
|
|
193
|
+
if (bad && node.error?.message) {
|
|
194
|
+
const errLine = el('div', 'tr-errm');
|
|
195
|
+
errLine.appendChild(el('span', 'tr-pre', branch + ' '));
|
|
196
|
+
errLine.appendChild(el('span', 'tr-errm-msg', node.error.message));
|
|
197
|
+
rootEl.appendChild(errLine);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function renderUsage(acc) {
|
|
203
|
+
if (onUsage) onUsage(acc);
|
|
204
|
+
if (!usageEls) return;
|
|
205
|
+
if (usageEls.in) usageEls.in.textContent = fmtNum(acc.input);
|
|
206
|
+
if (usageEls.out) usageEls.out.textContent = fmtNum(acc.output);
|
|
207
|
+
if (!usageEls.cost) return;
|
|
208
|
+
if (price == null || price.input == null || price.output == null) {
|
|
209
|
+
usageEls.cost.textContent = '—'; // 无公开单价的端点不做估算(如 DeepSeek)
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const cost = (acc.input * price.input + acc.output * price.output) / 1e6;
|
|
213
|
+
usageEls.cost.textContent = '$' + cost.toFixed(4);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 开树。rootName 为 run 根显示名(如 'run · 审查一份文档') */
|
|
217
|
+
function reset(rootName) {
|
|
218
|
+
traceRoot = {
|
|
219
|
+
id: 'root',
|
|
220
|
+
kind: 'run',
|
|
221
|
+
name: rootName || 'run',
|
|
222
|
+
children: [],
|
|
223
|
+
events: [],
|
|
224
|
+
order: [],
|
|
225
|
+
done: false,
|
|
226
|
+
ms: 0,
|
|
227
|
+
usage: { input: 0, output: 0 },
|
|
228
|
+
};
|
|
229
|
+
spanMap.clear();
|
|
230
|
+
spanMap.set('root', traceRoot);
|
|
231
|
+
expanded.clear(); // 展开态属于上一棵树:reset 开新树时一并清掉
|
|
232
|
+
usageAcc.input = 0;
|
|
233
|
+
usageAcc.output = 0;
|
|
234
|
+
usageAcc.cacheRead = 0;
|
|
235
|
+
usageAcc.cacheCreation = 0;
|
|
236
|
+
renderTrace();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function start(s) {
|
|
240
|
+
const node = {
|
|
241
|
+
id: s.id,
|
|
242
|
+
kind: s.kind,
|
|
243
|
+
name: s.name,
|
|
244
|
+
arg: s.arg || '',
|
|
245
|
+
children: [],
|
|
246
|
+
events: [],
|
|
247
|
+
order: [],
|
|
248
|
+
done: false,
|
|
249
|
+
ms: 0,
|
|
250
|
+
status: 'ok',
|
|
251
|
+
error: null,
|
|
252
|
+
usage: null,
|
|
253
|
+
};
|
|
254
|
+
spanMap.set(s.id, node);
|
|
255
|
+
const parent = spanMap.get(s.parent) || traceRoot;
|
|
256
|
+
parent.children.push(node);
|
|
257
|
+
parent.order.push({ t: 'span', n: node });
|
|
258
|
+
renderTrace();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* span 事件:框架把普通工具 / @Prompt 调用记成【turn 上的事件】,不给它们建 capability span
|
|
262
|
+
(只有 skill / subagent 会 recorder.begin('capability', …),见 engine/loop.ts)。
|
|
263
|
+
events 与 order 并存:events 是数据、order 负责与子 span 的先后顺序。
|
|
264
|
+
|
|
265
|
+
`text` = 行内摘要,`full` = 展开看的原文(省略时同 text —— 出参与多数事件本来就是原文,
|
|
266
|
+
只有 tool.input 的摘要被 fmtArg 砍过,调用方需要显式给 full)。
|
|
267
|
+
`key` 是展开状态的稳定标识:事件对象跨全量重建存活,所以键也稳定。 */
|
|
268
|
+
function event(id, type, tool, text, ok, full) {
|
|
269
|
+
const node = spanMap.get(id);
|
|
270
|
+
if (!node) return;
|
|
271
|
+
const e = {
|
|
272
|
+
type,
|
|
273
|
+
tool,
|
|
274
|
+
text,
|
|
275
|
+
ok: ok !== false,
|
|
276
|
+
full: full === undefined ? text : full,
|
|
277
|
+
key: 'ev' + ++evSeq,
|
|
278
|
+
};
|
|
279
|
+
node.events.push(e);
|
|
280
|
+
node.order.push({ t: 'ev', e });
|
|
281
|
+
renderTrace();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function end(s) {
|
|
285
|
+
const node = spanMap.get(s.id);
|
|
286
|
+
if (!node) return;
|
|
287
|
+
node.done = true;
|
|
288
|
+
node.ms = s.ms || 0;
|
|
289
|
+
node.status = s.status || 'ok';
|
|
290
|
+
node.error = s.error || null;
|
|
291
|
+
if (s.arg) node.arg = s.arg;
|
|
292
|
+
node.usage = s.usage
|
|
293
|
+
? {
|
|
294
|
+
input: s.usage.input || 0,
|
|
295
|
+
output: s.usage.output || 0,
|
|
296
|
+
cacheRead: s.usage.cacheRead || 0,
|
|
297
|
+
cacheCreation: s.usage.cacheCreation || 0,
|
|
298
|
+
}
|
|
299
|
+
: null;
|
|
300
|
+
// 计数器只累加 llm.turn(capability span 的 usage 是子 span 聚合,重复计入会双算)
|
|
301
|
+
if (s.usage && node.kind === 'llm.turn') {
|
|
302
|
+
usageAcc.input += node.usage.input;
|
|
303
|
+
usageAcc.output += node.usage.output;
|
|
304
|
+
usageAcc.cacheRead += node.usage.cacheRead;
|
|
305
|
+
usageAcc.cacheCreation += node.usage.cacheCreation;
|
|
306
|
+
renderUsage(usageAcc);
|
|
307
|
+
}
|
|
308
|
+
renderTrace();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function finish(ms, status, error) {
|
|
312
|
+
if (!traceRoot) return;
|
|
313
|
+
traceRoot.done = true;
|
|
314
|
+
traceRoot.ms = ms;
|
|
315
|
+
// run 失败时根 span 也要标红:原先 status 恒为 ok,错误只体现在子 span 上
|
|
316
|
+
traceRoot.status = status || 'ok';
|
|
317
|
+
traceRoot.error = error || null;
|
|
318
|
+
traceRoot.usage = { input: usageAcc.input, output: usageAcc.output };
|
|
319
|
+
renderTrace();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
reset,
|
|
324
|
+
start,
|
|
325
|
+
event,
|
|
326
|
+
end,
|
|
327
|
+
finish,
|
|
328
|
+
render: renderTrace,
|
|
329
|
+
setUsage: renderUsage,
|
|
330
|
+
/** 换单价(如真实模式切服务商):null 表示无公开单价 → 成本显示为 — */
|
|
331
|
+
setPrice(p) {
|
|
332
|
+
price = p ?? null;
|
|
333
|
+
renderUsage(usageAcc);
|
|
334
|
+
},
|
|
335
|
+
get usage() {
|
|
336
|
+
return usageAcc;
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* ===== src/fromTrace.js ===== */
|
|
342
|
+
/* 把框架的 Trace.spans[] 归一成渲染动作,喂给 createTraceView 建出的视图。
|
|
343
|
+
*
|
|
344
|
+
* 框架的 trace 是【扁平 spans[] + parentSpanId】,渲染器要的是【嵌套树 + 每个节点内
|
|
345
|
+
* 事件与子 span 按发生顺序混排的 order】。本文件就是那座桥 —— 同一份渲染器因此
|
|
346
|
+
* 既能吃官网的模拟回放,也能吃真实 run 的 trace。
|
|
347
|
+
*
|
|
348
|
+
* 归一规则:
|
|
349
|
+
* 1. 全局时间线:每个 span 的 start / end 与它的每个 event 汇到一起,按时间排序。
|
|
350
|
+
* tie-break:同一毫秒 start(0) → event(1) → end(2),且 start 按深度浅的在前 ——
|
|
351
|
+
* 保证父 span 先于子 span 打开、tool.input 落在它所属 turn 打开之后。
|
|
352
|
+
* 2. 显示名合成:框架的 capability span 名是【裸名】(类型记在 attributes,如 {subagent:'x'}),
|
|
353
|
+
* 这里补成 `type:name`,与渲染器的四类标识符对齐;事件里的工具名若是某个 capability span
|
|
354
|
+
* 的裸名,同样补前缀(否则回落 tool:)。
|
|
355
|
+
* 3. usage 只由 llm.turn 累计(capability span 的 usage 是子 span 聚合,计入会双算)。
|
|
356
|
+
*/
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
/** 从 span.attributes 认能力类型(框架用 `setAttribute(capabilityId, 'subagent', name)` 记类型) */
|
|
361
|
+
function spanType(s) {
|
|
362
|
+
const attrs = s.attributes || {};
|
|
363
|
+
for (const t of Object.keys(CAP_ICO)) {
|
|
364
|
+
if (attrs[t] != null) return t;
|
|
365
|
+
}
|
|
366
|
+
return '';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 显示名:capability span 补 `type:` 前缀(已是 type:name 形态则原样保留) */
|
|
370
|
+
function displayName(s) {
|
|
371
|
+
const raw = String(s.name || '');
|
|
372
|
+
const t = spanType(s);
|
|
373
|
+
if (!raw) return t || s.kind || '';
|
|
374
|
+
if (capabilityTypeOf(raw)) return raw; // 已带前缀
|
|
375
|
+
return t ? `${t}:${raw}` : raw;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function usageOf(s) {
|
|
379
|
+
if (!s.usage) return null;
|
|
380
|
+
return {
|
|
381
|
+
input: s.usage.inputTokens || 0,
|
|
382
|
+
output: s.usage.outputTokens || 0,
|
|
383
|
+
cacheRead: s.usage.cacheReadTokens || 0,
|
|
384
|
+
cacheCreation: s.usage.cacheCreationTokens || 0,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 事件工具名:**只有 `tool.*` 事件带工具名**。
|
|
389
|
+
* 框架的事件里 `usage.unpriced` / `llm.retry` / `budget.exceeded` / `context.budget` 的事件体
|
|
390
|
+
* 根本没有 `tool` 字段 —— 一律套前缀只会得到 `tool:?` 这种把「无工具」显示成「名字叫 ? 的工具」
|
|
391
|
+
* 的误导标签(面板与官网 playground 共用本渲染器,等于把假工具摆给使用者看)。
|
|
392
|
+
* 工具裸名靠兄弟 capability span 反查类型,找不到按 tool 算。 */
|
|
393
|
+
function eventToolName(evName, bodyTool, typeMap) {
|
|
394
|
+
if (!String(evName || '').startsWith('tool.')) return '';
|
|
395
|
+
const raw = String(bodyTool || '?');
|
|
396
|
+
if (capabilityTypeOf(raw)) return raw;
|
|
397
|
+
return `${typeMap.get(raw) || 'tool'}:${raw}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** 事件摘要:input 走 fmtArg(与 span 入参同一形态),output 走文本内容 */
|
|
401
|
+
function eventText(ev) {
|
|
402
|
+
const body = ev.body;
|
|
403
|
+
if (ev.name === 'tool.input') return fmtArg(body?.input);
|
|
404
|
+
if (ev.name === 'tool.output') {
|
|
405
|
+
const c = body?.content;
|
|
406
|
+
return typeof c === 'string' ? c : c == null ? '' : JSON.stringify(c);
|
|
407
|
+
}
|
|
408
|
+
return body == null ? '' : typeof body === 'string' ? body : JSON.stringify(body);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 事件全文(展开态用)。
|
|
413
|
+
*
|
|
414
|
+
* 只有 `tool.input` 需要单独取:`fmtArg` 把入参摘要**砍到 4 个键 / 每值 21 字符 / 整串 62 字符**,
|
|
415
|
+
* 直接拿摘要当全文就是「假展开」—— 点开看到的还是那 62 个字符。出参与其他事件本身就是原文
|
|
416
|
+
* (`eventText` 原样返回 content),无需二次处理。
|
|
417
|
+
* 原文口径统一在 view.js 的 `rawArg`:官网 playground 的模拟回放也要用它,两处一处定义。
|
|
418
|
+
*/
|
|
419
|
+
function eventFull(ev) {
|
|
420
|
+
if (ev.name === 'tool.input') return rawArg(ev.body?.input);
|
|
421
|
+
return eventText(ev);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 把真实 Trace 线性回放给视图(同步、一次性)。
|
|
426
|
+
*
|
|
427
|
+
* 两种用途共用本函数(这也是它必须呆在 trace-view 里、不进 CLI 的理由):
|
|
428
|
+
* - **收尾的整棵 trace**(run 结束后一次画完);
|
|
429
|
+
* - **在飞的那棵树**(CLI inspector 的实时右栏:每收到一条增量记账事件就整体重画一次,
|
|
430
|
+
* 见 `packages/cli/src/panel-logic.ts` 的 `applyTraceEvent`)。重画是幂等的 ——
|
|
431
|
+
* 同一棵 spans 交给它多少次,画出来的都是同一棵树。
|
|
432
|
+
*
|
|
433
|
+
* 返回 false 表示 trace 空、未做任何渲染。
|
|
434
|
+
*/
|
|
435
|
+
function playTrace(view, trace) {
|
|
436
|
+
const spans = Array.isArray(trace?.spans) ? trace.spans : [];
|
|
437
|
+
// ⚠️ 空 spans 也**必须 reset**:playTrace 的契约是「把这个 view 变成这棵树」——
|
|
438
|
+
// 直接 return 会把**上一条 trace 的树**留在页面上,而下拉里选中的是这一条
|
|
439
|
+
// (切换后看到别人的数据,比看到空白更坏;2026-10-21 在 `agentia view` 的多 trace
|
|
440
|
+
// 下拉里实测到:先选一个正常 trace,再选一个 0 spans 的,树原样不动)。
|
|
441
|
+
if (!spans.length) {
|
|
442
|
+
const label = trace?.traceId ? String(trace.traceId) : '空 trace';
|
|
443
|
+
view.reset(`run · ${label}(无 spans)`);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const byId = new Map(spans.map((s) => [s.spanId, s]));
|
|
448
|
+
const root = spans.find((s) => s.kind === 'run') || spans[0];
|
|
449
|
+
|
|
450
|
+
// 裸名 → 能力类型(供事件补前缀)
|
|
451
|
+
const typeMap = new Map();
|
|
452
|
+
for (const s of spans) {
|
|
453
|
+
const t = spanType(s);
|
|
454
|
+
if (s.kind === 'capability' && t) typeMap.set(String(s.name || ''), t);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const depthOf = (s) => {
|
|
458
|
+
let d = 0;
|
|
459
|
+
let cur = s;
|
|
460
|
+
const seen = new Set();
|
|
461
|
+
while (cur?.parentSpanId && byId.has(cur.parentSpanId) && !seen.has(cur.spanId)) {
|
|
462
|
+
seen.add(cur.spanId);
|
|
463
|
+
d += 1;
|
|
464
|
+
cur = byId.get(cur.parentSpanId);
|
|
465
|
+
}
|
|
466
|
+
return d;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const items = [];
|
|
470
|
+
let seq = 0; // 出现顺序:时间缺失 / 并列时的确定性回退(NaN 参与比较会让排序结果随引擎不定)
|
|
471
|
+
const timeOf = (v, fallback) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
|
|
472
|
+
for (const s of spans) {
|
|
473
|
+
const start = timeOf(s.startedAt, 0);
|
|
474
|
+
items.push({ t: start, ord: 0, d: depthOf(s), s, kind: 'start', seq: seq++ });
|
|
475
|
+
if (s.endedAt != null)
|
|
476
|
+
items.push({ t: timeOf(s.endedAt, start), ord: 2, d: 0, s, kind: 'end', seq: seq++ });
|
|
477
|
+
for (const ev of s.events || [])
|
|
478
|
+
items.push({ t: timeOf(ev.time, start), ord: 1, d: 0, s, kind: 'event', ev, seq: seq++ });
|
|
479
|
+
}
|
|
480
|
+
items.sort((a, b) => a.t - b.t || a.ord - b.ord || a.d - b.d || a.seq - b.seq);
|
|
481
|
+
|
|
482
|
+
const rootRaw = String(root.name || 'run');
|
|
483
|
+
// 框架的 run 根名可能是 runName(如 'app')或已是 'run · x' 形态,避免重复前缀
|
|
484
|
+
view.reset(rootRaw.startsWith('run') ? rootRaw : 'run · ' + rootRaw);
|
|
485
|
+
|
|
486
|
+
for (const it of items) {
|
|
487
|
+
if (it.kind === 'start') {
|
|
488
|
+
if (it.s === root) continue; // 根由 reset 建,不重复开
|
|
489
|
+
view.start({
|
|
490
|
+
id: it.s.spanId,
|
|
491
|
+
parent: it.s.parentSpanId,
|
|
492
|
+
kind: it.s.kind,
|
|
493
|
+
name: displayName(it.s),
|
|
494
|
+
arg: '',
|
|
495
|
+
});
|
|
496
|
+
} else if (it.kind === 'event') {
|
|
497
|
+
view.event(
|
|
498
|
+
it.s.spanId,
|
|
499
|
+
it.ev.name,
|
|
500
|
+
eventToolName(it.ev.name, it.ev.body?.tool, typeMap),
|
|
501
|
+
eventText(it.ev),
|
|
502
|
+
it.ev.body?.ok !== false,
|
|
503
|
+
eventFull(it.ev),
|
|
504
|
+
);
|
|
505
|
+
} else {
|
|
506
|
+
view.end({
|
|
507
|
+
id: it.s.spanId,
|
|
508
|
+
ms: it.s.endedAt - it.s.startedAt,
|
|
509
|
+
usage: usageOf(it.s),
|
|
510
|
+
status: it.s.status,
|
|
511
|
+
error: it.s.error,
|
|
512
|
+
arg: '',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 根 span 还没结束(`span.end` 尚未到达)⇒ **不收尾**:`finish()` 会把根标成已完成
|
|
518
|
+
// (● + 耗时),而这棵树其实还在长 —— 在飞时看到「已完成」是假事实。留 ◌ 才对。
|
|
519
|
+
// 收尾的整棵 trace 根必有 `endedAt`,所以这条分支只对「在飞 / 截断」的树生效。
|
|
520
|
+
if (root.endedAt != null) view.finish(root.endedAt - root.startedAt, root.status, root.error);
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* ===== src/summary.js ===== */
|
|
525
|
+
/* 能力排行(G2)—— 「哪个能力慢 / 贵 / 爱失败」。
|
|
526
|
+
*
|
|
527
|
+
* 与 trace 调用树同属**框架无关**的展示层:入参是 duck-typed 的 trace 形状,零依赖。
|
|
528
|
+
* CLI inspector 与官网 playground 共用同一份实现,避免两处聚合口径漂移。
|
|
529
|
+
*
|
|
530
|
+
* 数据来源(与框架侧的具名约定一致):
|
|
531
|
+
* - `capability` span(skill / subagent)→ 调用耗时与子孙 usage 聚合(tokens / costUsd)。
|
|
532
|
+
* ⚠️ **usage 只认顶层 capability**(没有 capability 祖先的):框架侧 capability.usage
|
|
533
|
+
* 是其**子孙 llm.turn 的聚合**(`src/engine/tracer.ts`),嵌套时(`@SubAgent` 套
|
|
534
|
+
* `@Skill`)外层与内层带着同一批 token ⇒ 逐行累加会重复计数。嵌套行仍计
|
|
535
|
+
* calls / errors / 耗时(那是它自己这次调用的真账,不重复),tokens / costUsd
|
|
536
|
+
* 留给顶层行 —— 各行可加总,与 `src/core/trace.ts`「capability 聚合不进
|
|
537
|
+
* totalUsage(避免重复计数)」同一条纪律。没选「从 llm.turn 重新归账」:
|
|
538
|
+
* 手写 / 外来的 trace 可能只在 capability span 上带 usage(没有嵌套的 llm.turn
|
|
539
|
+
* 子孙),重算会把这类成本静默清零。
|
|
540
|
+
* - `llm.turn` span 上的 `tool.output` 事件(普通工具不建 span)→ 工具耗时与成败,
|
|
541
|
+
* 读 `body.durationMs` / `body.ok`;
|
|
542
|
+
* - `approval.decided` 事件(**审批不建 span**,G7 护栏)→ `guardrail:<工具名>` 一类的
|
|
543
|
+
* 等审批时长与被拒次数,读 `body.tool` / `body.approved` / `body.waitedMs`。
|
|
544
|
+
* 扫面**不限 span kind**:落点因段而异(首段 llm.turn、恢复段 run 根 —— 续跑入口把
|
|
545
|
+
* run 根 id 当 turnId),限 kind = 真实 HITL 主流程(挂起 → 人批 → 恢复)零产出;
|
|
546
|
+
* 不双计:记账点只有一处(executeOneTool),一次决定只记一笔。
|
|
547
|
+
*
|
|
548
|
+
* ⚠️ 单条 run 内样本常 < 5,分位没有意义 —— 排行以 total/max 为主,不给 p50/p95
|
|
549
|
+
* (要看分位请用框架的 `buildRunReport` + `mergeRunReports`)。
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
function kindOf(span) {
|
|
553
|
+
if (span.attributes && span.attributes.skill !== undefined) return 'skill';
|
|
554
|
+
if (span.attributes && span.attributes.subagent !== undefined) return 'subagent';
|
|
555
|
+
return 'capability';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* 从一条 trace 算能力排行。
|
|
560
|
+
* @param {any} trace Trace 形状(spans[] / events[] / usage?)
|
|
561
|
+
* @returns {Array<{capability:string,calls:number,errors:number,totalMs:number,maxMs:number,tokens:number|null,costUsd:number|null}>}
|
|
562
|
+
* 按 totalMs 降序
|
|
563
|
+
*/
|
|
564
|
+
function summarizeTrace(trace) {
|
|
565
|
+
const acc = new Map();
|
|
566
|
+
const get = (capability) => {
|
|
567
|
+
let a = acc.get(capability);
|
|
568
|
+
if (!a) {
|
|
569
|
+
a = { capability, calls: 0, errors: 0, totalMs: 0, maxMs: 0, tokens: null, costUsd: null };
|
|
570
|
+
acc.set(capability, a);
|
|
571
|
+
}
|
|
572
|
+
return a;
|
|
573
|
+
};
|
|
574
|
+
const addMs = (a, ms) => {
|
|
575
|
+
const v = Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
576
|
+
a.totalMs += v;
|
|
577
|
+
if (v > a.maxMs) a.maxMs = v;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const spans = trace?.spans || [];
|
|
581
|
+
const byId = new Map();
|
|
582
|
+
for (const s of spans) if (s && s.spanId != null) byId.set(s.spanId, s);
|
|
583
|
+
// 该 span 是否嵌套在另一个 capability 之下(沿 parentSpanId 上爬;seen 防手写 trace 的环)
|
|
584
|
+
const hasCapabilityAncestor = (span) => {
|
|
585
|
+
let p = span.parentSpanId;
|
|
586
|
+
const seen = new Set();
|
|
587
|
+
while (p != null && !seen.has(p)) {
|
|
588
|
+
seen.add(p);
|
|
589
|
+
const ps = byId.get(p);
|
|
590
|
+
if (!ps) return false;
|
|
591
|
+
if (ps.kind === 'capability') return true;
|
|
592
|
+
p = ps.parentSpanId;
|
|
593
|
+
}
|
|
594
|
+
return false;
|
|
595
|
+
};
|
|
596
|
+
for (const span of spans) {
|
|
597
|
+
if (span.kind === 'capability') {
|
|
598
|
+
const a = get(`${kindOf(span)}:${span.name}`);
|
|
599
|
+
a.calls += 1;
|
|
600
|
+
if (span.status === 'error') a.errors += 1;
|
|
601
|
+
if (span.endedAt != null) addMs(a, span.endedAt - span.startedAt);
|
|
602
|
+
const u = span.usage;
|
|
603
|
+
if (u && !hasCapabilityAncestor(span)) {
|
|
604
|
+
a.tokens =
|
|
605
|
+
(a.tokens || 0) +
|
|
606
|
+
(u.inputTokens || 0) +
|
|
607
|
+
(u.outputTokens || 0) +
|
|
608
|
+
(u.cacheReadTokens || 0) +
|
|
609
|
+
(u.cacheCreationTokens || 0);
|
|
610
|
+
if (u.costEstimate != null) a.costUsd = (a.costUsd || 0) + u.costEstimate;
|
|
611
|
+
}
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
// approval.decided **不限 span kind**(落点因段而异:首段 llm.turn、恢复段 run 根 ——
|
|
615
|
+
// 续跑入口把 run 根 id 当 turnId,见头注);tool.output 只在 llm.turn 上。
|
|
616
|
+
const isTurn = span.kind === 'llm.turn';
|
|
617
|
+
for (const e of span.events || []) {
|
|
618
|
+
if (e.name === 'approval.decided') {
|
|
619
|
+
// G7 护栏 —— 审批**只是事件**(刻意不建 span),所以它天然不在任何 span 聚合里,
|
|
620
|
+
// 此前「哪个工具卡在审批上、被拒了几次」在排行上完全看不见。这一类补上它:
|
|
621
|
+
// calls = 走到「决定」这一步的次数(批准或被拒都算);
|
|
622
|
+
// errors = **被拒**次数(护栏真挡下了几次 —— 它不是系统错误,但那次工具确实没跑);
|
|
623
|
+
// total/max = 等审批时长(事件的 `waitedMs`)。⚠️ 只有宿主回填了 `requestedAt`
|
|
624
|
+
// 才有这个数 ⇒ 缺就记 0,**不编造**(框架侧刻意不记「没数据」的 0)。
|
|
625
|
+
// ⚠️ 口径边界:只聚 `approval.decided`。只有 `approval.requested` 而始终没恢复的
|
|
626
|
+
// 挂起**不产出** guardrail 行 —— 它没有结论、也没有时长,硬记一格等于编账。
|
|
627
|
+
const b = e.body;
|
|
628
|
+
if (!b || typeof b !== 'object' || typeof b.tool !== 'string') continue;
|
|
629
|
+
const ga = get(`guardrail:${b.tool}`);
|
|
630
|
+
ga.calls += 1;
|
|
631
|
+
if (b.approved === false) ga.errors += 1;
|
|
632
|
+
addMs(ga, typeof b.waitedMs === 'number' ? b.waitedMs : 0);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (!isTurn || e.name !== 'tool.output') continue;
|
|
636
|
+
const b = e.body;
|
|
637
|
+
if (!b || typeof b !== 'object' || typeof b.tool !== 'string') continue;
|
|
638
|
+
const a = get(`tool:${b.tool}`);
|
|
639
|
+
a.calls += 1;
|
|
640
|
+
if (b.ok === false) a.errors += 1;
|
|
641
|
+
addMs(a, typeof b.durationMs === 'number' ? b.durationMs : 0);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return [...acc.values()].sort((x, y) => y.totalMs - x.totalMs || y.calls - x.calls);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** 把排行渲染成一个小表格(返回 HTML 字符串;样式由 trace-view.css 提供) */
|
|
649
|
+
function renderSummary(rows) {
|
|
650
|
+
if (!rows || rows.length === 0) {
|
|
651
|
+
return '<div class="tv-sum-empty">// 没有可归因的能力(没有 capability span,也没有 tool.output 事件)</div>';
|
|
652
|
+
}
|
|
653
|
+
const fmtMs = (ms) => (ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms');
|
|
654
|
+
const body = rows
|
|
655
|
+
.map((r) => {
|
|
656
|
+
const err = r.errors > 0 ? `<span class="tv-sum-err">${r.errors}</span>` : '0';
|
|
657
|
+
const cost =
|
|
658
|
+
r.costUsd != null
|
|
659
|
+
? r.costUsd < 0.000001
|
|
660
|
+
? r.costUsd.toExponential(2)
|
|
661
|
+
: r.costUsd.toFixed(6)
|
|
662
|
+
: '-';
|
|
663
|
+
return `<tr><td class="tv-sum-capability">${escapeHtml(r.capability)}</td><td>${r.calls}</td><td>${err}</td><td>${fmtMs(r.totalMs)}</td><td>${fmtMs(r.maxMs)}</td><td>${r.tokens != null ? r.tokens : '-'}</td><td>${cost}</td></tr>`;
|
|
664
|
+
})
|
|
665
|
+
.join('');
|
|
666
|
+
return (
|
|
667
|
+
'<table class="tv-sum"><thead><tr><th>capability</th><th>calls</th><th>err</th><th>total</th><th>max</th><th>tokens</th><th>cost</th></tr></thead>' +
|
|
668
|
+
`<tbody>${body}</tbody></table>`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function escapeHtml(s) {
|
|
673
|
+
return String(s).replace(
|
|
674
|
+
/[&<>"']/g,
|
|
675
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c],
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* ===== src/index.js ===== */
|
|
680
|
+
/* @migor/trace-view —— Agentia trace 调用树渲染器(框架无关、零依赖)。
|
|
681
|
+
* 官网 playground 与 CLI inspector 共用同一份,避免两处渲染漂移。 */
|
|
682
|
+
|
|
683
|
+
export {
|
|
684
|
+
CAP_ICO,
|
|
685
|
+
capabilityTypeOf,
|
|
686
|
+
createTraceView,
|
|
687
|
+
fmtArg,
|
|
688
|
+
fmtMs,
|
|
689
|
+
fmtNum,
|
|
690
|
+
playTrace,
|
|
691
|
+
rawArg,
|
|
692
|
+
renderSummary,
|
|
693
|
+
summarizeTrace,
|
|
694
|
+
};
|