@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.
- package/dist/authored-dispatch.d.ts +24 -0
- package/dist/authored-dispatch.d.ts.map +1 -0
- package/dist/authored-dispatch.js +81 -0
- package/dist/authored-dispatch.js.map +1 -0
- package/dist/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +17 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +92 -16
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +9 -0
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-node-entry.js +1 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-root.js +1 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.d.ts +3 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +18 -7
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +8 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-mirror-step.d.ts +2 -0
- package/dist/cloud-mirror-step.d.ts.map +1 -1
- package/dist/cloud-mirror-step.js +9 -1
- package/dist/cloud-mirror-step.js.map +1 -1
- package/dist/cloud-mirror.d.ts.map +1 -1
- package/dist/cloud-mirror.js +17 -2
- package/dist/cloud-mirror.js.map +1 -1
- package/dist/worker-slots.d.ts +15 -0
- package/dist/worker-slots.d.ts.map +1 -1
- package/dist/worker-slots.js +40 -3
- package/dist/worker-slots.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-dispatch.ts +113 -0
- package/src/authored-flow-error.ts +3 -0
- package/src/authored-flow-executor.ts +126 -15
- package/src/authored-flow-loader.ts +12 -0
- package/src/authored-node-entry.ts +1 -0
- package/src/authored-root.ts +1 -0
- package/src/authored-worker-step.ts +17 -6
- package/src/cli/run.ts +10 -0
- package/src/cli.ts +3 -0
- package/src/cloud-mirror-step.ts +16 -2
- package/src/cloud-mirror.ts +15 -2
- package/src/worker-slots.ts +63 -4
package/src/cloud-mirror-step.ts
CHANGED
|
@@ -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
|
-
): {
|
|
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
|
/**
|
package/src/cloud-mirror.ts
CHANGED
|
@@ -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
|
-
|
|
375
|
-
|
|
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
|
package/src/worker-slots.ts
CHANGED
|
@@ -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<{
|
|
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
|
+
}
|