@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.
@@ -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
- }