jev-devtools 0.1.3 → 0.3.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.
Files changed (52) hide show
  1. package/README.md +50 -4
  2. package/THIRD_PARTY_NOTICES.txt +27 -0
  3. package/lib/flow/index.d.ts +82 -0
  4. package/lib/flow/index.js +588 -0
  5. package/lib/server/flows.d.ts +25 -0
  6. package/lib/server/flows.js +115 -0
  7. package/lib/server/index.d.ts +8 -0
  8. package/lib/server/index.js +163 -9
  9. package/lib/server/recorder.d.ts +12 -2
  10. package/lib/server/recorder.js +47 -9
  11. package/lib/src/ChoiceProbabilities.d.ts +4 -0
  12. package/lib/src/ChoiceProbabilities.js +19 -0
  13. package/lib/src/ConnectedInspector.js +46 -29
  14. package/lib/src/DiagramContent.d.ts +6 -0
  15. package/lib/src/DiagramContent.js +32 -0
  16. package/lib/src/FlowOverview.d.ts +7 -0
  17. package/lib/src/FlowOverview.js +228 -0
  18. package/lib/src/JevDevtools.d.ts +6 -1
  19. package/lib/src/JevDevtools.js +52 -48
  20. package/lib/src/JevDrawer.d.ts +3 -2
  21. package/lib/src/JevDrawer.js +12 -3
  22. package/lib/src/Questions.d.ts +5 -0
  23. package/lib/src/Questions.js +174 -0
  24. package/lib/src/TraceContent.d.ts +11 -0
  25. package/lib/src/TraceContent.js +32 -0
  26. package/lib/src/TraceDiagram.d.ts +4 -1
  27. package/lib/src/TraceDiagram.js +111 -25
  28. package/lib/src/condition-inputs.d.ts +9 -0
  29. package/lib/src/condition-inputs.js +100 -0
  30. package/lib/src/flow-condition.d.ts +4 -0
  31. package/lib/src/flow-condition.js +47 -0
  32. package/lib/src/flow-model.d.ts +7 -2
  33. package/lib/src/flow-model.js +3 -12
  34. package/lib/src/flow-protocol.d.ts +68 -0
  35. package/lib/src/flow-protocol.js +1 -0
  36. package/lib/src/flow-stream.d.ts +8 -0
  37. package/lib/src/flow-stream.js +80 -0
  38. package/lib/src/index.d.ts +1 -1
  39. package/lib/src/inspector-styles.js +1 -1
  40. package/lib/src/inspector.css +725 -8
  41. package/lib/src/protocol.d.ts +18 -0
  42. package/lib/src/trace-store.d.ts +6 -0
  43. package/lib/src/trace-store.js +66 -0
  44. package/lib/src/trace-stream.d.ts +2 -0
  45. package/lib/src/trace-stream.js +65 -0
  46. package/lib/src/useDiagramViewport.d.ts +5 -0
  47. package/lib/src/useDiagramViewport.js +34 -6
  48. package/package.json +7 -2
  49. package/skills/setup-jev-devtools/SKILL.md +6 -2
  50. package/skills/setup-jev-devtools/references/flows.md +150 -0
  51. package/skills/setup-jev-devtools/references/nextjs.md +1 -1
  52. package/skills/setup-jev-devtools/references/verify.md +3 -1
@@ -0,0 +1,588 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const owners = new WeakMap();
3
+ function freezeAst(ast, depth = 0) {
4
+ if (depth > 80)
5
+ throw new Error("Expression nesting exceeds 80");
6
+ if (Object.isFrozen(ast))
7
+ return;
8
+ if (ast.op === "get")
9
+ freezeAst(ast.from, depth + 1);
10
+ if ("args" in ast) {
11
+ if (ast.args.length > 256)
12
+ throw new Error("Too many condition operands");
13
+ for (const child of ast.args)
14
+ freezeAst(child, depth + 1);
15
+ Object.freeze(ast.args);
16
+ }
17
+ Object.freeze(ast);
18
+ }
19
+ /** Inspectable expression, not an application callback. Use get() for input fields. */
20
+ export class FlowExpression {
21
+ ast;
22
+ constructor(ast, owner) {
23
+ this.ast = ast;
24
+ if (owner)
25
+ owners.set(this, owner);
26
+ freezeAst(ast);
27
+ Object.freeze(this);
28
+ }
29
+ get(key) {
30
+ return expression({ op: "get", from: this.ast, key: String(key) }, [this]);
31
+ }
32
+ eq(value) {
33
+ return operation("eq", [this, value]);
34
+ }
35
+ lt(value) {
36
+ return operation("lt", [this, value]);
37
+ }
38
+ gte(value) {
39
+ return operation("gte", [this, value]);
40
+ }
41
+ }
42
+ function expression(ast, values) {
43
+ const tokens = new Set(values
44
+ .filter((v) => v instanceof FlowExpression)
45
+ .map((v) => owners.get(v))
46
+ .filter(Boolean));
47
+ if (tokens.size > 1)
48
+ throw new Error("References belong to different flows");
49
+ return new FlowExpression(ast, tokens.values().next().value);
50
+ }
51
+ function operation(op, args) {
52
+ return expression({
53
+ op,
54
+ args: args.map((v) => v instanceof FlowExpression ? v.ast : { op: "literal", value: v }),
55
+ }, args);
56
+ }
57
+ export const all = (...conditions) => operation("all", conditions);
58
+ export const any = (...conditions) => operation("any", conditions);
59
+ export const not = (condition) => operation("not", [condition]);
60
+ const plain = (v) => !!v &&
61
+ typeof v === "object" &&
62
+ (Object.getPrototypeOf(v) === Object.prototype ||
63
+ Object.getPrototypeOf(v) === null);
64
+ function mapValue(value, ref, seen = new Set(), depth = 0) {
65
+ if (depth > 40)
66
+ throw new Error("Flow value nesting exceeds 40");
67
+ if (value instanceof FlowExpression)
68
+ return ref(value);
69
+ if (typeof value === "function")
70
+ throw new Error("Flow values cannot contain callbacks");
71
+ if (!Array.isArray(value) && !plain(value))
72
+ return value; // Preserve SDK assets.
73
+ if (seen.has(value))
74
+ throw new Error("Cyclic flow value");
75
+ seen.add(value);
76
+ const result = Array.isArray(value)
77
+ ? value.map((v) => mapValue(v, ref, seen, depth + 1))
78
+ : Object.fromEntries(Object.entries(value).map(([k, v]) => [
79
+ k,
80
+ mapValue(v, ref, seen, depth + 1),
81
+ ]));
82
+ seen.delete(value);
83
+ return result;
84
+ }
85
+ const inspectAst = (ast, ref) => {
86
+ if (ast.op === "ref")
87
+ ref(ast.id);
88
+ else if (ast.op === "get")
89
+ inspectAst(ast.from, ref);
90
+ else if ("args" in ast)
91
+ ast.args.forEach((a) => inspectAst(a, ref));
92
+ };
93
+ function describe(ast) {
94
+ if (ast.op === "input")
95
+ return "input";
96
+ if (ast.op === "ref")
97
+ return ast.id;
98
+ if (ast.op === "get")
99
+ return `${describe(ast.from)}.${ast.key}`;
100
+ if (ast.op === "literal")
101
+ return "[literal]";
102
+ return `${ast.op}(${ast.args.map(describe).join(", ")})`;
103
+ }
104
+ function conditionRule(ast, requests) {
105
+ let remaining = 128;
106
+ const visit = (node) => {
107
+ if (--remaining < 0)
108
+ return { op: "literal", value: "[rule truncated]" };
109
+ if (node.op === "input" || node.op === "ref" || node.op === "get") {
110
+ const path = [];
111
+ let base = node;
112
+ while (base.op === "get") {
113
+ path.unshift(base.key);
114
+ base = base.from;
115
+ }
116
+ const answer = base.op === "ref" && requests.has(base.id) && path[0] === "answers";
117
+ const probability = answer && path.length === 3 && path[2] === "noul";
118
+ const root = base.op === "input"
119
+ ? "input"
120
+ : base.op === "ref"
121
+ ? base.id
122
+ : describe(base);
123
+ const fields = probability
124
+ ? path.slice(1, -1)
125
+ : answer
126
+ ? path.slice(1)
127
+ : path;
128
+ const label = root +
129
+ fields
130
+ .map((key) => /^[a-zA-Z_$][\w$]*$/.test(key)
131
+ ? `.${key}`
132
+ : `[${JSON.stringify(key)}]`)
133
+ .join("");
134
+ return {
135
+ op: "reference",
136
+ label,
137
+ probability,
138
+ name: answer
139
+ ? path.length === 3 && ["noul", "choice", "score"].includes(path[2])
140
+ ? path[1]
141
+ : path.slice(1).join(" ")
142
+ : (path.at(-1) ?? "Input"),
143
+ ...(base.op === "ref" && requests.has(base.id)
144
+ ? { stepId: base.id, path }
145
+ : {}),
146
+ };
147
+ }
148
+ if (node.op === "literal")
149
+ return {
150
+ op: "literal",
151
+ value: node.value === null ||
152
+ typeof node.value === "string" ||
153
+ typeof node.value === "boolean" ||
154
+ (typeof node.value === "number" && Number.isFinite(node.value))
155
+ ? node.value
156
+ : "[value unavailable]",
157
+ };
158
+ const args = [];
159
+ for (const arg of node.args) {
160
+ args.push(visit(arg));
161
+ if (remaining < 0)
162
+ break;
163
+ }
164
+ return { op: node.op, args };
165
+ };
166
+ return visit(ast);
167
+ }
168
+ function evaluate(ast, input, values) {
169
+ if (ast.op === "input")
170
+ return input;
171
+ if (ast.op === "literal")
172
+ return ast.value;
173
+ if (ast.op === "ref") {
174
+ if (!values.has(ast.id))
175
+ throw new Error(`Unavailable result: ${ast.id}`);
176
+ return values.get(ast.id);
177
+ }
178
+ if (ast.op === "get") {
179
+ const obj = evaluate(ast.from, input, values);
180
+ if (obj === null ||
181
+ typeof obj !== "object" ||
182
+ !Object.hasOwn(obj, ast.key) ||
183
+ obj[ast.key] === undefined)
184
+ throw new Error(`Missing value: ${describe(ast)}`);
185
+ return obj[ast.key];
186
+ }
187
+ // Evaluate every operand: a missing value must never hide behind short-circuiting.
188
+ const args = ast.args.map((a) => evaluate(a, input, values));
189
+ if (ast.op === "all" || ast.op === "any" || ast.op === "not") {
190
+ if (args.some((v) => typeof v !== "boolean"))
191
+ throw new Error("Boolean condition required");
192
+ return ast.op === "not"
193
+ ? !args[0]
194
+ : ast.op === "all"
195
+ ? args.every(Boolean)
196
+ : args.some(Boolean);
197
+ }
198
+ const [a, b] = args;
199
+ if (ast.op === "eq") {
200
+ if (a === undefined ||
201
+ b === undefined ||
202
+ (a !== null && !["string", "number", "boolean"].includes(typeof a)) ||
203
+ (b !== null && !["string", "number", "boolean"].includes(typeof b)) ||
204
+ typeof a !== typeof b ||
205
+ (typeof a === "number" && (!Number.isFinite(a) || !Number.isFinite(b))))
206
+ throw new Error("eq requires matching scalar values");
207
+ return a === b;
208
+ }
209
+ if (typeof a !== "number" ||
210
+ typeof b !== "number" ||
211
+ !Number.isFinite(a) ||
212
+ !Number.isFinite(b))
213
+ throw new Error("Numeric comparison requires finite numbers");
214
+ return ast.op === "lt" ? a < b : a >= b;
215
+ }
216
+ export function defineJevFlow(name, build) {
217
+ const owner = {}, ids = new Set(), nodes = [];
218
+ const input = new FlowExpression({ op: "input" }, owner);
219
+ let building = true;
220
+ let activeBlock;
221
+ function block(callback, available, path, previous = [], depth = 0, requireOutcome = false) {
222
+ if (depth > 16)
223
+ throw new Error("Flow nesting exceeds 16");
224
+ const token = {};
225
+ const result = { nodes: [] };
226
+ let terminal = false;
227
+ const validate = (value) => mapValue(value, (expr) => {
228
+ if (owners.get(expr) !== owner)
229
+ throw new Error("Foreign or unowned flow reference");
230
+ inspectAst(expr.ast, (id) => {
231
+ if (!available.has(id))
232
+ throw new Error(`Unavailable reference: ${id}`);
233
+ });
234
+ return expr;
235
+ });
236
+ function add(id, kind, value, display) {
237
+ if (!building)
238
+ throw new Error("Flow definition is closed");
239
+ if (activeBlock !== token)
240
+ throw new Error("Use the current branch or lane builder for steps");
241
+ if (terminal)
242
+ throw new Error("No steps allowed after a terminal output or branch");
243
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$/.test(id) || ids.has(id))
244
+ throw new Error(`Invalid or duplicate step ID: ${id}`);
245
+ if (ids.size >= 256)
246
+ throw new Error("Flows support at most 256 steps");
247
+ const copy = {};
248
+ if (display !== undefined) {
249
+ if (!display || typeof display !== "object" || Array.isArray(display))
250
+ throw new Error("Step display metadata must be an object");
251
+ for (const [key, max] of [
252
+ ["title", 120],
253
+ ["description", 600],
254
+ ]) {
255
+ const text = display[key];
256
+ if (text === undefined)
257
+ continue;
258
+ if (typeof text !== "string" || !text.trim() || text.length > max)
259
+ throw new Error(`${key} must be a non-empty string of at most ${max} characters`);
260
+ copy[key] = text.trim();
261
+ }
262
+ }
263
+ ids.add(id);
264
+ validate(value);
265
+ const dependencies = new Set(previous);
266
+ mapValue(value, (expr) => {
267
+ inspectAst(expr.ast, (r) => dependencies.add(r));
268
+ return expr;
269
+ });
270
+ nodes.push({
271
+ id,
272
+ kind,
273
+ dependencies: [...dependencies],
274
+ path: [...path],
275
+ ...copy,
276
+ });
277
+ previous = [id];
278
+ available.add(id);
279
+ const step = { id, kind };
280
+ result.nodes.push(step);
281
+ return step;
282
+ }
283
+ const builder = {
284
+ input,
285
+ jev(id, request, display) {
286
+ const frozen = mapValue(request, (expr) => expr);
287
+ const step = add(id, "jev", frozen, display);
288
+ step.request = frozen;
289
+ return {
290
+ answer(question) {
291
+ if (!Object.hasOwn(frozen.questions, question))
292
+ throw new Error(`Unknown question: ${question}`);
293
+ const type = frozen.questions[question].type;
294
+ const fields = type === "noul"
295
+ ? ["type", "noul"]
296
+ : type === "choice"
297
+ ? ["type", "choice", "confidence", "probabilities"]
298
+ : type === "score"
299
+ ? ["type", "score", "confidence", "probabilities", "legend"]
300
+ : ["type", "box", "confidence"];
301
+ return Object.fromEntries(fields.map((field) => [
302
+ field,
303
+ new FlowExpression({
304
+ op: "get",
305
+ from: {
306
+ op: "get",
307
+ from: {
308
+ op: "get",
309
+ from: { op: "ref", id },
310
+ key: "answers",
311
+ },
312
+ key: question,
313
+ },
314
+ key: field,
315
+ }, owner),
316
+ ]));
317
+ },
318
+ };
319
+ },
320
+ branch(id, paths, display) {
321
+ const step = add(id, "branch", paths.when, display);
322
+ step.when = paths.when;
323
+ if (!(paths.when instanceof FlowExpression))
324
+ throw new Error("branch requires a condition expression");
325
+ const node = nodes.find((n) => n.id === id);
326
+ node.condition = describe(paths.when.ast);
327
+ node.conditionRule = conditionRule(paths.when.ast, new Set(nodes.filter((n) => n.kind === "jev").map((n) => n.id)));
328
+ step.then = block(paths.then, new Set(available), [...path, `${id}:then`], [id], depth + 1, true);
329
+ step.else = block(paths.else, new Set(available), [...path, `${id}:else`], [id], depth + 1, true);
330
+ terminal = true;
331
+ return new FlowExpression({ op: "ref", id }, owner);
332
+ },
333
+ parallel(id, lanes, display) {
334
+ const prior = [...previous], base = new Set(available);
335
+ const step = add(id, "parallel", undefined, display);
336
+ step.lanes = {};
337
+ if (!Object.keys(lanes).length)
338
+ throw new Error("parallel requires at least one lane");
339
+ const refs = {};
340
+ const tails = [];
341
+ for (const [key, callback] of Object.entries(lanes)) {
342
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$/.test(key))
343
+ throw new Error("Invalid lane name");
344
+ const lane = block(callback, new Set(base), [...path, `${id}:${key}`], prior, depth + 1);
345
+ step.lanes[key] = lane;
346
+ tails.push(lane.nodes.at(-1)?.id ?? prior.at(-1) ?? "");
347
+ refs[key] = new FlowExpression({ op: "get", from: { op: "ref", id }, key }, owner);
348
+ }
349
+ nodes.find((n) => n.id === id).dependencies = tails.filter(Boolean);
350
+ nodes.find((n) => n.id === id).lanes = Object.keys(lanes);
351
+ return refs;
352
+ },
353
+ output(outcome, value, display) {
354
+ const step = add(outcome, "output", value, display);
355
+ step.outcome = outcome;
356
+ step.value = mapValue(value, (e) => e);
357
+ terminal = true;
358
+ return new FlowExpression({ op: "ref", id: outcome }, owner);
359
+ },
360
+ };
361
+ const parentBlock = activeBlock;
362
+ activeBlock = token;
363
+ let returned;
364
+ try {
365
+ returned = callback(builder);
366
+ }
367
+ finally {
368
+ activeBlock = parentBlock;
369
+ }
370
+ if (!(returned instanceof FlowExpression))
371
+ throw new Error("Flow callbacks must return an expression, output, or branch");
372
+ if (requireOutcome && !terminal)
373
+ throw new Error("Branch paths must end with output or branch");
374
+ if (terminal &&
375
+ (returned.ast.op !== "ref" || returned.ast.id !== result.nodes.at(-1).id))
376
+ throw new Error("Return the terminal output or branch");
377
+ validate(returned);
378
+ result.result = returned;
379
+ return result;
380
+ }
381
+ if (!name.trim() || name.length > 100)
382
+ throw new Error("Invalid flow name");
383
+ let program;
384
+ try {
385
+ program = block(build, new Set(), [], [], 0, true);
386
+ }
387
+ finally {
388
+ building = false;
389
+ }
390
+ if (!["branch", "output"].includes(program.nodes.at(-1)?.kind ?? ""))
391
+ throw new Error("Flow must end with output or branch");
392
+ const definition = {
393
+ version: 1,
394
+ id: randomUUID(),
395
+ name,
396
+ nodes,
397
+ };
398
+ const definitionView = structuredClone(definition);
399
+ const freezeRule = (rule) => {
400
+ if (!rule || typeof rule !== "object")
401
+ return;
402
+ for (const value of Object.values(rule))
403
+ freezeRule(value);
404
+ Object.freeze(rule);
405
+ };
406
+ for (const node of definitionView.nodes) {
407
+ Object.freeze(node.dependencies);
408
+ Object.freeze(node.path);
409
+ if (node.lanes)
410
+ Object.freeze(node.lanes);
411
+ freezeRule(node.conditionRule);
412
+ Object.freeze(node);
413
+ }
414
+ Object.freeze(definitionView.nodes);
415
+ Object.freeze(definitionView);
416
+ return {
417
+ definition: definitionView,
418
+ async run(data, options) {
419
+ const controller = new AbortController();
420
+ const cancel = () => controller.abort(options.signal?.reason);
421
+ if (options.signal?.aborted)
422
+ cancel();
423
+ else
424
+ options.signal?.addEventListener("abort", cancel, { once: true });
425
+ const run = {
426
+ version: 1,
427
+ id: randomUUID(),
428
+ definitionId: definition.id,
429
+ name,
430
+ startedAt: Date.now(),
431
+ status: "pending",
432
+ steps: Object.fromEntries(nodes.map((n) => [
433
+ n.id,
434
+ { id: n.id, executionId: randomUUID(), status: "waiting" },
435
+ ])),
436
+ };
437
+ const started = performance.now(), values = new Map();
438
+ let observer;
439
+ try {
440
+ observer = options.devtools?.startFlow(structuredClone(definition), structuredClone(run));
441
+ }
442
+ catch {
443
+ /* Recording cannot stop execution. */
444
+ }
445
+ const emit = () => {
446
+ try {
447
+ if (observer) {
448
+ const { value, ...metadata } = run;
449
+ observer.update({
450
+ ...structuredClone(metadata),
451
+ ...(value !== undefined ? { value } : {}),
452
+ });
453
+ }
454
+ }
455
+ catch {
456
+ /* Best effort capture. */
457
+ }
458
+ };
459
+ const abortCheck = () => {
460
+ if (controller.signal.aborted)
461
+ throw new DOMException("Flow cancelled", "AbortError");
462
+ };
463
+ const mark = (b, status) => {
464
+ for (const step of b.nodes) {
465
+ if (run.steps[step.id].status === "waiting")
466
+ run.steps[step.id].status = status;
467
+ if (step.then)
468
+ mark(step.then, status);
469
+ if (step.else)
470
+ mark(step.else, status);
471
+ for (const lane of Object.values(step.lanes ?? {}))
472
+ mark(lane, status);
473
+ }
474
+ };
475
+ async function execute(b) {
476
+ for (const step of b.nodes) {
477
+ abortCheck();
478
+ const state = run.steps[step.id], time = performance.now();
479
+ state.status = "running";
480
+ state.startedAt = Date.now();
481
+ emit();
482
+ try {
483
+ let value;
484
+ if (step.kind === "jev") {
485
+ const request = mapValue(step.request, (e) => evaluate(e.ast, data, values));
486
+ const call = () => options.client.systemOne(request, {
487
+ signal: controller.signal,
488
+ });
489
+ const link = {
490
+ runId: run.id,
491
+ stepId: step.id,
492
+ executionId: state.executionId,
493
+ };
494
+ value = await (observer
495
+ ? observer.request(link, (id) => {
496
+ state.traceId = id;
497
+ emit();
498
+ }, call)
499
+ : call());
500
+ abortCheck();
501
+ }
502
+ else if (step.kind === "branch") {
503
+ const decision = evaluate(step.when.ast, data, values);
504
+ if (typeof decision !== "boolean")
505
+ throw new Error("Branch condition did not return a boolean");
506
+ state.decision = decision;
507
+ mark(decision ? step.else : step.then, "untaken");
508
+ emit();
509
+ value = await execute(decision ? step.then : step.else);
510
+ }
511
+ else if (step.kind === "parallel") {
512
+ const entries = Object.entries(step.lanes);
513
+ let firstError;
514
+ let failed = false;
515
+ const results = await Promise.allSettled(entries.map(async ([, lane]) => {
516
+ try {
517
+ return await execute(lane);
518
+ }
519
+ catch (e) {
520
+ if (!failed) {
521
+ firstError = e;
522
+ failed = true;
523
+ }
524
+ controller.abort(e);
525
+ throw e;
526
+ }
527
+ }));
528
+ if (failed)
529
+ throw firstError;
530
+ value = Object.fromEntries(results.map((r, i) => [
531
+ entries[i][0],
532
+ r.value,
533
+ ]));
534
+ }
535
+ else
536
+ value = {
537
+ outcome: step.outcome,
538
+ value: mapValue(step.value, (e) => evaluate(e.ast, data, values)),
539
+ };
540
+ values.set(step.id, value);
541
+ state.status = "completed";
542
+ }
543
+ catch (error) {
544
+ state.status =
545
+ error instanceof Error &&
546
+ ["AbortError", "APIUserAbortError"].includes(error.name)
547
+ ? "cancelled"
548
+ : "error";
549
+ state.error = error instanceof Error ? error.name : "FlowError";
550
+ throw error;
551
+ }
552
+ finally {
553
+ state.durationMs = performance.now() - time;
554
+ emit();
555
+ }
556
+ }
557
+ return evaluate(b.result.ast, data, values);
558
+ }
559
+ try {
560
+ const result = (await execute(program));
561
+ abortCheck();
562
+ if (!result || typeof result.outcome !== "string")
563
+ throw new Error("Flow must return a named output");
564
+ run.status = "success";
565
+ run.outcome = result.outcome;
566
+ // Only the recorder snapshots output data; runtime keeps the actual typed value.
567
+ if (result.value !== undefined)
568
+ run.value = result.value;
569
+ return { runId: run.id, ...result };
570
+ }
571
+ catch (error) {
572
+ run.status =
573
+ error instanceof Error &&
574
+ ["AbortError", "APIUserAbortError"].includes(error.name)
575
+ ? "cancelled"
576
+ : "error";
577
+ run.error = error instanceof Error ? error.name : "FlowError";
578
+ mark(program, run.status === "cancelled" ? "cancelled" : "blocked");
579
+ throw error;
580
+ }
581
+ finally {
582
+ run.durationMs = performance.now() - started;
583
+ emit();
584
+ options.signal?.removeEventListener("abort", cancel);
585
+ }
586
+ },
587
+ };
588
+ }
@@ -0,0 +1,25 @@
1
+ import type { FlowDefinition, FlowRun, FlowUpdate } from "../src/flow-protocol.js";
2
+ import { type CaptureOptions } from "./recorder.js";
3
+ /** Independent bounded run store. Definitions are retained only while referenced. */
4
+ export declare class FlowRecorder {
5
+ private options;
6
+ private runs;
7
+ private definitions;
8
+ private sessions;
9
+ private definitionRevisions;
10
+ private revisions;
11
+ private revision;
12
+ private listeners;
13
+ constructor(options: CaptureOptions);
14
+ subscribe(fn: (sessions: Set<string>) => void): () => void;
15
+ private notify;
16
+ start(definition: FlowDefinition, run: FlowRun, session: string): void;
17
+ update(run: FlowRun): void;
18
+ private remove;
19
+ private prune;
20
+ clear(session: string): void;
21
+ changes(session: string, cursor?: number): {
22
+ revision: number;
23
+ data: FlowUpdate;
24
+ };
25
+ }