@zenera/neo 1.1.0
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/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/agent.d.ts +66 -0
- package/dist/agent.js +97 -0
- package/dist/architecture.d.ts +74 -0
- package/dist/architecture.js +105 -0
- package/dist/embedding.d.ts +60 -0
- package/dist/embedding.js +21 -0
- package/dist/embeddings/gemini.d.ts +24 -0
- package/dist/embeddings/gemini.js +71 -0
- package/dist/embeddings/index.d.ts +4 -0
- package/dist/embeddings/index.js +4 -0
- package/dist/embeddings/openai.d.ts +18 -0
- package/dist/embeddings/openai.js +27 -0
- package/dist/embeddings/openrouter.d.ts +21 -0
- package/dist/embeddings/openrouter.js +54 -0
- package/dist/events.d.ts +144 -0
- package/dist/events.js +110 -0
- package/dist/ids.d.ts +14 -0
- package/dist/ids.js +33 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/inspect/client.d.ts +2 -0
- package/dist/inspect/client.js +1326 -0
- package/dist/inspect/css.d.ts +2 -0
- package/dist/inspect/css.js +164 -0
- package/dist/inspect/index.d.ts +9 -0
- package/dist/inspect/index.js +12 -0
- package/dist/inspect/page.d.ts +3 -0
- package/dist/inspect/page.js +93 -0
- package/dist/inspect/report.d.ts +54 -0
- package/dist/inspect/report.js +62 -0
- package/dist/kernel.d.ts +178 -0
- package/dist/kernel.js +929 -0
- package/dist/memory-stores/factory.d.ts +15 -0
- package/dist/memory-stores/factory.js +29 -0
- package/dist/memory-stores/file.d.ts +37 -0
- package/dist/memory-stores/file.js +174 -0
- package/dist/memory-stores/in-memory.d.ts +17 -0
- package/dist/memory-stores/in-memory.js +80 -0
- package/dist/memory-stores/index.d.ts +4 -0
- package/dist/memory-stores/index.js +5 -0
- package/dist/memory.d.ts +115 -0
- package/dist/memory.js +213 -0
- package/dist/model.d.ts +25 -0
- package/dist/model.js +2 -0
- package/dist/models/anthropic.d.ts +36 -0
- package/dist/models/anthropic.js +293 -0
- package/dist/models/factory.d.ts +202 -0
- package/dist/models/factory.js +533 -0
- package/dist/models/gemini.d.ts +45 -0
- package/dist/models/gemini.js +368 -0
- package/dist/models/index.d.ts +7 -0
- package/dist/models/index.js +7 -0
- package/dist/models/openai-chat.d.ts +19 -0
- package/dist/models/openai-chat.js +203 -0
- package/dist/models/openai-responses.d.ts +31 -0
- package/dist/models/openai-responses.js +258 -0
- package/dist/models/openrouter.d.ts +44 -0
- package/dist/models/openrouter.js +230 -0
- package/dist/payload-stores/factory.d.ts +15 -0
- package/dist/payload-stores/factory.js +30 -0
- package/dist/payload-stores/file.d.ts +29 -0
- package/dist/payload-stores/file.js +112 -0
- package/dist/payload-stores/in-memory.d.ts +17 -0
- package/dist/payload-stores/in-memory.js +40 -0
- package/dist/payload-stores/index.d.ts +4 -0
- package/dist/payload-stores/index.js +5 -0
- package/dist/payload.d.ts +89 -0
- package/dist/payload.js +181 -0
- package/dist/project/config.d.ts +302 -0
- package/dist/project/config.js +290 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +4 -0
- package/dist/project/load.d.ts +142 -0
- package/dist/project/load.js +430 -0
- package/dist/project/refs.d.ts +23 -0
- package/dist/project/refs.js +70 -0
- package/dist/prompt.d.ts +66 -0
- package/dist/prompt.js +62 -0
- package/dist/runner.d.ts +103 -0
- package/dist/runner.js +537 -0
- package/dist/services.d.ts +23 -0
- package/dist/services.js +44 -0
- package/dist/skill-providers/factory.d.ts +16 -0
- package/dist/skill-providers/factory.js +26 -0
- package/dist/skill-providers/file.d.ts +44 -0
- package/dist/skill-providers/file.js +209 -0
- package/dist/skill-providers/index.d.ts +4 -0
- package/dist/skill-providers/index.js +5 -0
- package/dist/skill-providers/static.d.ts +13 -0
- package/dist/skill-providers/static.js +47 -0
- package/dist/skills.d.ts +102 -0
- package/dist/skills.js +232 -0
- package/dist/state.d.ts +86 -0
- package/dist/state.js +49 -0
- package/dist/tools/exa.d.ts +23 -0
- package/dist/tools/exa.js +389 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/sandbox.d.ts +172 -0
- package/dist/tools/sandbox.js +602 -0
- package/dist/tools/workspace.d.ts +53 -0
- package/dist/tools/workspace.js +1034 -0
- package/dist/trajectory.d.ts +213 -0
- package/dist/trajectory.js +232 -0
- package/dist/types.d.ts +195 -0
- package/dist/types.js +134 -0
- package/package.json +98 -0
|
@@ -0,0 +1,1326 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The page's brain
|
|
3
|
+
//
|
|
4
|
+
// Written without template literals so that nothing here has to be escaped
|
|
5
|
+
// when it is embedded, and so the file stays greppable.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const CLIENT = `
|
|
8
|
+
const DATA = JSON.parse(document.getElementById('run-data').textContent);
|
|
9
|
+
const STATE = DATA.state;
|
|
10
|
+
const BLOBS = DATA.blobs;
|
|
11
|
+
const MEDIA = DATA.media || [];
|
|
12
|
+
const CUT = new Set(DATA.truncated || []);
|
|
13
|
+
|
|
14
|
+
// --- payload access -------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function blob(ref) {
|
|
17
|
+
if (!ref || typeof ref.sha256 !== 'string') return '';
|
|
18
|
+
const v = BLOBS[ref.sha256];
|
|
19
|
+
if (v === undefined) return '[payload not resolved: ' + ref.sha256.slice(0, 12) + ']';
|
|
20
|
+
return CUT.has(ref.sha256) ? v + '\\n\\n[… truncated, ' + ref.size + ' bytes total]' : v;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pretty(raw) {
|
|
24
|
+
try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (e) { return raw; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- flatten the trajectory ----------------------------------------------
|
|
28
|
+
|
|
29
|
+
const ENTRIES = [];
|
|
30
|
+
const BY_KEY = new Map();
|
|
31
|
+
|
|
32
|
+
function covered(nodes) {
|
|
33
|
+
const out = new Set();
|
|
34
|
+
for (const n of nodes) if (n.type === 'compaction') for (const id of n.covers) out.add(id);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function flatten(nodes, depth, branch) {
|
|
39
|
+
const hidden = covered(nodes);
|
|
40
|
+
const level = [];
|
|
41
|
+
for (const node of nodes) {
|
|
42
|
+
const e = {
|
|
43
|
+
key: '', node: node, depth: depth, branch: branch,
|
|
44
|
+
covered: hidden.has(node.id), children: []
|
|
45
|
+
};
|
|
46
|
+
// A join's branches ran between the fork and the join, so they are
|
|
47
|
+
// numbered and listed there — which means recursing before this entry
|
|
48
|
+
// takes its own number.
|
|
49
|
+
if (node.type === 'join') {
|
|
50
|
+
for (const b of node.branches) e.children.push({ name: b.name, entries: flatten(b.nodes, depth + 1, b.name) });
|
|
51
|
+
}
|
|
52
|
+
e.key = 'n' + ENTRIES.length;
|
|
53
|
+
ENTRIES.push(e); BY_KEY.set(e.key, e); level.push(e);
|
|
54
|
+
}
|
|
55
|
+
return level;
|
|
56
|
+
}
|
|
57
|
+
const ROOT = flatten(STATE.trajectory, 0, null);
|
|
58
|
+
|
|
59
|
+
// --- labels ---------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function names(list) { return list.join(', '); }
|
|
62
|
+
|
|
63
|
+
function base(path) { return path.split('/').pop() || path; }
|
|
64
|
+
|
|
65
|
+
function label(n) {
|
|
66
|
+
switch (n.type) {
|
|
67
|
+
case 'user_input': return n.synthetic ? 'user input (synthetic)' : 'user input';
|
|
68
|
+
case 'system_prompt': return 'system prompt'
|
|
69
|
+
+ (n.sources && n.sources.length ? ': ' + names(n.sources.map(function (s) { return base(s.path); })) : '');
|
|
70
|
+
case 'load_skills': return 'skills: ' + names(n.skills.map(function (s) { return s.name; }));
|
|
71
|
+
case 'memory_recall': return 'memory recall: ' + n.scope;
|
|
72
|
+
case 'memory_op': return 'memory ' + n.op;
|
|
73
|
+
case 'llm_call': return 'llm: ' + n.model + (n.toolCalls.length
|
|
74
|
+
? ' -> ' + names(n.toolCalls.map(function (c) { return c.name; })) : '');
|
|
75
|
+
case 'tool_call': return 'call: ' + n.name;
|
|
76
|
+
case 'tool_result': return 'result: ' + n.name + (n.isError ? ' (error)' : '');
|
|
77
|
+
case 'handoff': return 'handoff: ' + n.from + ' -> ' + n.to;
|
|
78
|
+
case 'fork': return 'fork: ' + names(n.branches.map(function (b) { return b.name; }));
|
|
79
|
+
case 'join': return 'join: ' + names(n.branches.map(function (b) { return b.name + '=' + b.status; }));
|
|
80
|
+
case 'compaction': return 'compaction: ' + n.reason + ' (' + n.covers.length + ' nodes)';
|
|
81
|
+
case 'final_output': return 'final output';
|
|
82
|
+
default: return n.type;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function preview(n) {
|
|
87
|
+
switch (n.type) {
|
|
88
|
+
case 'user_input': {
|
|
89
|
+
const t = n.content.find(function (p) { return p.type === 'text'; });
|
|
90
|
+
return t ? blob(t.text) : '';
|
|
91
|
+
}
|
|
92
|
+
case 'system_prompt': return blob(n.prompt);
|
|
93
|
+
case 'llm_call': return blob(n.text);
|
|
94
|
+
case 'tool_call': return blob(n.args);
|
|
95
|
+
case 'tool_result': return blob(n.result);
|
|
96
|
+
case 'compaction': return blob(n.summary);
|
|
97
|
+
case 'final_output': return blob(n.output);
|
|
98
|
+
case 'memory_recall': return blob(n.content);
|
|
99
|
+
case 'load_skills': return blob(n.content);
|
|
100
|
+
default: return '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- DOM helpers ----------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function el(tag, cls, text) {
|
|
107
|
+
const e = document.createElement(tag);
|
|
108
|
+
if (cls) e.className = cls;
|
|
109
|
+
if (text !== undefined && text !== null) e.textContent = String(text);
|
|
110
|
+
return e;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function block(title, tag) {
|
|
114
|
+
const s = el('section', 'blk');
|
|
115
|
+
const h = el('h3', null, title);
|
|
116
|
+
if (tag) h.appendChild(el('span', 'tag', tag));
|
|
117
|
+
s.appendChild(h);
|
|
118
|
+
return s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function textBlock(title, body, tag) {
|
|
122
|
+
const s = block(title, tag);
|
|
123
|
+
if (body) s.appendChild(el('pre', 'text', body));
|
|
124
|
+
else s.appendChild(el('div', 'empty', '(empty)'));
|
|
125
|
+
return s;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function foldBlock(title, body) {
|
|
129
|
+
const s = block(title);
|
|
130
|
+
const d = document.createElement('details');
|
|
131
|
+
d.appendChild(el('summary', null, 'show'));
|
|
132
|
+
d.appendChild(el('pre', null, body));
|
|
133
|
+
s.appendChild(d);
|
|
134
|
+
return s;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// An <img> never runs script, but the src is still pinned to shapes we
|
|
138
|
+
// understand: a raster data: uri (svg is excluded on principle) or an http url.
|
|
139
|
+
const IMG_SUBTYPES = ['png', 'jpeg', 'jpg', 'gif', 'webp', 'avif', 'bmp'];
|
|
140
|
+
|
|
141
|
+
/** Undoes the report's media hoisting; anything else passes through. */
|
|
142
|
+
function mediaUrl(u) {
|
|
143
|
+
if (typeof u !== 'string' || u.indexOf('media:') !== 0) return u;
|
|
144
|
+
const v = MEDIA[Number(u.slice(6))];
|
|
145
|
+
return v === undefined ? u : v;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function imageSrc(p) {
|
|
149
|
+
if (p.type !== 'image') return null;
|
|
150
|
+
const u = mediaUrl(p.url);
|
|
151
|
+
if (typeof u !== 'string') return null;
|
|
152
|
+
if (u.indexOf('https://') === 0 || u.indexOf('http://') === 0) return u;
|
|
153
|
+
if (u.indexOf('data:image/') !== 0) return null;
|
|
154
|
+
const sub = u.slice(11).split(/[;,]/)[0].toLowerCase();
|
|
155
|
+
return IMG_SUBTYPES.indexOf(sub) < 0 ? null : u;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The rendered picture, or null when the part is not one we will inline. */
|
|
159
|
+
function imageEl(p, thumb) {
|
|
160
|
+
const src = imageSrc(p);
|
|
161
|
+
if (!src) return null;
|
|
162
|
+
const img = el('img', thumb ? 'media thumb' : 'media');
|
|
163
|
+
img.src = src;
|
|
164
|
+
img.alt = p.type + (p.mimeType ? ' ' + p.mimeType : '');
|
|
165
|
+
img.loading = 'lazy';
|
|
166
|
+
if (thumb) {
|
|
167
|
+
img.title = 'click to enlarge';
|
|
168
|
+
img.addEventListener('click', function () { img.classList.toggle('thumb'); });
|
|
169
|
+
}
|
|
170
|
+
return img;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function mediaBlock(title, p) {
|
|
174
|
+
const tag = p.type + (p.mimeType ? ' · ' + p.mimeType : '');
|
|
175
|
+
const src = imageSrc(p);
|
|
176
|
+
const img = imageEl(p, false);
|
|
177
|
+
if (!img) return textBlock(title, mediaUrl(p.url), tag);
|
|
178
|
+
const s = block(title, tag);
|
|
179
|
+
s.appendChild(img);
|
|
180
|
+
// The bytes stay reachable; they just no longer own the screen.
|
|
181
|
+
const d = document.createElement('details');
|
|
182
|
+
d.appendChild(el('summary', null, 'source'));
|
|
183
|
+
d.appendChild(el('pre', null, src));
|
|
184
|
+
s.appendChild(d);
|
|
185
|
+
return s;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// A titled container for several blocks. Returns the element to append and the
|
|
189
|
+
// body to fill, so callers never have to know the internal structure.
|
|
190
|
+
function group(title, kind, tag) {
|
|
191
|
+
const s = el('section', 'group ' + kind);
|
|
192
|
+
const h = el('div', 'ghdr', title);
|
|
193
|
+
if (tag) h.appendChild(el('span', 'tag', tag));
|
|
194
|
+
s.appendChild(h);
|
|
195
|
+
const body = el('div', 'body');
|
|
196
|
+
s.appendChild(body);
|
|
197
|
+
return { el: s, body: body };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stat(key, value) {
|
|
201
|
+
const d = el('span', 'stat', key + ' ');
|
|
202
|
+
d.appendChild(el('b', null, value));
|
|
203
|
+
return d;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- header ---------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
function kv(key, value) {
|
|
209
|
+
const d = el('div', 'kv');
|
|
210
|
+
d.appendChild(document.createTextNode(key + ' '));
|
|
211
|
+
d.appendChild(el('b', null, value));
|
|
212
|
+
return d;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
(function head() {
|
|
216
|
+
const h = document.getElementById('head');
|
|
217
|
+
const u = STATE.usage || {};
|
|
218
|
+
const llm = ENTRIES.filter(function (e) { return e.node.type === 'llm_call'; }).length;
|
|
219
|
+
h.appendChild(el('h1', null, DATA.title));
|
|
220
|
+
h.appendChild(kv('run', STATE.runId));
|
|
221
|
+
h.appendChild(kv('agent', STATE.agentName));
|
|
222
|
+
h.appendChild(kv('phase', STATE.phase));
|
|
223
|
+
h.appendChild(kv('nodes', ENTRIES.length));
|
|
224
|
+
h.appendChild(kv('llm calls', llm));
|
|
225
|
+
h.appendChild(kv('tokens', (u.inputTokens || 0) + ' in / ' + (u.outputTokens || 0) + ' out'));
|
|
226
|
+
if (STATE.error) h.appendChild(kv('error', STATE.error));
|
|
227
|
+
h.appendChild(kv('generated', DATA.generatedAt));
|
|
228
|
+
})();
|
|
229
|
+
|
|
230
|
+
// --- timing ---------------------------------------------------------------
|
|
231
|
+
//
|
|
232
|
+
// A node is stamped when it is appended, which is when the work behind it
|
|
233
|
+
// finished. So the interval between a node and the one before it in the same
|
|
234
|
+
// lane *is* that work: an llm_call's interval is the model latency, a
|
|
235
|
+
// tool_result's is the tool. That gives every node a start and a duration
|
|
236
|
+
// without storing either.
|
|
237
|
+
//
|
|
238
|
+
// Offsets are counted from the start of the turn, not of the run: in a
|
|
239
|
+
// follow-up the interesting question is "how long did this answer take", and
|
|
240
|
+
// counting from a conversation that began an hour ago answers nothing. A trunk
|
|
241
|
+
// user_input opens a new turn; a branch inherits the turn that forked it, so
|
|
242
|
+
// ten branches starting at one offset still read as parallel.
|
|
243
|
+
|
|
244
|
+
function stampOf(node) {
|
|
245
|
+
const t = Date.parse(node.ts);
|
|
246
|
+
return isNaN(t) ? null : t;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
(function time(level, lanePrev, turnStart) {
|
|
250
|
+
let prev = lanePrev;
|
|
251
|
+
let t0 = turnStart;
|
|
252
|
+
level.forEach(function (e) {
|
|
253
|
+
const ts = stampOf(e.node);
|
|
254
|
+
// A real user turn resets the clock and starts at itself: the gap since
|
|
255
|
+
// the previous run is waiting, not work. Synthetic inputs (a branch seed,
|
|
256
|
+
// a re-injection) are not turns — same rule the kernel uses.
|
|
257
|
+
const opensTurn = !e.branch && e.node.type === 'user_input' && !e.node.synthetic;
|
|
258
|
+
if (opensTurn) t0 = ts;
|
|
259
|
+
e.turnStart = t0 === null ? ts : t0;
|
|
260
|
+
e.startMs = opensTurn || prev === null ? ts : prev;
|
|
261
|
+
e.endMs = ts;
|
|
262
|
+
// Branches run between the fork and the join, so they start where the
|
|
263
|
+
// trunk left off — and the join's own duration is the whole parallel
|
|
264
|
+
// section it waited on.
|
|
265
|
+
if (e.node.type === 'join') {
|
|
266
|
+
let slowest = null;
|
|
267
|
+
e.children.forEach(function (br) {
|
|
268
|
+
time(br.entries, prev, e.turnStart);
|
|
269
|
+
// The branch as a whole gets the same shape as a node — start, end,
|
|
270
|
+
// turn — so a reader sees how long *it* ran instead of having to
|
|
271
|
+
// subtract two offsets to find out that ten branches were parallel.
|
|
272
|
+
const last = br.entries[br.entries.length - 1];
|
|
273
|
+
br.turnStart = e.turnStart;
|
|
274
|
+
br.startMs = prev;
|
|
275
|
+
br.endMs = last ? last.endMs : prev;
|
|
276
|
+
if (br.entries.length) br.entries[0].branchSpan = br;
|
|
277
|
+
// The join costs exactly its slowest branch, so that branch is the
|
|
278
|
+
// only one worth optimizing: it gets marked and rendered lighter.
|
|
279
|
+
if (br.entries.length && (!slowest || br.endMs > slowest.endMs)) slowest = br;
|
|
280
|
+
});
|
|
281
|
+
if (slowest) slowest.slowest = true;
|
|
282
|
+
}
|
|
283
|
+
prev = ts;
|
|
284
|
+
});
|
|
285
|
+
})(ROOT, null, null);
|
|
286
|
+
|
|
287
|
+
function fmtSecs(ms) {
|
|
288
|
+
const s = ms / 1000;
|
|
289
|
+
if (s < 100) return s.toFixed(1) + 's';
|
|
290
|
+
const m = Math.floor(s / 60);
|
|
291
|
+
return m + 'm' + String(Math.floor(s - m * 60)).padStart(2, '0');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Offset of a node's start from the beginning of its turn. */
|
|
295
|
+
function startedAt(e) {
|
|
296
|
+
if (e.startMs === null || e.turnStart === null) return '';
|
|
297
|
+
return fmtSecs(e.startMs - e.turnStart);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** How long the work behind a node took. Blank when it is not worth a number. */
|
|
301
|
+
function took(e) {
|
|
302
|
+
if (e.startMs === null || e.endMs === null) return '';
|
|
303
|
+
const ms = e.endMs - e.startMs;
|
|
304
|
+
if (ms < 10) return '';
|
|
305
|
+
return fmtDur(ms);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function fmtDur(ms) {
|
|
309
|
+
return ms < 1000 ? Math.round(ms) + 'ms' : fmtSecs(ms);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function spanMs(e) {
|
|
313
|
+
return e.startMs === null || e.endMs === null ? 0 : e.endMs - e.startMs;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// One colour per branch, stable for the life of the page. The trunk keeps the
|
|
317
|
+
// neutral accent so branches are the thing that stands out.
|
|
318
|
+
const PALETTE = ['#6ea8fe', '#4ec9a0', '#e2b341', '#d1a6ff', '#f0776c',
|
|
319
|
+
'#5fd0d8', '#f09a5c', '#a3d160', '#ef8ec0', '#9aa4ff'];
|
|
320
|
+
const BRANCH_COLOR = new Map();
|
|
321
|
+
function branchColor(name) {
|
|
322
|
+
if (!name) return null;
|
|
323
|
+
if (!BRANCH_COLOR.has(name)) BRANCH_COLOR.set(name, PALETTE[BRANCH_COLOR.size % PALETTE.length]);
|
|
324
|
+
return BRANCH_COLOR.get(name);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- timeline -------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
const listEl = document.getElementById('timeline');
|
|
330
|
+
let lastBranch = false;
|
|
331
|
+
let lastStamp = null;
|
|
332
|
+
|
|
333
|
+
ENTRIES.forEach(function (e, i) {
|
|
334
|
+
// Branch histories are contiguous in the flattened order, so a change of
|
|
335
|
+
// branch is a group boundary: label it and restart the elapsed column.
|
|
336
|
+
if (e.branch !== lastBranch) {
|
|
337
|
+
const sep = el('li', 'sep');
|
|
338
|
+
const color = branchColor(e.branch);
|
|
339
|
+
if (color) sep.style.setProperty('--bc', color);
|
|
340
|
+
sep.appendChild(el('span', 'dot'));
|
|
341
|
+
sep.appendChild(el('span', null, e.branch ? 'branch · ' + e.branch : 'trunk'));
|
|
342
|
+
if (e.branchSpan) {
|
|
343
|
+
const bd = took(e.branchSpan);
|
|
344
|
+
if (e.branchSpan.slowest) sep.classList.add('slow');
|
|
345
|
+
if (bd) {
|
|
346
|
+
sep.appendChild(el('span', 'dur',
|
|
347
|
+
't+' + startedAt(e.branchSpan) + ' · ' + bd + (e.branchSpan.slowest ? ' · slowest' : '')));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
listEl.appendChild(sep);
|
|
351
|
+
lastBranch = e.branch;
|
|
352
|
+
lastStamp = null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const li = el('li', e.branch ? 'branch' : '');
|
|
356
|
+
if (e.covered) li.classList.add('covered');
|
|
357
|
+
const color = branchColor(e.branch);
|
|
358
|
+
if (color) li.style.setProperty('--bc', color);
|
|
359
|
+
li.dataset.key = e.key;
|
|
360
|
+
|
|
361
|
+
const row = el('div', 'row');
|
|
362
|
+
// Consecutive nodes often start at the same instant (a tool call is recorded
|
|
363
|
+
// the moment the response carrying it is applied). Repeating the offset
|
|
364
|
+
// would only bury the moments where time actually moved.
|
|
365
|
+
const at = startedAt(e);
|
|
366
|
+
row.appendChild(el('span', 't' + (at === lastStamp ? ' same' : ''), at === lastStamp ? '·' : at));
|
|
367
|
+
lastStamp = at;
|
|
368
|
+
row.appendChild(el('span', 'idx', '#' + (i + 1)));
|
|
369
|
+
const badge = el('span', 'type ' + e.node.type
|
|
370
|
+
+ (e.node.type === 'tool_result' && e.node.isError ? ' err' : ''), e.node.type.replace(/_/g, ' '));
|
|
371
|
+
row.appendChild(badge);
|
|
372
|
+
const d = took(e);
|
|
373
|
+
if (d) row.appendChild(el('span', 'dur', d));
|
|
374
|
+
li.appendChild(row);
|
|
375
|
+
li.appendChild(el('div', 'prev', label(e.node)
|
|
376
|
+
+ (preview(e.node) ? ' — ' + preview(e.node).slice(0, 90) : '')));
|
|
377
|
+
li.addEventListener('click', function () { select(e.key, true); });
|
|
378
|
+
listEl.appendChild(li);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// A group header is only worth showing while something in its group is.
|
|
382
|
+
function applyFilter(q) {
|
|
383
|
+
const rows = Array.prototype.slice.call(listEl.children);
|
|
384
|
+
rows.forEach(function (li) {
|
|
385
|
+
if (li.classList.contains('sep')) return;
|
|
386
|
+
const e = BY_KEY.get(li.dataset.key);
|
|
387
|
+
const hay = (label(e.node) + ' ' + e.node.type + ' ' + (e.branch || '')
|
|
388
|
+
+ ' ' + preview(e.node)).toLowerCase();
|
|
389
|
+
li.hidden = q.length > 0 && hay.indexOf(q) === -1;
|
|
390
|
+
});
|
|
391
|
+
let sep = null, kept = false;
|
|
392
|
+
rows.forEach(function (li) {
|
|
393
|
+
if (li.classList.contains('sep')) {
|
|
394
|
+
if (sep) sep.hidden = !kept;
|
|
395
|
+
sep = li; kept = false;
|
|
396
|
+
} else if (!li.hidden) kept = true;
|
|
397
|
+
});
|
|
398
|
+
if (sep) sep.hidden = !kept;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
document.getElementById('filter').addEventListener('input', function (ev) {
|
|
402
|
+
applyFilter(ev.target.value.toLowerCase());
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// --- detail ---------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
function message(m) {
|
|
408
|
+
const wrap = el('div', 'msg ' + m.role + (m.isError ? ' err' : ''));
|
|
409
|
+
const role = el('div', 'role');
|
|
410
|
+
role.appendChild(el('span', null, m.role + (m.agent ? ' · ' + m.agent : '')
|
|
411
|
+
+ (m.name ? ' · ' + m.name : '')));
|
|
412
|
+
if (m.callId) role.appendChild(el('span', 'badge', m.callId));
|
|
413
|
+
wrap.appendChild(role);
|
|
414
|
+
|
|
415
|
+
if (m.role === 'user' && Array.isArray(m.content)) {
|
|
416
|
+
m.content.forEach(function (p) {
|
|
417
|
+
if (p.type === 'text') { wrap.appendChild(el('pre', 'text', p.text)); return; }
|
|
418
|
+
const img = imageEl(p, true);
|
|
419
|
+
wrap.appendChild(img || el('pre', 'text', '[' + p.type + '] ' + mediaUrl(p.url)));
|
|
420
|
+
});
|
|
421
|
+
} else if (typeof m.content === 'string' && m.content) {
|
|
422
|
+
wrap.appendChild(el('pre', 'text', m.role === 'tool' ? pretty(m.content) : m.content));
|
|
423
|
+
} else if (!m.toolCalls) {
|
|
424
|
+
wrap.appendChild(el('div', 'empty', '(no content)'));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
(m.toolCalls || []).forEach(function (c) {
|
|
428
|
+
const box = el('div', 'call');
|
|
429
|
+
box.appendChild(el('div', 'name', c.name + '(' + c.id + ')'));
|
|
430
|
+
box.appendChild(el('pre', null, pretty(c.args)));
|
|
431
|
+
wrap.appendChild(box);
|
|
432
|
+
});
|
|
433
|
+
return wrap;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function requestBlocks(raw, into) {
|
|
437
|
+
let req;
|
|
438
|
+
try { req = JSON.parse(raw); } catch (e) {
|
|
439
|
+
into.appendChild(textBlock('Request (unparsed)', raw));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (req.system) into.appendChild(textBlock('System prompt', req.system)).classList.add('sys');
|
|
443
|
+
|
|
444
|
+
const conv = block('Messages', (req.messages || []).length + ' messages');
|
|
445
|
+
(req.messages || []).forEach(function (m) { conv.appendChild(message(m)); });
|
|
446
|
+
if (!(req.messages || []).length) conv.appendChild(el('div', 'empty', '(none)'));
|
|
447
|
+
into.appendChild(conv);
|
|
448
|
+
|
|
449
|
+
const tools = block('Tools offered', (req.tools || []).length + ' tools');
|
|
450
|
+
(req.tools || []).forEach(function (t) {
|
|
451
|
+
const d = document.createElement('details');
|
|
452
|
+
d.appendChild(el('summary', null, t.name + (t.description ? ' — ' + t.description : '')));
|
|
453
|
+
d.appendChild(el('pre', null, JSON.stringify(t.parameters, null, 2)));
|
|
454
|
+
tools.appendChild(d);
|
|
455
|
+
});
|
|
456
|
+
if (!(req.tools || []).length) tools.appendChild(el('div', 'empty', '(none)'));
|
|
457
|
+
into.appendChild(tools);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function usageTag(u) {
|
|
461
|
+
if (!u) return '';
|
|
462
|
+
return (u.inputTokens || 0) + ' in (' + (u.cachedInputTokens || 0) + ' cached) / '
|
|
463
|
+
+ (u.outputTokens || 0) + ' out (' + (u.reasoningTokens || 0) + ' reasoning)';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function detailFor(e) {
|
|
467
|
+
const n = e.node;
|
|
468
|
+
const out = document.createDocumentFragment();
|
|
469
|
+
|
|
470
|
+
const h = el('h2', 'dt');
|
|
471
|
+
h.appendChild(el('span', 'type ' + n.type, n.type.replace(/_/g, ' ')));
|
|
472
|
+
h.appendChild(el('span', null, label(n)));
|
|
473
|
+
out.appendChild(h);
|
|
474
|
+
|
|
475
|
+
const meta = el('div', 'meta');
|
|
476
|
+
meta.appendChild(el('span', null, 'id ' + n.id));
|
|
477
|
+
meta.appendChild(el('span', null, 'agent ' + n.agent));
|
|
478
|
+
meta.appendChild(el('span', null, n.ts));
|
|
479
|
+
meta.appendChild(el('span', null, 'started t+' + startedAt(e) + (took(e) ? ' · took ' + took(e) : '')));
|
|
480
|
+
if (e.branch) {
|
|
481
|
+
const br = el('span', null, 'branch ' + e.branch);
|
|
482
|
+
br.style.color = branchColor(e.branch);
|
|
483
|
+
meta.appendChild(br);
|
|
484
|
+
}
|
|
485
|
+
if (e.covered) meta.appendChild(el('span', null, 'hidden by a later compaction'));
|
|
486
|
+
out.appendChild(meta);
|
|
487
|
+
|
|
488
|
+
switch (n.type) {
|
|
489
|
+
case 'system_prompt':
|
|
490
|
+
out.appendChild(textBlock('Prompt', blob(n.prompt)));
|
|
491
|
+
// The editable half: which file to open when the instruction is wrong.
|
|
492
|
+
(n.sources || []).forEach(function (s) {
|
|
493
|
+
out.appendChild(textBlock(s.path, blob(s.content), 'prompt file'));
|
|
494
|
+
});
|
|
495
|
+
break;
|
|
496
|
+
|
|
497
|
+
case 'user_input':
|
|
498
|
+
n.content.forEach(function (p, i) {
|
|
499
|
+
out.appendChild(p.type === 'text'
|
|
500
|
+
? textBlock('Text part ' + (i + 1), blob(p.text))
|
|
501
|
+
: mediaBlock('Media part ' + (i + 1), p));
|
|
502
|
+
});
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case 'load_skills':
|
|
506
|
+
out.appendChild(textBlock('Skills', n.skills.map(function (s) {
|
|
507
|
+
return s.name + (s.version ? '@' + s.version : '') + ' ' + s.contentHash.slice(0, 12)
|
|
508
|
+
+ (s.file ? ' ' + s.file : '');
|
|
509
|
+
}).join('\\n'), n.provider));
|
|
510
|
+
out.appendChild(textBlock('Injected instructions', blob(n.content)));
|
|
511
|
+
out.appendChild(textBlock('Tools unlocked', n.toolNames.join('\\n')));
|
|
512
|
+
break;
|
|
513
|
+
|
|
514
|
+
case 'memory_recall':
|
|
515
|
+
out.appendChild(textBlock('Query', JSON.stringify(n.query, null, 2), n.store + ' · ' + n.scope));
|
|
516
|
+
out.appendChild(textBlock('Hits', n.hits.map(function (hit) {
|
|
517
|
+
return hit.id + ' score=' + hit.score + ' rev=' + hit.revision;
|
|
518
|
+
}).join('\\n')));
|
|
519
|
+
out.appendChild(textBlock('Injected block', blob(n.content)));
|
|
520
|
+
break;
|
|
521
|
+
|
|
522
|
+
case 'memory_op':
|
|
523
|
+
out.appendChild(textBlock('Operation',
|
|
524
|
+
n.op + ' ' + n.recordId + ' (rev ' + n.revision + ')', n.store + ' · ' + n.scope));
|
|
525
|
+
if (n.before) out.appendChild(textBlock('Before', blob(n.before)));
|
|
526
|
+
if (n.after) out.appendChild(textBlock('After', blob(n.after)));
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'llm_call': {
|
|
530
|
+
const u = n.usage || {};
|
|
531
|
+
const strip = el('div', 'stats');
|
|
532
|
+
strip.appendChild(stat('model', n.model));
|
|
533
|
+
strip.appendChild(stat('in', u.inputTokens || 0));
|
|
534
|
+
strip.appendChild(stat('cached', u.cachedInputTokens || 0));
|
|
535
|
+
strip.appendChild(stat('out', u.outputTokens || 0));
|
|
536
|
+
if (u.reasoningTokens) strip.appendChild(stat('reasoning', u.reasoningTokens));
|
|
537
|
+
strip.appendChild(stat('stop', n.stopReason || '?'));
|
|
538
|
+
out.appendChild(strip);
|
|
539
|
+
|
|
540
|
+
const req = group('Request', 'req', 'sent to ' + n.model);
|
|
541
|
+
if (n.request) requestBlocks(blob(n.request), req.body);
|
|
542
|
+
else {
|
|
543
|
+
req.body.appendChild(el('div', 'empty',
|
|
544
|
+
'not recorded — construct the runner with { recordRequests: true } to capture it'));
|
|
545
|
+
}
|
|
546
|
+
out.appendChild(req.el);
|
|
547
|
+
|
|
548
|
+
const res = group('Response', 'res', n.stopReason);
|
|
549
|
+
if (n.thinking) res.body.appendChild(textBlock('Reasoning', blob(n.thinking)));
|
|
550
|
+
res.body.appendChild(textBlock('Text', blob(n.text)));
|
|
551
|
+
n.toolCalls.forEach(function (c) {
|
|
552
|
+
res.body.appendChild(textBlock('Tool call · ' + c.name, pretty(blob(c.args)), c.callId));
|
|
553
|
+
});
|
|
554
|
+
out.appendChild(res.el);
|
|
555
|
+
|
|
556
|
+
out.appendChild(textBlock('Request digest', n.requestDigest || '(not recorded)'));
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
case 'tool_call':
|
|
561
|
+
out.appendChild(textBlock('Arguments', pretty(blob(n.args)), n.name + ' · ' + n.callId));
|
|
562
|
+
break;
|
|
563
|
+
|
|
564
|
+
case 'tool_result':
|
|
565
|
+
out.appendChild(textBlock(n.isError ? 'Error result' : 'Result',
|
|
566
|
+
pretty(blob(n.result)),
|
|
567
|
+
n.name + (n.durationMs !== undefined ? ' · ' + n.durationMs + 'ms' : '')));
|
|
568
|
+
break;
|
|
569
|
+
|
|
570
|
+
case 'handoff':
|
|
571
|
+
out.appendChild(textBlock('Handoff', n.from + ' -> ' + n.to
|
|
572
|
+
+ (n.reason ? '\\n\\nreason: ' + n.reason : '')));
|
|
573
|
+
break;
|
|
574
|
+
|
|
575
|
+
case 'fork':
|
|
576
|
+
n.branches.forEach(function (b) {
|
|
577
|
+
out.appendChild(textBlock('Branch · ' + b.name, blob(b.instructions),
|
|
578
|
+
b.agent + ' · ' + n.contextMode + ' · ' + b.childRunId));
|
|
579
|
+
});
|
|
580
|
+
break;
|
|
581
|
+
|
|
582
|
+
case 'join':
|
|
583
|
+
n.branches.forEach(function (b, i) {
|
|
584
|
+
const br = e.children[i];
|
|
585
|
+
const bd = br ? took(br) : '';
|
|
586
|
+
out.appendChild(textBlock('Branch · ' + b.name,
|
|
587
|
+
b.status === 'ok' ? blob(b.output) : (b.error || blob(b.output)),
|
|
588
|
+
b.status + ' · ' + b.agent + ' · ' + b.nodes.length + ' nodes'
|
|
589
|
+
+ (bd ? ' · ' + bd : '') + ' · ' + usageTag(b.usage)));
|
|
590
|
+
});
|
|
591
|
+
break;
|
|
592
|
+
|
|
593
|
+
case 'compaction': {
|
|
594
|
+
out.appendChild(textBlock('Summary', blob(n.summary), n.reason));
|
|
595
|
+
const cov = block('Replaces', n.covers.length + ' nodes');
|
|
596
|
+
const line = el('div', 'empty', '');
|
|
597
|
+
line.textContent = '';
|
|
598
|
+
n.covers.forEach(function (id) {
|
|
599
|
+
const hit = ENTRIES.find(function (x) { return x.node.id === id; });
|
|
600
|
+
const b = el('button', 'link', hit ? label(hit.node) : id);
|
|
601
|
+
if (hit) b.addEventListener('click', function () { select(hit.key, true); });
|
|
602
|
+
line.appendChild(b);
|
|
603
|
+
});
|
|
604
|
+
cov.appendChild(line);
|
|
605
|
+
out.appendChild(cov);
|
|
606
|
+
out.appendChild(textBlock('Summarizer usage', usageTag(n.usage)));
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
case 'final_output':
|
|
611
|
+
out.appendChild(textBlock('Output', blob(n.output)));
|
|
612
|
+
if (n.parsed !== undefined) out.appendChild(textBlock('Parsed', JSON.stringify(n.parsed, null, 2)));
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
out.appendChild(foldBlock('Raw node', JSON.stringify(n, null, 2)));
|
|
617
|
+
return out;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const detailEl = document.getElementById('detail');
|
|
621
|
+
const statsEl = document.getElementById('stats');
|
|
622
|
+
let selected = null;
|
|
623
|
+
|
|
624
|
+
function select(key, focus) {
|
|
625
|
+
const e = BY_KEY.get(key);
|
|
626
|
+
if (!e) return;
|
|
627
|
+
selected = key;
|
|
628
|
+
detailEl.replaceChildren(detailFor(e));
|
|
629
|
+
Array.prototype.forEach.call(listEl.children, function (li) {
|
|
630
|
+
li.classList.toggle('sel', li.dataset.key === key);
|
|
631
|
+
});
|
|
632
|
+
const li = listEl.querySelector('li[data-key="' + key + '"]');
|
|
633
|
+
if (li) li.scrollIntoView({ block: 'nearest' });
|
|
634
|
+
if (focus) show('detail');
|
|
635
|
+
detailEl.scrollTop = 0;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// --- stats ----------------------------------------------------------------
|
|
639
|
+
//
|
|
640
|
+
// Tokens are not fungible across models and seconds are not fungible across
|
|
641
|
+
// lanes: a thousand tokens cost a different amount on a different model, and
|
|
642
|
+
// branch time runs in parallel with other branch time. So every number here
|
|
643
|
+
// is broken down by model and by agent, and the wall clock sits next to the
|
|
644
|
+
// sums so the gap between them is visible instead of misleading.
|
|
645
|
+
|
|
646
|
+
function bucket() {
|
|
647
|
+
return { calls: 0, errors: 0, ms: 0, in: 0, cached: 0, out: 0, reasoning: 0 };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function bucketOf(map, key) {
|
|
651
|
+
if (!map.has(key)) map.set(key, bucket());
|
|
652
|
+
return map.get(key);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function addUsage(into, u) {
|
|
656
|
+
if (!u) return;
|
|
657
|
+
into.in += u.inputTokens || 0;
|
|
658
|
+
into.cached += u.cachedInputTokens || 0;
|
|
659
|
+
into.out += u.outputTokens || 0;
|
|
660
|
+
into.reasoning += u.reasoningTokens || 0;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function num(n) { return n.toLocaleString('en-US'); }
|
|
664
|
+
|
|
665
|
+
function table(cols, rows, foot) {
|
|
666
|
+
const t = el('table', 'st');
|
|
667
|
+
const head = document.createElement('thead');
|
|
668
|
+
const hr = document.createElement('tr');
|
|
669
|
+
cols.forEach(function (c) { hr.appendChild(el('th', null, c)); });
|
|
670
|
+
head.appendChild(hr);
|
|
671
|
+
t.appendChild(head);
|
|
672
|
+
const body = document.createElement('tbody');
|
|
673
|
+
rows.forEach(function (r) {
|
|
674
|
+
const tr = document.createElement('tr');
|
|
675
|
+
r.forEach(function (v) { tr.appendChild(el('td', null, v)); });
|
|
676
|
+
body.appendChild(tr);
|
|
677
|
+
});
|
|
678
|
+
t.appendChild(body);
|
|
679
|
+
if (foot) {
|
|
680
|
+
const tf = document.createElement('tfoot');
|
|
681
|
+
const fr = document.createElement('tr');
|
|
682
|
+
foot.forEach(function (v) { fr.appendChild(el('td', null, v)); });
|
|
683
|
+
tf.appendChild(fr);
|
|
684
|
+
t.appendChild(tf);
|
|
685
|
+
}
|
|
686
|
+
return t;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const TOKEN_COLS = ['calls', 'in', 'cached', 'out', 'reasoning', 'time', 'avg'];
|
|
690
|
+
|
|
691
|
+
function tokenRow(name, b) {
|
|
692
|
+
return [name, num(b.calls), num(b.in), num(b.cached), num(b.out), num(b.reasoning),
|
|
693
|
+
fmtDur(b.ms), b.calls ? fmtDur(b.ms / b.calls) : '—'];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function byTokens(a, b) { return (b[1].in + b[1].out) - (a[1].in + a[1].out); }
|
|
697
|
+
|
|
698
|
+
function buildStats() {
|
|
699
|
+
const byModel = new Map(), byAgent = new Map(), byTool = new Map(), byType = new Map();
|
|
700
|
+
const all = bucket();
|
|
701
|
+
let llmMs = 0, toolMs = 0, toolCalls = 0, from = null, to = null;
|
|
702
|
+
|
|
703
|
+
ENTRIES.forEach(function (e) {
|
|
704
|
+
const n = e.node;
|
|
705
|
+
byType.set(n.type, (byType.get(n.type) || 0) + 1);
|
|
706
|
+
if (e.startMs !== null && (from === null || e.startMs < from)) from = e.startMs;
|
|
707
|
+
if (e.endMs !== null && (to === null || e.endMs > to)) to = e.endMs;
|
|
708
|
+
|
|
709
|
+
// A join's usage is the sum of its branches, whose nodes are already in
|
|
710
|
+
// this list, so counting it too would double every branch token. A
|
|
711
|
+
// compaction has no model of its own but did burn a summarizer.
|
|
712
|
+
if (n.type === 'llm_call' || n.type === 'compaction') {
|
|
713
|
+
const ms = spanMs(e);
|
|
714
|
+
const model = n.type === 'compaction' ? 'summarizer' : n.model;
|
|
715
|
+
[bucketOf(byModel, model), bucketOf(byAgent, n.agent), all].forEach(function (b) {
|
|
716
|
+
addUsage(b, n.usage);
|
|
717
|
+
b.calls += 1;
|
|
718
|
+
b.ms += ms;
|
|
719
|
+
});
|
|
720
|
+
llmMs += ms;
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
// The tool's own measurement when the runner took one; the gap to the
|
|
724
|
+
// previous node otherwise.
|
|
725
|
+
if (n.type === 'tool_result') {
|
|
726
|
+
const b = bucketOf(byTool, n.name);
|
|
727
|
+
const ms = n.durationMs !== undefined ? n.durationMs : spanMs(e);
|
|
728
|
+
b.calls += 1;
|
|
729
|
+
b.ms += ms;
|
|
730
|
+
if (n.isError) b.errors += 1;
|
|
731
|
+
toolCalls += 1;
|
|
732
|
+
toolMs += ms;
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const pills = el('div', 'stats');
|
|
737
|
+
pills.appendChild(stat('wall clock', from === null ? '—' : fmtDur(to - from)));
|
|
738
|
+
pills.appendChild(stat('in llm', fmtDur(llmMs)));
|
|
739
|
+
pills.appendChild(stat('in tools', fmtDur(toolMs)));
|
|
740
|
+
pills.appendChild(stat('llm calls', num(all.calls)));
|
|
741
|
+
pills.appendChild(stat('tool calls', num(toolCalls)));
|
|
742
|
+
pills.appendChild(stat('tokens', num(all.in) + ' in / ' + num(all.out) + ' out'));
|
|
743
|
+
pills.appendChild(stat('cached', num(all.cached)));
|
|
744
|
+
pills.appendChild(stat('nodes', num(ENTRIES.length)));
|
|
745
|
+
statsEl.replaceChildren(pills);
|
|
746
|
+
|
|
747
|
+
const note = el('div', 'hint', 'Branches run in parallel, so llm and tool time are sums '
|
|
748
|
+
+ 'over every lane and add up to more than the wall clock.');
|
|
749
|
+
note.style.marginBottom = '14px';
|
|
750
|
+
statsEl.appendChild(note);
|
|
751
|
+
|
|
752
|
+
const models = Array.from(byModel.entries()).sort(byTokens);
|
|
753
|
+
const modelBlock = block('Tokens by model', models.length + (models.length === 1 ? ' model' : ' models'));
|
|
754
|
+
modelBlock.appendChild(table(['model'].concat(TOKEN_COLS),
|
|
755
|
+
models.map(function (m) { return tokenRow(m[0], m[1]); }), tokenRow('total', all)));
|
|
756
|
+
statsEl.appendChild(modelBlock);
|
|
757
|
+
|
|
758
|
+
const agents = Array.from(byAgent.entries()).sort(byTokens);
|
|
759
|
+
const agentBlock = block('Tokens by agent', agents.length + (agents.length === 1 ? ' agent' : ' agents'));
|
|
760
|
+
agentBlock.appendChild(table(['agent'].concat(TOKEN_COLS),
|
|
761
|
+
agents.map(function (a) { return tokenRow(a[0], a[1]); }), tokenRow('total', all)));
|
|
762
|
+
statsEl.appendChild(agentBlock);
|
|
763
|
+
|
|
764
|
+
const tools = Array.from(byTool.entries()).sort(function (a, b) { return b[1].ms - a[1].ms; });
|
|
765
|
+
const toolBlock = block('Tool calls', num(toolCalls) + ' calls · ' + fmtDur(toolMs));
|
|
766
|
+
if (tools.length) {
|
|
767
|
+
toolBlock.appendChild(table(['tool', 'calls', 'errors', 'time', 'avg'],
|
|
768
|
+
tools.map(function (t) {
|
|
769
|
+
const b = t[1];
|
|
770
|
+
return [t[0], num(b.calls), b.errors ? num(b.errors) : '', fmtDur(b.ms),
|
|
771
|
+
b.calls ? fmtDur(b.ms / b.calls) : '—'];
|
|
772
|
+
})));
|
|
773
|
+
} else {
|
|
774
|
+
toolBlock.appendChild(el('div', 'empty', '(none)'));
|
|
775
|
+
}
|
|
776
|
+
statsEl.appendChild(toolBlock);
|
|
777
|
+
|
|
778
|
+
const types = Array.from(byType.entries()).sort(function (a, b) { return b[1] - a[1]; });
|
|
779
|
+
const typeBlock = block('Nodes by type', num(ENTRIES.length) + ' nodes');
|
|
780
|
+
typeBlock.appendChild(table(['type', 'count'],
|
|
781
|
+
types.map(function (t) { return [t[0].replace(/_/g, ' '), num(t[1])]; })));
|
|
782
|
+
statsEl.appendChild(typeBlock);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// --- views ----------------------------------------------------------------
|
|
786
|
+
|
|
787
|
+
// Panes that hold a zoomable diagram, by view name. A pane laid out while it
|
|
788
|
+
// was hidden has no width to fit against, so becoming visible is the moment it
|
|
789
|
+
// gets a second chance.
|
|
790
|
+
const PANES = {};
|
|
791
|
+
|
|
792
|
+
function show(view) {
|
|
793
|
+
document.getElementById('graph').hidden = view !== 'graph';
|
|
794
|
+
document.getElementById('agents').hidden = view !== 'agents';
|
|
795
|
+
detailEl.hidden = view !== 'detail';
|
|
796
|
+
statsEl.hidden = view !== 'stats';
|
|
797
|
+
Array.prototype.forEach.call(document.getElementById('viewtabs').children, function (b) {
|
|
798
|
+
b.classList.toggle('on', b.dataset.view === view);
|
|
799
|
+
});
|
|
800
|
+
if (PANES[view]) PANES[view].refit();
|
|
801
|
+
}
|
|
802
|
+
Array.prototype.forEach.call(document.getElementById('viewtabs').children, function (b) {
|
|
803
|
+
b.addEventListener('click', function () { show(b.dataset.view); });
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// --- zoom and pan ---------------------------------------------------------
|
|
807
|
+
//
|
|
808
|
+
// A diagram grows with what it describes, so a fixed scale is useless past a
|
|
809
|
+
// dozen nodes. The canvas is moved with a CSS transform rather than scrolled:
|
|
810
|
+
// that keeps zooming anchored on the pointer and costs no layout.
|
|
811
|
+
//
|
|
812
|
+
// Written as a factory because there are two of these — the run graph and the
|
|
813
|
+
// architecture — and they must not share a scale, a pan or a drag.
|
|
814
|
+
|
|
815
|
+
const MIN_ZOOM = 0.1;
|
|
816
|
+
const MAX_ZOOM = 4;
|
|
817
|
+
|
|
818
|
+
function panZoom(paneId) {
|
|
819
|
+
const pane = document.getElementById(paneId);
|
|
820
|
+
const viewportEl = pane.querySelector('.viewport');
|
|
821
|
+
const canvasEl = pane.querySelector('.canvas');
|
|
822
|
+
const zoomEl = pane.querySelector('.lvl');
|
|
823
|
+
let zoom = 1, panX = 0, panY = 0;
|
|
824
|
+
let natW = 0, natH = 0;
|
|
825
|
+
let pendingFit = false;
|
|
826
|
+
|
|
827
|
+
function applyView() {
|
|
828
|
+
canvasEl.style.transform = 'translate(' + panX + 'px,' + panY + 'px) scale(' + zoom + ')';
|
|
829
|
+
zoomEl.textContent = Math.round(zoom * 100) + '%';
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function zoomAt(factor, cx, cy) {
|
|
833
|
+
const next = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom * factor));
|
|
834
|
+
const ratio = next / zoom;
|
|
835
|
+
// Keep the point under the cursor fixed while the scale changes.
|
|
836
|
+
panX = cx - (cx - panX) * ratio;
|
|
837
|
+
panY = cy - (cy - panY) * ratio;
|
|
838
|
+
zoom = next;
|
|
839
|
+
applyView();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function zoomCenter(factor) {
|
|
843
|
+
const r = viewportEl.getBoundingClientRect();
|
|
844
|
+
zoomAt(factor, r.width / 2, r.height / 2);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function fit() {
|
|
848
|
+
const r = viewportEl.getBoundingClientRect();
|
|
849
|
+
if (!r.width || !natW || !natH) { pendingFit = true; return; }
|
|
850
|
+
pendingFit = false;
|
|
851
|
+
zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM,
|
|
852
|
+
Math.min((r.width - 32) / natW, (r.height - 32) / natH)));
|
|
853
|
+
panX = (r.width - natW * zoom) / 2;
|
|
854
|
+
panY = Math.max(16, (r.height - natH * zoom) / 2);
|
|
855
|
+
applyView();
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
pane.querySelector('.gtools').addEventListener('click', function (ev) {
|
|
859
|
+
const what = ev.target.dataset ? ev.target.dataset.zoom : null;
|
|
860
|
+
if (what === 'in') zoomCenter(1.25);
|
|
861
|
+
else if (what === 'out') zoomCenter(1 / 1.25);
|
|
862
|
+
else if (what === 'fit') fit();
|
|
863
|
+
else if (what === 'reset') { zoom = 1; panX = 16; panY = 16; applyView(); }
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
viewportEl.addEventListener('wheel', function (ev) {
|
|
867
|
+
ev.preventDefault();
|
|
868
|
+
const r = viewportEl.getBoundingClientRect();
|
|
869
|
+
zoomAt(Math.exp(-ev.deltaY * 0.002), ev.clientX - r.left, ev.clientY - r.top);
|
|
870
|
+
}, { passive: false });
|
|
871
|
+
|
|
872
|
+
let dragging = false, dragMoved = false, startX = 0, startY = 0, baseX = 0, baseY = 0;
|
|
873
|
+
|
|
874
|
+
viewportEl.addEventListener('pointerdown', function (ev) {
|
|
875
|
+
if (ev.button !== 0) return;
|
|
876
|
+
dragging = true; dragMoved = false;
|
|
877
|
+
startX = ev.clientX; startY = ev.clientY; baseX = panX; baseY = panY;
|
|
878
|
+
});
|
|
879
|
+
viewportEl.addEventListener('pointermove', function (ev) {
|
|
880
|
+
if (!dragging) return;
|
|
881
|
+
const dx = ev.clientX - startX, dy = ev.clientY - startY;
|
|
882
|
+
if (!dragMoved) {
|
|
883
|
+
if (Math.abs(dx) + Math.abs(dy) <= 4) return;
|
|
884
|
+
dragMoved = true;
|
|
885
|
+
// Captured only once this is a drag: capturing on pointerdown would
|
|
886
|
+
// retarget the following click to the viewport and no node would ever
|
|
887
|
+
// receive it.
|
|
888
|
+
viewportEl.setPointerCapture(ev.pointerId);
|
|
889
|
+
viewportEl.classList.add('drag');
|
|
890
|
+
}
|
|
891
|
+
panX = baseX + dx; panY = baseY + dy;
|
|
892
|
+
applyView();
|
|
893
|
+
});
|
|
894
|
+
function endDrag(ev) {
|
|
895
|
+
if (!dragging) return;
|
|
896
|
+
dragging = false;
|
|
897
|
+
viewportEl.classList.remove('drag');
|
|
898
|
+
if (viewportEl.hasPointerCapture(ev.pointerId)) viewportEl.releasePointerCapture(ev.pointerId);
|
|
899
|
+
}
|
|
900
|
+
viewportEl.addEventListener('pointerup', endDrag);
|
|
901
|
+
viewportEl.addEventListener('pointercancel', endDrag);
|
|
902
|
+
viewportEl.addEventListener('dblclick', function () { fit(); });
|
|
903
|
+
window.addEventListener('resize', function () { if (pendingFit) fit(); });
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
canvas: canvasEl,
|
|
907
|
+
setNatural: function (w, h) { natW = w; natH = h; fit(); },
|
|
908
|
+
refit: function () { if (pendingFit) fit(); },
|
|
909
|
+
// A pan that ends on a node must not be read as a click on it.
|
|
910
|
+
moved: function () { return dragMoved; }
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
PANES.graph = panZoom('graph');
|
|
915
|
+
PANES.agents = panZoom('agents');
|
|
916
|
+
|
|
917
|
+
// --- diagram --------------------------------------------------------------
|
|
918
|
+
|
|
919
|
+
const CLASSES = {
|
|
920
|
+
llm_call: 'llm', tool_call: 'tool', tool_result: 'tool', compaction: 'comp',
|
|
921
|
+
fork: 'fork', join: 'fork', final_output: 'final', handoff: 'hand'
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// A failed call is the one thing in a trace people look for, so it is coloured
|
|
925
|
+
// by outcome rather than by type — the same red the sidebar badge uses.
|
|
926
|
+
function nodeClass(n) {
|
|
927
|
+
if (n.type === 'tool_result' && n.isError) return 'err';
|
|
928
|
+
return CLASSES[n.type];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Diagram labels are built from node types and identifiers only, and are
|
|
932
|
+
// stripped to a conservative character set: nothing a model wrote can reach
|
|
933
|
+
// the Mermaid parser, let alone the SVG.
|
|
934
|
+
function safe(text) {
|
|
935
|
+
return String(text).replace(/[^\\w .:\\-\\/>+=()]/g, ' ').slice(0, 60).trim();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function shape(e, i) {
|
|
939
|
+
const n = e.node;
|
|
940
|
+
// The timing is ours, not the run's: digits and a unit, appended after
|
|
941
|
+
// sanitizing. It stays on one line because Mermaid's strict mode drops the
|
|
942
|
+
// <br/> that would break it.
|
|
943
|
+
const d = took(e);
|
|
944
|
+
const text = '"' + (i + 1) + '. ' + safe(label(n))
|
|
945
|
+
+ ' t+' + startedAt(e) + (d ? ' · ' + d : '') + '"';
|
|
946
|
+
if (n.type === 'fork' || n.type === 'join') return e.key + '{{' + text + '}}';
|
|
947
|
+
if (n.type === 'final_output') return e.key + '([' + text + '])';
|
|
948
|
+
if (n.type === 'user_input' || n.type === 'system_prompt') return e.key + '[/' + text + '/]';
|
|
949
|
+
return e.key + '[' + text + ']';
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function chain(level, lines) {
|
|
953
|
+
let prev = null;
|
|
954
|
+
level.forEach(function (e) {
|
|
955
|
+
const i = ENTRIES.indexOf(e);
|
|
956
|
+
lines.push(' ' + shape(e, i));
|
|
957
|
+
const cls = nodeClass(e.node);
|
|
958
|
+
if (cls) lines.push(' class ' + e.key + ' ' + cls + ';');
|
|
959
|
+
if (e.covered) lines.push(' class ' + e.key + ' covered;');
|
|
960
|
+
if (prev) lines.push(' ' + prev.key + ' --> ' + e.key);
|
|
961
|
+
|
|
962
|
+
if (e.node.type === 'join' && e.children.length) {
|
|
963
|
+
// A branch hangs off the fork that opened it and rejoins at the join.
|
|
964
|
+
const fork = level.find(function (x) {
|
|
965
|
+
return x.node.type === 'fork' && x.node.callId === e.node.callId;
|
|
966
|
+
});
|
|
967
|
+
e.children.forEach(function (br, bi) {
|
|
968
|
+
if (!br.entries.length) return;
|
|
969
|
+
const bd = took(br);
|
|
970
|
+
const sg = 'sg_' + e.key + '_' + bi;
|
|
971
|
+
lines.push(' subgraph ' + sg + '["' + safe(br.name)
|
|
972
|
+
+ ' t+' + startedAt(br) + (bd ? ' · ' + bd : '')
|
|
973
|
+
+ (br.slowest ? ' · slowest' : '') + '"]');
|
|
974
|
+
lines.push(' direction TB');
|
|
975
|
+
chain(br.entries, lines);
|
|
976
|
+
lines.push(' end');
|
|
977
|
+
// The one branch the join actually waited for, lifted out of the pack.
|
|
978
|
+
if (br.slowest) lines.push(' style ' + sg + ' fill:#5b6070,stroke:#a9b2c6,color:#f2f4f8;');
|
|
979
|
+
if (fork) lines.push(' ' + fork.key + ' -.-> ' + br.entries[0].key);
|
|
980
|
+
lines.push(' ' + br.entries[br.entries.length - 1].key + ' -.-> ' + e.key);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
prev = e;
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function diagram() {
|
|
988
|
+
const lines = ['flowchart TD'];
|
|
989
|
+
lines.push(' classDef llm fill:#1e3357,stroke:#3d5f9e,color:#cfe0ff;');
|
|
990
|
+
lines.push(' classDef tool fill:#1e3d33,stroke:#3d7a66,color:#c8f0e2;');
|
|
991
|
+
lines.push(' classDef comp fill:#40331a,stroke:#7a6535,color:#f0dfb4;');
|
|
992
|
+
lines.push(' classDef fork fill:#3a2450,stroke:#6b4b90,color:#e6d3ff;');
|
|
993
|
+
lines.push(' classDef final fill:#14392c,stroke:#2f7a5c,color:#c6f5e0;');
|
|
994
|
+
lines.push(' classDef hand fill:#3d2a1b,stroke:#7a5535,color:#f2d9c2;');
|
|
995
|
+
lines.push(' classDef err fill:#4a1f1c,stroke:#8a3a33,color:#f0776c;');
|
|
996
|
+
lines.push(' classDef covered opacity:0.45,stroke-dasharray:4 3;');
|
|
997
|
+
chain(ROOT, lines);
|
|
998
|
+
return lines.join('\\n');
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// One Mermaid, two diagrams. Loading it is the only network the page does, so
|
|
1002
|
+
// it happens once and both panes wait on the same promise.
|
|
1003
|
+
let mermaidLib = null;
|
|
1004
|
+
function mermaid() {
|
|
1005
|
+
if (!mermaidLib) {
|
|
1006
|
+
mermaidLib = import(MERMAID_URL).then(function (m) {
|
|
1007
|
+
m.default.initialize({
|
|
1008
|
+
startOnLoad: false, theme: 'dark', securityLevel: 'strict',
|
|
1009
|
+
flowchart: { htmlLabels: false }
|
|
1010
|
+
});
|
|
1011
|
+
return m.default;
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
return mermaidLib;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Renders one diagram into a pane and wires its nodes. False when it failed. */
|
|
1018
|
+
async function draw(pane, id, source, onNode) {
|
|
1019
|
+
let lib;
|
|
1020
|
+
try {
|
|
1021
|
+
lib = await mermaid();
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
pane.canvas.replaceChildren(el('div', 'hint pad',
|
|
1024
|
+
'Mermaid could not be loaded (offline?). The timeline on the left has the same nodes.'));
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
const rendered = await lib.render(id, source);
|
|
1029
|
+
// Mermaid produced this markup from labels we sanitized above; no payload
|
|
1030
|
+
// text reaches it.
|
|
1031
|
+
pane.canvas.innerHTML = rendered.svg;
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
pane.canvas.replaceChildren(el('pre', null, 'diagram failed: ' + err.message + '\\n\\n' + source));
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
const svg = pane.canvas.querySelector('svg');
|
|
1037
|
+
if (svg) {
|
|
1038
|
+
// Mermaid sizes the SVG to fit its container; we want its natural size so
|
|
1039
|
+
// the transform above is the only thing deciding scale.
|
|
1040
|
+
const box = svg.viewBox && svg.viewBox.baseVal;
|
|
1041
|
+
const w = (box && box.width) || svg.getBoundingClientRect().width;
|
|
1042
|
+
const h = (box && box.height) || svg.getBoundingClientRect().height;
|
|
1043
|
+
svg.removeAttribute('width');
|
|
1044
|
+
svg.removeAttribute('height');
|
|
1045
|
+
svg.style.maxWidth = 'none';
|
|
1046
|
+
svg.style.width = w + 'px';
|
|
1047
|
+
svg.style.height = h + 'px';
|
|
1048
|
+
pane.setNatural(w, h);
|
|
1049
|
+
}
|
|
1050
|
+
pane.canvas.querySelectorAll('.node').forEach(onNode);
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function drawGraph() {
|
|
1055
|
+
const ok = await draw(PANES.graph, 'run-graph', diagram(), function (g) {
|
|
1056
|
+
// Mermaid ids are '<renderId>-flowchart-<ourKey>-<seq>'; only the key matters.
|
|
1057
|
+
const m = /-(n\\d+)-\\d+$/.exec(g.id || '');
|
|
1058
|
+
const key = m ? m[1] : null;
|
|
1059
|
+
if (!key || !BY_KEY.has(key)) return;
|
|
1060
|
+
g.addEventListener('click', function () { if (!PANES.graph.moved()) select(key, true); });
|
|
1061
|
+
});
|
|
1062
|
+
if (!ok) show('detail');
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// --- architecture ---------------------------------------------------------
|
|
1066
|
+
//
|
|
1067
|
+
// The run graph answers "what happened". This one answers "what was wired up",
|
|
1068
|
+
// which is the question a trajectory structurally cannot: the agent nobody
|
|
1069
|
+
// handed off to and the tool nobody called leave no node behind. The declared
|
|
1070
|
+
// wiring arrives with the report when a runner was there to describe itself;
|
|
1071
|
+
// otherwise the run is read backwards into the same shape, which is smaller
|
|
1072
|
+
// but never wrong.
|
|
1073
|
+
|
|
1074
|
+
const SEP = '::';
|
|
1075
|
+
|
|
1076
|
+
function bump(map, key, entryKey) {
|
|
1077
|
+
const cur = map.get(key);
|
|
1078
|
+
if (cur) { cur.count += 1; return; }
|
|
1079
|
+
map.set(key, { count: 1, key: entryKey });
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// What this run actually exercised, keyed the same way the diagram names
|
|
1083
|
+
// things — so drawing "used" is a lookup rather than a search.
|
|
1084
|
+
const USED = {
|
|
1085
|
+
agents: new Map(), models: new Map(), tools: new Map(),
|
|
1086
|
+
skills: new Map(), handoffs: new Map(), memory: new Map(), forks: new Map()
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
ENTRIES.forEach(function (e) {
|
|
1090
|
+
const n = e.node;
|
|
1091
|
+
bump(USED.agents, n.agent, e.key);
|
|
1092
|
+
if (n.type === 'llm_call') {
|
|
1093
|
+
if (!USED.models.has(n.agent)) USED.models.set(n.agent, n.model);
|
|
1094
|
+
} else if (n.type === 'tool_call') {
|
|
1095
|
+
bump(USED.tools, n.agent + SEP + n.name, e.key);
|
|
1096
|
+
} else if (n.type === 'load_skills') {
|
|
1097
|
+
n.skills.forEach(function (s) { bump(USED.skills, n.agent + SEP + s.name, e.key); });
|
|
1098
|
+
} else if (n.type === 'handoff') {
|
|
1099
|
+
bump(USED.handoffs, n.from + SEP + n.to, e.key);
|
|
1100
|
+
} else if (n.type === 'memory_recall' || n.type === 'memory_op') {
|
|
1101
|
+
bump(USED.memory, n.agent + SEP + n.store + '/' + n.scope, e.key);
|
|
1102
|
+
} else if (n.type === 'fork') {
|
|
1103
|
+
n.branches.forEach(function (b) { bump(USED.forks, n.agent + SEP + b.agent, e.key); });
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
function has(list, name) {
|
|
1108
|
+
return list.some(function (x) { return x.name === name; });
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** The run read backwards into the same shape. Used when none was supplied. */
|
|
1112
|
+
function observedArchitecture() {
|
|
1113
|
+
const byName = new Map();
|
|
1114
|
+
function agentOf(name) {
|
|
1115
|
+
if (!byName.has(name)) {
|
|
1116
|
+
byName.set(name, { name: name, tools: [], handoffs: [], memory: [] });
|
|
1117
|
+
}
|
|
1118
|
+
return byName.get(name);
|
|
1119
|
+
}
|
|
1120
|
+
ENTRIES.forEach(function (e) {
|
|
1121
|
+
const n = e.node;
|
|
1122
|
+
const a = agentOf(n.agent);
|
|
1123
|
+
if (n.type === 'llm_call') {
|
|
1124
|
+
if (!a.model) a.model = n.model;
|
|
1125
|
+
} else if (n.type === 'tool_call') {
|
|
1126
|
+
// Handoff tools are drawn as edges between agents, never as tool nodes.
|
|
1127
|
+
const handoff = n.name.indexOf('transfer_to_') === 0;
|
|
1128
|
+
if (!handoff && !has(a.tools, n.name)) a.tools.push({ name: n.name });
|
|
1129
|
+
} else if (n.type === 'load_skills') {
|
|
1130
|
+
if (!a.skills) a.skills = { provider: n.provider, discovery: 'index', catalog: [] };
|
|
1131
|
+
n.skills.forEach(function (s) {
|
|
1132
|
+
if (has(a.skills.catalog, s.name)) return;
|
|
1133
|
+
// The unlocked tools are recorded per activation, not per skill, so
|
|
1134
|
+
// they can only be attributed when one skill loaded on its own.
|
|
1135
|
+
a.skills.catalog.push({
|
|
1136
|
+
name: s.name, version: s.version,
|
|
1137
|
+
toolNames: n.skills.length === 1 ? n.toolNames : undefined
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
} else if (n.type === 'handoff') {
|
|
1141
|
+
const from = agentOf(n.from);
|
|
1142
|
+
agentOf(n.to);
|
|
1143
|
+
if (from.handoffs.indexOf(n.to) < 0) from.handoffs.push(n.to);
|
|
1144
|
+
} else if (n.type === 'memory_recall' || n.type === 'memory_op') {
|
|
1145
|
+
const known = a.memory.some(function (m) {
|
|
1146
|
+
return m.store === n.store && m.scope === n.scope;
|
|
1147
|
+
});
|
|
1148
|
+
if (!known) {
|
|
1149
|
+
a.memory.push({
|
|
1150
|
+
store: n.store, scope: n.scope,
|
|
1151
|
+
access: n.type === 'memory_op' ? 'read-write' : 'read'
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
} else if (n.type === 'fork') {
|
|
1155
|
+
if (!a.fork) a.fork = { agents: [] };
|
|
1156
|
+
n.branches.forEach(function (b) {
|
|
1157
|
+
agentOf(b.agent);
|
|
1158
|
+
if (a.fork.agents.indexOf(b.agent) < 0) a.fork.agents.push(b.agent);
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
return { source: 'observed', agents: Array.from(byName.values()) };
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const ARCH = DATA.architecture || observedArchitecture();
|
|
1166
|
+
|
|
1167
|
+
// Diagram key -> the first trajectory node that exercised the thing it names,
|
|
1168
|
+
// or null when nothing did. Clicking is only offered for the former.
|
|
1169
|
+
const ARCH_KEYS = new Map();
|
|
1170
|
+
let archSeq = 0;
|
|
1171
|
+
function archKey(used) {
|
|
1172
|
+
const k = 'x' + archSeq++;
|
|
1173
|
+
ARCH_KEYS.set(k, used ? used.key : null);
|
|
1174
|
+
return k;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** An agent with more tools than this gets a "+N more" node instead of a wall. */
|
|
1178
|
+
const MAX_TOOLS = 12;
|
|
1179
|
+
|
|
1180
|
+
function times(used) {
|
|
1181
|
+
return used ? ' x' + used.count : '';
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function archDiagram() {
|
|
1185
|
+
const lines = ['flowchart LR'];
|
|
1186
|
+
lines.push(' classDef agent fill:#1e3357,stroke:#3d5f9e,color:#cfe0ff;');
|
|
1187
|
+
lines.push(' classDef start fill:#14392c,stroke:#2f7a5c,color:#c6f5e0;');
|
|
1188
|
+
lines.push(' classDef tool fill:#1e3d33,stroke:#3d7a66,color:#c8f0e2;');
|
|
1189
|
+
lines.push(' classDef skill fill:#3a2450,stroke:#6b4b90,color:#e6d3ff;');
|
|
1190
|
+
lines.push(' classDef mem fill:#40331a,stroke:#7a6535,color:#f0dfb4;');
|
|
1191
|
+
lines.push(' classDef more fill:#1b202c,stroke:#39415a,color:#8a92a6;');
|
|
1192
|
+
lines.push(' classDef idle opacity:0.4,stroke-dasharray:4 3;');
|
|
1193
|
+
|
|
1194
|
+
const startAgent = (STATE.spec && STATE.spec.startAgent) || STATE.agentName;
|
|
1195
|
+
const selfKey = new Map();
|
|
1196
|
+
// Minted up front so a hand-off can name an agent declared further down.
|
|
1197
|
+
ARCH.agents.forEach(function (a) {
|
|
1198
|
+
selfKey.set(a.name, archKey(USED.agents.get(a.name)));
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
ARCH.agents.forEach(function (a) {
|
|
1202
|
+
const self = selfKey.get(a.name);
|
|
1203
|
+
const live = USED.agents.get(a.name);
|
|
1204
|
+
// The model on the node is the declared one, or — when the report carries
|
|
1205
|
+
// no wiring — whatever this run was seen using.
|
|
1206
|
+
const model = a.model || USED.models.get(a.name);
|
|
1207
|
+
lines.push(' subgraph sg_' + self + '["' + safe(a.name)
|
|
1208
|
+
+ (a.description ? ' - ' + safe(a.description) : '') + '"]');
|
|
1209
|
+
lines.push(' direction LR');
|
|
1210
|
+
lines.push(' ' + self + '[["' + safe(a.name)
|
|
1211
|
+
+ (model ? ' ' + safe(model) + (a.inheritedModel ? ' (inherited)' : '') : '')
|
|
1212
|
+
+ '"]]');
|
|
1213
|
+
lines.push(' class ' + self + ' ' + (a.name === startAgent ? 'start' : 'agent') + ';');
|
|
1214
|
+
if (!live) lines.push(' class ' + self + ' idle;');
|
|
1215
|
+
|
|
1216
|
+
// Skills first: a skill owns tools, so its node has to exist before the
|
|
1217
|
+
// tools it unlocks can point back at it.
|
|
1218
|
+
const skillKey = new Map();
|
|
1219
|
+
const catalog = (a.skills && a.skills.catalog) || [];
|
|
1220
|
+
catalog.forEach(function (s) {
|
|
1221
|
+
const used = USED.skills.get(a.name + SEP + s.name);
|
|
1222
|
+
const k = archKey(used);
|
|
1223
|
+
skillKey.set(s.name, k);
|
|
1224
|
+
lines.push(' ' + k + '(["' + safe(s.name) + (s.preload ? ' (preloaded)' : '')
|
|
1225
|
+
+ times(used) + '"])');
|
|
1226
|
+
lines.push(' class ' + k + ' skill;');
|
|
1227
|
+
if (!used) lines.push(' class ' + k + ' idle;');
|
|
1228
|
+
lines.push(' ' + self + ' --- ' + k);
|
|
1229
|
+
});
|
|
1230
|
+
if (a.skills && a.skills.unresolved) {
|
|
1231
|
+
const k = archKey(null);
|
|
1232
|
+
lines.push(' ' + k + '(["catalog unavailable: ' + safe(a.skills.provider) + '"])');
|
|
1233
|
+
lines.push(' class ' + k + ' skill;');
|
|
1234
|
+
lines.push(' class ' + k + ' idle;');
|
|
1235
|
+
lines.push(' ' + self + ' --- ' + k);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const shown = a.tools.slice(0, MAX_TOOLS);
|
|
1239
|
+
shown.forEach(function (t) {
|
|
1240
|
+
const used = USED.tools.get(a.name + SEP + t.name);
|
|
1241
|
+
const k = archKey(used);
|
|
1242
|
+
lines.push(' ' + k + '["' + safe(t.name) + times(used) + '"]');
|
|
1243
|
+
lines.push(' class ' + k + ' tool;');
|
|
1244
|
+
if (!used) lines.push(' class ' + k + ' idle;');
|
|
1245
|
+
// A skill-owned tool hangs off the skill that unlocks it, not off the
|
|
1246
|
+
// agent: that edge is the whole point of a locked tool.
|
|
1247
|
+
const owner = t.skill ? skillKey.get(t.skill) : null;
|
|
1248
|
+
if (owner) lines.push(' ' + owner + ' -.-> ' + k);
|
|
1249
|
+
else lines.push(' ' + self + ' --- ' + k);
|
|
1250
|
+
});
|
|
1251
|
+
if (a.tools.length > shown.length) {
|
|
1252
|
+
const k = archKey(null);
|
|
1253
|
+
lines.push(' ' + k + '["+' + (a.tools.length - shown.length) + ' more tools"]');
|
|
1254
|
+
lines.push(' class ' + k + ' more;');
|
|
1255
|
+
lines.push(' ' + self + ' --- ' + k);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
a.memory.forEach(function (m) {
|
|
1259
|
+
const used = USED.memory.get(a.name + SEP + m.store + '/' + m.scope);
|
|
1260
|
+
const k = archKey(used);
|
|
1261
|
+
lines.push(' ' + k + '[("' + safe(m.store) + ' / ' + safe(m.scope)
|
|
1262
|
+
+ (m.access === 'read-write' ? ' (rw)' : ' (ro)') + '")]');
|
|
1263
|
+
lines.push(' class ' + k + ' mem;');
|
|
1264
|
+
if (!used) lines.push(' class ' + k + ' idle;');
|
|
1265
|
+
lines.push(' ' + self + ' --- ' + k);
|
|
1266
|
+
});
|
|
1267
|
+
lines.push(' end');
|
|
1268
|
+
// Mermaid's default subgraph is a light box, which on a dark page reads as
|
|
1269
|
+
// the foreground rather than as the container. An agent nobody entered is
|
|
1270
|
+
// outlined rather than filled, so a dead corner of the wiring is visible
|
|
1271
|
+
// from across the diagram.
|
|
1272
|
+
lines.push(' style sg_' + self + ' fill:#12151d,color:#8a92a6,stroke:'
|
|
1273
|
+
+ (live ? '#39415a;' : '#2b3346,stroke-dasharray:5 4;'));
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
// Edges between agents live outside every subgraph, so Mermaid routes them
|
|
1277
|
+
// between the boxes instead of trying to keep them inside one.
|
|
1278
|
+
ARCH.agents.forEach(function (a) {
|
|
1279
|
+
const self = selfKey.get(a.name);
|
|
1280
|
+
a.handoffs.forEach(function (to) {
|
|
1281
|
+
if (!selfKey.has(to)) return;
|
|
1282
|
+
const used = USED.handoffs.get(a.name + SEP + to);
|
|
1283
|
+
lines.push(' ' + self + (used ? ' -- "handoff' + times(used) + '" --> ' : ' -.-> ')
|
|
1284
|
+
+ selfKey.get(to));
|
|
1285
|
+
});
|
|
1286
|
+
((a.fork && a.fork.agents) || []).forEach(function (to) {
|
|
1287
|
+
if (!selfKey.has(to) || to === a.name) return;
|
|
1288
|
+
const used = USED.forks.get(a.name + SEP + to);
|
|
1289
|
+
lines.push(' ' + self + ' == "fork' + times(used) + '" ==> ' + selfKey.get(to));
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
return lines.join('\\n');
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function archLegend() {
|
|
1296
|
+
const agents = ARCH.agents.length;
|
|
1297
|
+
const tools = ARCH.agents.reduce(function (n, a) { return n + a.tools.length; }, 0);
|
|
1298
|
+
const skills = ARCH.agents.reduce(function (n, a) {
|
|
1299
|
+
return n + ((a.skills && a.skills.catalog.length) || 0);
|
|
1300
|
+
}, 0);
|
|
1301
|
+
const idle = ARCH.agents.filter(function (a) { return !USED.agents.has(a.name); }).length;
|
|
1302
|
+
return (ARCH.source === 'declared' ? 'declared' : 'observed only')
|
|
1303
|
+
+ ' \\u00b7 ' + agents + ' agents \\u00b7 ' + tools + ' tools \\u00b7 ' + skills + ' skills'
|
|
1304
|
+
+ (idle ? ' \\u00b7 ' + idle + ' idle' : '')
|
|
1305
|
+
+ ' \\u00b7 dimmed = unused \\u00b7 click to jump';
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async function drawArch() {
|
|
1309
|
+
document.getElementById('alegend').textContent = archLegend();
|
|
1310
|
+
if (!ARCH.agents.length) {
|
|
1311
|
+
PANES.agents.canvas.replaceChildren(el('div', 'hint pad', 'No agents to draw.'));
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
await draw(PANES.agents, 'arch-graph', archDiagram(), function (g) {
|
|
1315
|
+
const m = /-(x\\d+)-\\d+$/.exec(g.id || '');
|
|
1316
|
+
const key = m && ARCH_KEYS.get(m[1]);
|
|
1317
|
+
if (!key) return;
|
|
1318
|
+
g.addEventListener('click', function () { if (!PANES.agents.moved()) select(key, true); });
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
drawGraph().then(drawArch);
|
|
1323
|
+
buildStats();
|
|
1324
|
+
if (ENTRIES.length) select('n0', false);
|
|
1325
|
+
`;
|
|
1326
|
+
//# sourceMappingURL=client.js.map
|