@relayflows/sdk 2.0.34 → 2.0.35

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 (53) hide show
  1. package/dist/authored-dispatch.d.ts +24 -0
  2. package/dist/authored-dispatch.d.ts.map +1 -0
  3. package/dist/authored-dispatch.js +81 -0
  4. package/dist/authored-dispatch.js.map +1 -0
  5. package/dist/authored-flow-error.d.ts +1 -1
  6. package/dist/authored-flow-error.d.ts.map +1 -1
  7. package/dist/authored-flow-error.js.map +1 -1
  8. package/dist/authored-flow-executor.d.ts +17 -0
  9. package/dist/authored-flow-executor.d.ts.map +1 -1
  10. package/dist/authored-flow-executor.js +92 -16
  11. package/dist/authored-flow-executor.js.map +1 -1
  12. package/dist/authored-flow-loader.d.ts.map +1 -1
  13. package/dist/authored-flow-loader.js +9 -0
  14. package/dist/authored-flow-loader.js.map +1 -1
  15. package/dist/authored-node-entry.js +1 -0
  16. package/dist/authored-node-entry.js.map +1 -1
  17. package/dist/authored-root.js +1 -0
  18. package/dist/authored-root.js.map +1 -1
  19. package/dist/authored-worker-step.d.ts +3 -1
  20. package/dist/authored-worker-step.d.ts.map +1 -1
  21. package/dist/authored-worker-step.js +18 -7
  22. package/dist/authored-worker-step.js.map +1 -1
  23. package/dist/cli/run.d.ts +5 -0
  24. package/dist/cli/run.d.ts.map +1 -1
  25. package/dist/cli/run.js +8 -0
  26. package/dist/cli/run.js.map +1 -1
  27. package/dist/cli.d.ts.map +1 -1
  28. package/dist/cli.js +3 -0
  29. package/dist/cli.js.map +1 -1
  30. package/dist/cloud-mirror-step.d.ts +2 -0
  31. package/dist/cloud-mirror-step.d.ts.map +1 -1
  32. package/dist/cloud-mirror-step.js +9 -1
  33. package/dist/cloud-mirror-step.js.map +1 -1
  34. package/dist/cloud-mirror.d.ts.map +1 -1
  35. package/dist/cloud-mirror.js +17 -2
  36. package/dist/cloud-mirror.js.map +1 -1
  37. package/dist/worker-slots.d.ts +15 -0
  38. package/dist/worker-slots.d.ts.map +1 -1
  39. package/dist/worker-slots.js +40 -3
  40. package/dist/worker-slots.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/authored-dispatch.ts +113 -0
  43. package/src/authored-flow-error.ts +3 -0
  44. package/src/authored-flow-executor.ts +126 -15
  45. package/src/authored-flow-loader.ts +12 -0
  46. package/src/authored-node-entry.ts +1 -0
  47. package/src/authored-root.ts +1 -0
  48. package/src/authored-worker-step.ts +17 -6
  49. package/src/cli/run.ts +10 -0
  50. package/src/cli.ts +3 -0
  51. package/src/cloud-mirror-step.ts +16 -2
  52. package/src/cloud-mirror.ts +15 -2
  53. package/src/worker-slots.ts +63 -4
@@ -152,6 +152,8 @@ export interface MirroredJournal {
152
152
  children: string[];
153
153
  /** Authored graph hints by step id, from an authored root's step index. */
154
154
  hints: Map<string, { label?: string; after?: string[] }>;
155
+ /** Authored completion receipts are durable evidence, not graph nodes. */
156
+ hidden: string[];
155
157
  }
156
158
 
157
159
  type Payload = Record<string, unknown>;
@@ -507,6 +509,7 @@ export function mirrorJournal(
507
509
  transcripts,
508
510
  children: index.children,
509
511
  hints: index.hints,
512
+ hidden: index.hidden,
510
513
  };
511
514
  }
512
515
 
@@ -522,7 +525,11 @@ export function mirrorJournal(
522
525
  function authoredIndex(
523
526
  events: readonly JournalEvent[],
524
527
  env: NodeJS.ProcessEnv,
525
- ): { children: string[]; hints: Map<string, { label?: string; after?: string[] }> } {
528
+ ): {
529
+ children: string[];
530
+ hints: Map<string, { label?: string; after?: string[] }>;
531
+ hidden: string[];
532
+ } {
526
533
  const messages: unknown[] = [];
527
534
  for (const event of events) {
528
535
  if (event.entry_type !== 'stream.appended') continue;
@@ -531,8 +538,15 @@ function authoredIndex(
531
538
  }
532
539
  const children: string[] = [];
533
540
  const hints = new Map<string, { label?: string; after?: string[] }>();
541
+ const hidden: string[] = [];
534
542
  for (const entry of foldAuthoredStepRecords(messages).values()) {
535
543
  if (/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(entry.runId)) children.push(entry.runId);
544
+ // This is the executor's durable verdict receipt, not an authored unit of
545
+ // work. Only an entry in the authored root index can be hidden here, so a
546
+ // declarative step coincidentally named `complete-N` remains visible.
547
+ if (/(?:^|--)complete-[1-9][0-9]*$/.test(entry.step)) {
548
+ hidden.push(`${entry.runId}/${entry.step}`);
549
+ }
536
550
  const name = label(entry.label, env);
537
551
  const after = entry.after === undefined
538
552
  ? undefined
@@ -546,7 +560,7 @@ function authoredIndex(
546
560
  ...(after === undefined ? {} : { after }),
547
561
  });
548
562
  }
549
- return { children, hints };
563
+ return { children, hints, hidden };
550
564
  }
551
565
 
552
566
  /**
@@ -287,6 +287,7 @@ export function createRunMirror(options: RunMirrorOptions): RunMirror {
287
287
  const finals = new Map<string, { journalRunId: string; row: FinalStep }>();
288
288
  const transcripts = new Map<string, { stepName: string; attempts: Array<{ attempt: number; path: string }> }>();
289
289
  const hints = new Map<string, { label?: string; after?: string[] }>();
290
+ const hidden = new Set<string>();
290
291
  /** Live steps the cache cap dropped, by identity, so a capped view owns up to it. */
291
292
  const evicted = new Set<string>();
292
293
  /** Finished steps past the report cap, by identity. Counted, never sent. */
@@ -318,6 +319,7 @@ export function createRunMirror(options: RunMirrorOptions): RunMirror {
318
319
 
319
320
  const remember = (journalRunId: string, step: SnapshotStep, order: number): void => {
320
321
  const key = `${journalRunId}/${step.stepName}`;
322
+ if (hidden.has(key)) return;
321
323
  const existing = steps.get(key);
322
324
  const changed = existing === undefined || fingerprint(existing.step) !== fingerprint(step);
323
325
  steps.set(key, { step, order, changedAt: changed ? now() : existing!.changedAt });
@@ -365,19 +367,30 @@ export function createRunMirror(options: RunMirrorOptions): RunMirror {
365
367
  continue;
366
368
  }
367
369
  for (const child of folded.children) admit(child);
370
+ for (const key of folded.hidden) {
371
+ hidden.add(key);
372
+ steps.delete(key);
373
+ finals.delete(key);
374
+ transcripts.delete(key);
375
+ }
368
376
  for (const [key, hint] of folded.hints) {
369
377
  // Merge, never replace: a completion record can omit a label or an
370
378
  // edge the admission carried, and the view must not lose a step's
371
379
  // name between them.
372
380
  const existing = hints.get(key);
373
381
  hints.set(key, {
374
- ...(hint.label ?? existing?.label ? { label: hint.label ?? existing?.label } : {}),
375
- ...(hint.after ?? existing?.after ? { after: hint.after ?? existing?.after } : {}),
382
+ // The root's authored index is scanned before the child journal and
383
+ // carries causal edges across child runs. A child spec only knows
384
+ // its own single-step graph (`depends_on: []`), so it may fill a
385
+ // missing field but must never erase the root's richer answer.
386
+ ...(existing?.label ?? hint.label ? { label: existing?.label ?? hint.label } : {}),
387
+ ...(existing?.after ?? hint.after ? { after: existing?.after ?? hint.after } : {}),
376
388
  });
377
389
  }
378
390
  folded.steps.forEach((step, order) => remember(runId, step, order));
379
391
  for (const row of folded.finals) {
380
392
  const key = `${runId}/${row.stepName}`;
393
+ if (hidden.has(key)) continue;
381
394
  // The report cap bounds what this process holds, not just what it
382
395
  // sends: a run with ten thousand steps must not grow its own mirror.
383
396
  // Steps past it are counted by identity, so a step read twice is one
@@ -7,6 +7,16 @@ export function isAgentCapacity(value: unknown): value is number {
7
7
  return Number.isInteger(value) && (value as number) >= 1 && (value as number) <= MAX_LOCAL_AGENT_CAPACITY;
8
8
  }
9
9
 
10
+ export interface WorkerSlotScope {
11
+ run<T>(work: () => Promise<T>): Promise<T>;
12
+ /** Refuse this flow's queued/future work without closing the shared pool. */
13
+ close(reason: unknown): void;
14
+ }
15
+
16
+ interface ScopeState {
17
+ closed?: { readonly reason: unknown };
18
+ }
19
+
10
20
  /**
11
21
  * First-come admission to a worker that holds `capacity` dispatches at once.
12
22
  *
@@ -19,7 +29,11 @@ export function isAgentCapacity(value: unknown): value is number {
19
29
  */
20
30
  export class WorkerSlots {
21
31
  private held = 0;
22
- private readonly waiting: Array<{ resolve: () => void; reject: (reason: unknown) => void }> = [];
32
+ private readonly waiting: Array<{
33
+ resolve: () => void;
34
+ reject: (reason: unknown) => void;
35
+ scope?: ScopeState;
36
+ }> = [];
23
37
  private closed: { readonly reason: unknown } | undefined;
24
38
 
25
39
  constructor(readonly capacity: number) {
@@ -27,18 +41,37 @@ export class WorkerSlots {
27
41
  }
28
42
 
29
43
  async run<T>(work: () => Promise<T>): Promise<T> {
44
+ return this.runInScope(work);
45
+ }
46
+
47
+ /** One independently closable admission scope over this shared capacity. */
48
+ scope(): WorkerSlotScope {
49
+ const scope: ScopeState = {};
50
+ return Object.freeze({
51
+ run: <T>(work: () => Promise<T>) => this.runInScope(work, scope),
52
+ close: (reason: unknown) => this.closeScope(scope, reason),
53
+ });
54
+ }
55
+
56
+ private async runInScope<T>(work: () => Promise<T>, scope?: ScopeState): Promise<T> {
30
57
  if (this.closed !== undefined) throw this.closed.reason;
58
+ if (scope?.closed !== undefined) throw scope.closed.reason;
31
59
  if (this.held < this.capacity) this.held++;
32
60
  // A released slot is handed straight to the next waiter, so `held` never
33
61
  // dips below capacity while anyone is queued and no later caller can jump it.
34
62
  else {
35
- await new Promise<void>((resolve, reject) => this.waiting.push({ resolve, reject }));
63
+ await new Promise<void>((resolve, reject) => this.waiting.push({ resolve, reject, scope }));
36
64
  // Woken with the slot, but the body may have failed in between (see release).
37
65
  const closed = this.closedReason();
38
66
  if (closed !== undefined) {
39
67
  this.release();
40
68
  throw closed.reason;
41
69
  }
70
+ const scopeClosed = scopeClosedReason(scope);
71
+ if (scopeClosed !== undefined) {
72
+ this.release();
73
+ throw scopeClosed.reason;
74
+ }
42
75
  }
43
76
  try {
44
77
  return await work();
@@ -66,17 +99,28 @@ export class WorkerSlots {
66
99
  return;
67
100
  }
68
101
  setImmediate(() => {
69
- if (this.closed === undefined) {
102
+ if (this.closed === undefined && next.scope?.closed === undefined) {
70
103
  next.resolve();
71
104
  return;
72
105
  }
73
106
  // Closed meanwhile. `close` could not see this waiter (already taken off
74
107
  // the queue), so refuse it here, and pass the slot on instead of leaking it.
75
- next.reject(this.closed.reason);
108
+ next.reject(this.closed?.reason ?? next.scope!.closed!.reason);
76
109
  this.release();
77
110
  });
78
111
  }
79
112
 
113
+ private closeScope(scope: ScopeState, reason: unknown): void {
114
+ if (scope.closed !== undefined) return;
115
+ scope.closed = { reason };
116
+ for (let index = this.waiting.length - 1; index >= 0; index--) {
117
+ const waiter = this.waiting[index]!;
118
+ if (waiter.scope !== scope) continue;
119
+ this.waiting.splice(index, 1);
120
+ waiter.reject(reason);
121
+ }
122
+ }
123
+
80
124
  /**
81
125
  * Refuse every queued and future admission with `reason`; work already
82
126
  * holding a slot is not interrupted. The authored body calls this when it
@@ -90,3 +134,18 @@ export class WorkerSlots {
90
134
  for (const waiter of this.waiting.splice(0)) waiter.reject(reason);
91
135
  }
92
136
  }
137
+
138
+ /** Read through a call so a check after an `await` is not narrowed away. */
139
+ function scopeClosedReason(scope?: ScopeState): { readonly reason: unknown } | undefined {
140
+ return scope?.closed;
141
+ }
142
+
143
+ /** One run-tree-wide capacity pool, shared by parent and child flows. */
144
+ export interface AuthoredWorkerSlots {
145
+ readonly agent: WorkerSlots;
146
+ readonly llm: WorkerSlots;
147
+ }
148
+
149
+ export function authoredWorkerSlots(capacity: number): AuthoredWorkerSlots {
150
+ return Object.freeze({ agent: new WorkerSlots(capacity), llm: new WorkerSlots(capacity) });
151
+ }