@clemrig/flow-ui 0.5.0 → 0.6.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/README.md +3 -1
- package/dist/server.d.ts +5 -0
- package/dist/server.js +29 -13
- package/dist/server.js.map +1 -1
- package/package.json +15 -5
- package/page/app.css +1 -0
- package/page/app.js +13 -0
- package/page/index.html +13 -0
- package/dist/static/dashboard.js +0 -207
- package/dist/static/detail.js +0 -489
- package/dist/static/follow.js +0 -103
- package/dist/static/index.html +0 -20
- package/dist/static/style.css +0 -460
- package/dist/static/timeline.js +0 -443
package/dist/static/timeline.js
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
/**
|
|
3
|
-
* The dashboard's pure logic: a run's events grouped into one block per stage
|
|
4
|
-
* execution, those blocks grouped into launches, and the formats of durations,
|
|
5
|
-
* costs, tokens and agent settings. Served as is to the browser.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** @typedef {import("@clemrig/flow").RunEvent} RunEvent */
|
|
9
|
-
/** @typedef {import("@clemrig/flow").AgentActivity} AgentActivity */
|
|
10
|
-
/** @typedef {import("@clemrig/flow").AgentSettings} AgentSettings */
|
|
11
|
-
/** @typedef {import("@clemrig/flow").TokenCounts} TokenCounts */
|
|
12
|
-
/** @typedef {import("@clemrig/flow").AgentUsage} AgentUsage */
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {object} Call
|
|
16
|
-
* @property {string} [runtime]
|
|
17
|
-
* @property {string} [model]
|
|
18
|
-
* @property {string} [session]
|
|
19
|
-
* @property {AgentSettings} [settings] Absent from runs recorded before calls recorded their settings.
|
|
20
|
-
* @property {"running" | "completed" | "failed"} status
|
|
21
|
-
* @property {AgentUsage} [usage] As the runtime announced it at the end of the call.
|
|
22
|
-
* @property {"call" | "session"} [scope] Whether `usage` is the call's own or its session's total.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} Tool
|
|
27
|
-
* @property {string} id
|
|
28
|
-
* @property {string} name
|
|
29
|
-
* @property {"running" | "completed" | "failed"} status
|
|
30
|
-
* @property {number} [durationMs]
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @typedef {object} Block
|
|
35
|
-
* @property {string} stage
|
|
36
|
-
* @property {string} kind
|
|
37
|
-
* @property {"running" | "passed" | "failed" | "completed"} status
|
|
38
|
-
* @property {string} startedAt
|
|
39
|
-
* @property {number | null} durationMs
|
|
40
|
-
* @property {{ verdict: string; reason: string; destination: string | null }} [decision]
|
|
41
|
-
* @property {{ key: string; count: number; max: number }} [retry]
|
|
42
|
-
* @property {string} [detail]
|
|
43
|
-
* @property {unknown} [output] What the stage's agent answered.
|
|
44
|
-
* @property {unknown} [feedback] The correction the stage sent back, or could not send past its maximum.
|
|
45
|
-
* @property {Call} [call]
|
|
46
|
-
* @property {Tool[]} tools
|
|
47
|
-
* @property {TokenCounts | null} tokens The call's responses so far.
|
|
48
|
-
* @property {boolean} provisional Whether `tokens` includes figures replaced at the end of the call.
|
|
49
|
-
* @property {string[]} warnings
|
|
50
|
-
* @property {{ at: string; message: string }[]} journal
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {string} stage
|
|
55
|
-
* @param {string} kind
|
|
56
|
-
* @param {string} at
|
|
57
|
-
* @returns {Block}
|
|
58
|
-
*/
|
|
59
|
-
const block = (stage, kind, at) => ({
|
|
60
|
-
stage,
|
|
61
|
-
kind,
|
|
62
|
-
status: "running",
|
|
63
|
-
startedAt: at,
|
|
64
|
-
durationMs: null,
|
|
65
|
-
tools: [],
|
|
66
|
-
tokens: null,
|
|
67
|
-
provisional: false,
|
|
68
|
-
warnings: [],
|
|
69
|
-
journal: [],
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {Block} target
|
|
74
|
-
* @param {AgentActivity} activity
|
|
75
|
-
*/
|
|
76
|
-
function apply(target, activity) {
|
|
77
|
-
switch (activity.kind) {
|
|
78
|
-
case "call":
|
|
79
|
-
if (activity.status === "started") {
|
|
80
|
-
const { runtime, model, session, settings } = activity;
|
|
81
|
-
target.call = { runtime, model, session, settings, status: "running" };
|
|
82
|
-
} else
|
|
83
|
-
target.call = {
|
|
84
|
-
...target.call,
|
|
85
|
-
runtime: activity.runtime,
|
|
86
|
-
status: activity.status,
|
|
87
|
-
usage: activity.usage,
|
|
88
|
-
scope: activity.scope,
|
|
89
|
-
};
|
|
90
|
-
break;
|
|
91
|
-
case "tool": {
|
|
92
|
-
const tool = target.tools.find(({ id }) => id === activity.id);
|
|
93
|
-
const update =
|
|
94
|
-
activity.status === "started"
|
|
95
|
-
? { id: activity.id, name: activity.name, status: "running" }
|
|
96
|
-
: {
|
|
97
|
-
id: activity.id,
|
|
98
|
-
name: activity.name,
|
|
99
|
-
status: activity.status,
|
|
100
|
-
durationMs: activity.durationMs,
|
|
101
|
-
};
|
|
102
|
-
if (tool) Object.assign(tool, update);
|
|
103
|
-
else target.tools.push(/** @type {Tool} */ (update));
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
case "response": {
|
|
107
|
-
const sum = target.tokens ?? {
|
|
108
|
-
input: 0,
|
|
109
|
-
output: 0,
|
|
110
|
-
cacheRead: 0,
|
|
111
|
-
cacheWrite: 0,
|
|
112
|
-
};
|
|
113
|
-
for (const field of /** @type {const} */ ([
|
|
114
|
-
"input",
|
|
115
|
-
"output",
|
|
116
|
-
"cacheRead",
|
|
117
|
-
"cacheWrite",
|
|
118
|
-
]))
|
|
119
|
-
sum[field] += activity.tokens[field];
|
|
120
|
-
target.tokens = sum;
|
|
121
|
-
target.provisional ||= activity.provisional;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case "warning":
|
|
125
|
-
target.warnings.push(activity.message);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* One block per stage execution, in order: a stage's journal, agent activity
|
|
131
|
-
* and decision go to its latest block.
|
|
132
|
-
* @param {RunEvent[]} events
|
|
133
|
-
* @returns {Block[]}
|
|
134
|
-
*/
|
|
135
|
-
export function timeline(events) {
|
|
136
|
-
/** @type {Block[]} */
|
|
137
|
-
const blocks = [];
|
|
138
|
-
const latest = (/** @type {string} */ stage) =>
|
|
139
|
-
blocks.findLast((candidate) => candidate.stage === stage);
|
|
140
|
-
for (const event of events) {
|
|
141
|
-
if (event.type === "stage" && event.status === "started") {
|
|
142
|
-
blocks.push(block(event.stage, event.kind, event.at));
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
let target = latest(event.stage);
|
|
146
|
-
if (!target || (event.type === "stage" && target.status !== "running")) {
|
|
147
|
-
// A stage reported without a start, such as one failed by an interruption.
|
|
148
|
-
target = block(event.stage, event.kind, event.at);
|
|
149
|
-
blocks.push(target);
|
|
150
|
-
}
|
|
151
|
-
if (event.type === "stage") {
|
|
152
|
-
target.status = event.status === "started" ? "running" : event.status;
|
|
153
|
-
target.durationMs = Date.parse(event.at) - Date.parse(target.startedAt);
|
|
154
|
-
if (event.decision) target.decision = event.decision;
|
|
155
|
-
if (event.retry) target.retry = event.retry;
|
|
156
|
-
if (event.detail) target.detail = event.detail;
|
|
157
|
-
if (Object.hasOwn(event, "output")) target.output = event.output;
|
|
158
|
-
if (event.feedback !== undefined) target.feedback = event.feedback;
|
|
159
|
-
} else if (event.type === "log")
|
|
160
|
-
target.journal.push({ at: event.at, message: event.message });
|
|
161
|
-
else apply(target, event.activity);
|
|
162
|
-
}
|
|
163
|
-
return blocks;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* A stage's answer or correction as text: a string as it is, anything else as
|
|
168
|
-
* indented JSON.
|
|
169
|
-
* @param {unknown} value
|
|
170
|
-
* @returns {string}
|
|
171
|
-
*/
|
|
172
|
-
export function outputText(value) {
|
|
173
|
-
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* @param {number | null} ms
|
|
178
|
-
* @returns {string}
|
|
179
|
-
*/
|
|
180
|
-
export function duration(ms) {
|
|
181
|
-
if (ms === null) return "—";
|
|
182
|
-
const seconds = Math.floor(ms / 1000);
|
|
183
|
-
const hours = Math.floor(seconds / 3600);
|
|
184
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
185
|
-
return `${hours ? `${hours} h ` : ""}${minutes} min ${seconds % 60} s`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* A duration in its two largest units: `1 h 07 min`, `7 min 02 s`, `12 s`,
|
|
190
|
-
* `< 1 s`.
|
|
191
|
-
* @param {number | null} ms
|
|
192
|
-
* @returns {string}
|
|
193
|
-
*/
|
|
194
|
-
export function brief(ms) {
|
|
195
|
-
if (ms === null) return "—";
|
|
196
|
-
const seconds = Math.floor(ms / 1000);
|
|
197
|
-
if (seconds < 1) return "< 1 s";
|
|
198
|
-
const hours = Math.floor(seconds / 3600);
|
|
199
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
200
|
-
const pad = (/** @type {number} */ n) => String(n).padStart(2, "0");
|
|
201
|
-
if (hours) return `${hours} h ${pad(minutes)} min`;
|
|
202
|
-
if (minutes) return `${minutes} min ${pad(seconds % 60)} s`;
|
|
203
|
-
return `${seconds} s`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* A token count: exact below ten thousand, then in thousands or millions.
|
|
208
|
-
* @param {number} count
|
|
209
|
-
* @returns {string}
|
|
210
|
-
*/
|
|
211
|
-
export function tokenCount(count) {
|
|
212
|
-
if (count >= 1e6) return `${(count / 1e6).toFixed(1)} M`;
|
|
213
|
-
if (count >= 1e4) return `${Math.round(count / 1e3)} k`;
|
|
214
|
-
return count.toLocaleString("en-US");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* A call's tokens: read without the cache, written, read from and written to
|
|
219
|
-
* the cache; provisional figures, which the call's end replaces, say so.
|
|
220
|
-
* @param {TokenCounts} tokens
|
|
221
|
-
* @param {boolean} provisional
|
|
222
|
-
* @returns {string}
|
|
223
|
-
*/
|
|
224
|
-
export function tokenLine(tokens, provisional) {
|
|
225
|
-
const { input, output, cacheRead, cacheWrite } = tokens;
|
|
226
|
-
return `${tokenCount(input)} in · ${tokenCount(output)} out · ${tokenCount(cacheRead)} cache read · ${tokenCount(cacheWrite)} cache write${provisional ? " (provisional)" : ""}`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Setting names that are not plain words. */
|
|
230
|
-
const settingLabels = /** @type {Record<string, string>} */ ({
|
|
231
|
-
mcpServers: "MCP servers",
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* A setting's name as the operator reads it: `permissionMode` → `permission mode`.
|
|
236
|
-
* @param {string} key
|
|
237
|
-
* @returns {string}
|
|
238
|
-
*/
|
|
239
|
-
function settingLabel(key) {
|
|
240
|
-
return (
|
|
241
|
-
settingLabels[key] ??
|
|
242
|
-
key.replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`)
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* An agent call's settings as its runtime recorded them, whatever the runtime,
|
|
248
|
-
* as labelled rows: its thinking, other single values and timeout in one, then
|
|
249
|
-
* one per list, its tools even when empty, the others only with items.
|
|
250
|
-
* @param {AgentSettings} settings
|
|
251
|
-
* @returns {{ label: string; value: string }[]}
|
|
252
|
-
*/
|
|
253
|
-
export function settingRows(settings) {
|
|
254
|
-
const entries = Object.entries(settings);
|
|
255
|
-
const values = entries.flatMap(([key, value]) =>
|
|
256
|
-
key === "thinking" ||
|
|
257
|
-
key === "timeoutMs" ||
|
|
258
|
-
value === undefined ||
|
|
259
|
-
Array.isArray(value)
|
|
260
|
-
? []
|
|
261
|
-
: [`${settingLabel(key)} ${String(value)}`],
|
|
262
|
-
);
|
|
263
|
-
const lists = entries.flatMap(([key, value]) => {
|
|
264
|
-
if (!Array.isArray(value) || (!value.length && key !== "tools")) return [];
|
|
265
|
-
const label = key === "tools" ? "allowed tools" : settingLabel(key);
|
|
266
|
-
return [
|
|
267
|
-
{
|
|
268
|
-
label: `${label[0]?.toUpperCase()}${label.slice(1)}`,
|
|
269
|
-
value: value.join(", ") || "none",
|
|
270
|
-
},
|
|
271
|
-
];
|
|
272
|
-
});
|
|
273
|
-
return [
|
|
274
|
-
{
|
|
275
|
-
label: "Settings",
|
|
276
|
-
value: [
|
|
277
|
-
`thinking ${settings.thinking}`,
|
|
278
|
-
...values,
|
|
279
|
-
`timeout ${brief(settings.timeoutMs)}`,
|
|
280
|
-
].join(" · "),
|
|
281
|
-
},
|
|
282
|
-
...lists,
|
|
283
|
-
];
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* An estimated cost as the runtimes announced it; unknown without any measure.
|
|
288
|
-
* @param {number | null} usd
|
|
289
|
-
* @param {boolean} partial Some agent call announced no cost.
|
|
290
|
-
* @returns {string}
|
|
291
|
-
*/
|
|
292
|
-
export function cost(usd, partial) {
|
|
293
|
-
const value = usd === null ? "—" : `${usd.toFixed(4)} USD`;
|
|
294
|
-
return partial ? `${value} (partial)` : value;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* An estimated cost as the Kanban shows it: in dollars, `≥` while some call
|
|
299
|
-
* announced none, a dash while none announced any.
|
|
300
|
-
* @param {number | null} usd
|
|
301
|
-
* @param {boolean} partial
|
|
302
|
-
* @returns {string}
|
|
303
|
-
*/
|
|
304
|
-
export function dollars(usd, partial) {
|
|
305
|
-
if (usd === null) return "—";
|
|
306
|
-
return `${partial ? "≥ " : ""}$${usd.toFixed(2)}`;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** @typedef {"passed" | "correction" | "failed" | "running"} State */
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* How a stage execution went: running, failed, sent back for a correction, or
|
|
313
|
-
* passed (the run's last stage included).
|
|
314
|
-
* @param {Block} block
|
|
315
|
-
* @returns {State}
|
|
316
|
-
*/
|
|
317
|
-
export function stateOf(block) {
|
|
318
|
-
if (block.status === "running" || block.status === "failed")
|
|
319
|
-
return block.status;
|
|
320
|
-
return block.decision?.verdict === "correction" ? "correction" : "passed";
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* A stage execution's time so far: its duration once ended.
|
|
325
|
-
* @param {Block} block
|
|
326
|
-
* @param {number} now
|
|
327
|
-
* @returns {number}
|
|
328
|
-
*/
|
|
329
|
-
export function spent(block, now) {
|
|
330
|
-
return block.durationMs ?? Math.max(0, now - Date.parse(block.startedAt));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* @typedef {object} Launch
|
|
335
|
-
* @property {number} number From 1: the run's first execution, then each resumption.
|
|
336
|
-
* @property {string} startedAt
|
|
337
|
-
* @property {string | null} endedAt Null while it runs.
|
|
338
|
-
* @property {"running" | "failed" | "completed"} status
|
|
339
|
-
* @property {string} [cause] Why a failed launch stopped.
|
|
340
|
-
* @property {{ block: Block; index: number }[]} stages Its stage executions, with their index in the run.
|
|
341
|
-
*/
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* The stage execution to show until the operator chooses one: the last, which
|
|
345
|
-
* is the one running or the one where the run stopped, never an earlier
|
|
346
|
-
* launch's failure once the run was resumed.
|
|
347
|
-
* @param {Block[]} blocks
|
|
348
|
-
* @returns {number | null}
|
|
349
|
-
*/
|
|
350
|
-
export function shownStage(blocks) {
|
|
351
|
-
return blocks.length ? blocks.length - 1 : null;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* The run's launches: its first execution, then one per resumption. A failed
|
|
356
|
-
* stage ends a launch, since it ends the run, and only a failed run is
|
|
357
|
-
* resumed: the next stage starts the following launch. A failure reported
|
|
358
|
-
* once the run completed, when its completed state could not be saved,
|
|
359
|
-
* ends the same launch.
|
|
360
|
-
* @param {Block[]} blocks
|
|
361
|
-
* @returns {Launch[]}
|
|
362
|
-
*/
|
|
363
|
-
export function launches(blocks) {
|
|
364
|
-
/** @type {Launch[]} */
|
|
365
|
-
const found = [];
|
|
366
|
-
blocks.forEach((block, index) => {
|
|
367
|
-
let current = found.at(-1);
|
|
368
|
-
if (!current || current.status === "failed") {
|
|
369
|
-
current = {
|
|
370
|
-
number: found.length + 1,
|
|
371
|
-
startedAt: block.startedAt,
|
|
372
|
-
endedAt: null,
|
|
373
|
-
status: "running",
|
|
374
|
-
stages: [],
|
|
375
|
-
};
|
|
376
|
-
found.push(current);
|
|
377
|
-
}
|
|
378
|
-
current.stages.push({ block, index });
|
|
379
|
-
if (block.status === "failed" || block.status === "completed") {
|
|
380
|
-
current.status = block.status;
|
|
381
|
-
current.endedAt =
|
|
382
|
-
block.durationMs === null
|
|
383
|
-
? block.startedAt
|
|
384
|
-
: new Date(
|
|
385
|
-
Date.parse(block.startedAt) + block.durationMs,
|
|
386
|
-
).toISOString();
|
|
387
|
-
if (block.detail) current.cause = block.detail;
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
return found;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* What a stage execution did, in one line: its failure, its agent's model,
|
|
395
|
-
* tools and cost, its decision, or its last journal message.
|
|
396
|
-
* @param {Block} block
|
|
397
|
-
* @returns {string}
|
|
398
|
-
*/
|
|
399
|
-
export function summary(block) {
|
|
400
|
-
if (block.status === "failed" && block.detail) return block.detail;
|
|
401
|
-
if (block.call?.model) {
|
|
402
|
-
const tools = `${block.tools.length} tool${block.tools.length === 1 ? "" : "s"}`;
|
|
403
|
-
const usd = block.call.usage?.costUsd;
|
|
404
|
-
return [
|
|
405
|
-
block.call.model,
|
|
406
|
-
tools,
|
|
407
|
-
...(usd === undefined ? [] : [dollars(usd, false)]),
|
|
408
|
-
].join(" · ");
|
|
409
|
-
}
|
|
410
|
-
if (block.decision) {
|
|
411
|
-
const { verdict, reason, destination } = block.decision;
|
|
412
|
-
return verdict === "correction"
|
|
413
|
-
? `back to ${destination}: ${reason.split("\n")[0]}`
|
|
414
|
-
: `accepted → ${destination ?? "end"}`;
|
|
415
|
-
}
|
|
416
|
-
return block.journal.at(-1)?.message ?? "";
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* The tools a stage execution used, by name, the most used first: how many
|
|
421
|
-
* calls, how long in all and how many failed.
|
|
422
|
-
* @param {Tool[]} tools
|
|
423
|
-
* @returns {{ name: string; count: number; durationMs: number; failed: number }[]}
|
|
424
|
-
*/
|
|
425
|
-
export function toolUse(tools) {
|
|
426
|
-
/** @type {Map<string, { name: string; count: number; durationMs: number; failed: number }>} */
|
|
427
|
-
const byName = new Map();
|
|
428
|
-
for (const tool of tools) {
|
|
429
|
-
const entry = byName.get(tool.name) ?? {
|
|
430
|
-
name: tool.name,
|
|
431
|
-
count: 0,
|
|
432
|
-
durationMs: 0,
|
|
433
|
-
failed: 0,
|
|
434
|
-
};
|
|
435
|
-
entry.count++;
|
|
436
|
-
entry.durationMs += tool.durationMs ?? 0;
|
|
437
|
-
if (tool.status === "failed") entry.failed++;
|
|
438
|
-
byName.set(tool.name, entry);
|
|
439
|
-
}
|
|
440
|
-
return [...byName.values()].sort(
|
|
441
|
-
(a, b) => b.count - a.count || a.name.localeCompare(b.name),
|
|
442
|
-
);
|
|
443
|
-
}
|