@relayflows/sdk 2.0.26 → 2.0.28
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-flow-executor.d.ts +8 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +16 -7
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-lifecycle.d.ts +68 -1
- package/dist/authored-flow-lifecycle.d.ts.map +1 -1
- package/dist/authored-flow-lifecycle.js +108 -10
- package/dist/authored-flow-lifecycle.js.map +1 -1
- package/dist/authored-flow-operation.d.ts +9 -1
- package/dist/authored-flow-operation.d.ts.map +1 -1
- package/dist/authored-flow-operation.js +7 -1
- package/dist/authored-flow-operation.js.map +1 -1
- package/dist/authored-node-entry.js +4 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +2 -1
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-promise-graph.d.ts +19 -4
- package/dist/authored-promise-graph.d.ts.map +1 -1
- package/dist/authored-promise-graph.js +44 -19
- package/dist/authored-promise-graph.js.map +1 -1
- package/dist/authored-root.d.ts +2 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +2 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-graph.d.ts +83 -0
- package/dist/authored-step-graph.d.ts.map +1 -0
- package/dist/authored-step-graph.js +217 -0
- package/dist/authored-step-graph.js.map +1 -0
- package/dist/authored-step-index.d.ts +30 -0
- package/dist/authored-step-index.d.ts.map +1 -1
- package/dist/authored-step-index.js +26 -1
- package/dist/authored-step-index.js.map +1 -1
- package/dist/authored-step-output.d.ts +3 -0
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +4 -2
- package/dist/authored-step-output.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 +25 -6
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +5 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +9 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-excerpt.d.ts +48 -0
- package/dist/cli/step-excerpt.d.ts.map +1 -0
- package/dist/cli/step-excerpt.js +302 -0
- package/dist/cli/step-excerpt.js.map +1 -0
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +9 -20
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +8 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +6 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloud-transcript-codex.d.ts +42 -0
- package/dist/cloud-transcript-codex.d.ts.map +1 -0
- package/dist/cloud-transcript-codex.js +411 -0
- package/dist/cloud-transcript-codex.js.map +1 -0
- package/dist/cloud-transcript-types.d.ts +161 -0
- package/dist/cloud-transcript-types.d.ts.map +1 -0
- package/dist/cloud-transcript-types.js +47 -0
- package/dist/cloud-transcript-types.js.map +1 -0
- package/dist/cloud-transcript.d.ts +2 -78
- package/dist/cloud-transcript.d.ts.map +1 -1
- package/dist/cloud-transcript.js +36 -38
- package/dist/cloud-transcript.js.map +1 -1
- package/dist/failure-kinds.d.ts +10 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +8 -2
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +22 -6
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts +2 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +5 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +8 -3
- package/dist/local-agent.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +29 -3
- package/dist/preflight.js.map +1 -1
- package/dist/promise-ancestry.d.ts +20 -0
- package/dist/promise-ancestry.d.ts.map +1 -0
- package/dist/promise-ancestry.js +104 -0
- package/dist/promise-ancestry.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +7 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +7 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +42 -10
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-slots.d.ts +42 -0
- package/dist/worker-slots.d.ts.map +1 -0
- package/dist/worker-slots.js +94 -0
- package/dist/worker-slots.js.map +1 -0
- package/package.json +2 -2
- package/src/authored-flow-executor.ts +22 -3
- package/src/authored-flow-lifecycle.ts +111 -16
- package/src/authored-flow-operation.ts +10 -0
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +2 -1
- package/src/authored-promise-graph.ts +46 -19
- package/src/authored-root.ts +4 -0
- package/src/authored-step-graph.ts +246 -0
- package/src/authored-step-index.ts +44 -1
- package/src/authored-step-output.ts +7 -3
- package/src/authored-worker-step.ts +25 -6
- package/src/cli/direct-run.ts +5 -2
- package/src/cli/run.ts +11 -4
- package/src/cli/step-excerpt.ts +332 -0
- package/src/cli/step-failure.ts +9 -20
- package/src/cli-commands.ts +6 -0
- package/src/cli.ts +40 -9
- package/src/cloud-transcript-codex.ts +429 -0
- package/src/cloud-transcript-types.ts +214 -0
- package/src/cloud-transcript.ts +44 -136
- package/src/failure-kinds.ts +10 -2
- package/src/journal-client.ts +25 -6
- package/src/llm-worker.ts +7 -2
- package/src/local-agent.ts +8 -2
- package/src/preflight.ts +37 -2
- package/src/promise-ancestry.ts +98 -0
- package/src/validate.ts +7 -0
- package/src/worker-cli.ts +41 -9
- package/src/worker-slots.ts +92 -0
package/src/preflight.ts
CHANGED
|
@@ -395,7 +395,9 @@ function unknownModelDiagnostics(
|
|
|
395
395
|
stepId: step.id,
|
|
396
396
|
...(resolution === undefined ? {} : { cli: resolution.cli }),
|
|
397
397
|
model,
|
|
398
|
-
message: unknownModelMessage(
|
|
398
|
+
message: unknownModelMessage(
|
|
399
|
+
step.id, model, resolution?.cli, options.modelRegistryPath, resolution?.modelSource,
|
|
400
|
+
),
|
|
399
401
|
});
|
|
400
402
|
}
|
|
401
403
|
|
|
@@ -418,15 +420,46 @@ function isKnownModel(model: string, models: readonly string[] | undefined): boo
|
|
|
418
420
|
return models?.includes(model) === true;
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Attribute an effective model to where its value actually came from. Only a
|
|
425
|
+
* `step` value was written on the step; saying "declares" about an adapter
|
|
426
|
+
* default sends the reader looking for a `model:` key that is not there.
|
|
427
|
+
*/
|
|
428
|
+
function describeEffectiveModel(
|
|
429
|
+
stepId: string,
|
|
430
|
+
model: string,
|
|
431
|
+
cli: string,
|
|
432
|
+
modelSource: CliModelSource | undefined,
|
|
433
|
+
): string {
|
|
434
|
+
if (modelSource === 'adapter') {
|
|
435
|
+
return `Step "${stepId}" declares no model; the default for CLI "${cli}" is "${model}"`;
|
|
436
|
+
}
|
|
437
|
+
if (modelSource === 'named') {
|
|
438
|
+
return `Step "${stepId}" uses model "${model}" from its named agent for CLI "${cli}"`;
|
|
439
|
+
}
|
|
440
|
+
return `Step "${stepId}" declares model "${model}" for CLI "${cli}"`;
|
|
441
|
+
}
|
|
442
|
+
|
|
421
443
|
function unknownModelMessage(
|
|
422
444
|
stepId: string,
|
|
423
445
|
model: string,
|
|
424
446
|
cli: string | undefined,
|
|
425
447
|
registryPath: string | undefined,
|
|
448
|
+
modelSource: CliModelSource | undefined,
|
|
426
449
|
): string {
|
|
427
450
|
const source = registryPath === undefined
|
|
428
451
|
? 'the nearest project config (no model registry was found)'
|
|
429
452
|
: `project model registry "${registryPath}"`;
|
|
453
|
+
if (modelSource === 'adapter' && cli !== undefined) {
|
|
454
|
+
// Registry policy still governs a default — it is the model that will run
|
|
455
|
+
// — but the remedy has to name the value the author never typed, and the
|
|
456
|
+
// second way out: declaring a model the registry already allows.
|
|
457
|
+
const add = registryPath === undefined
|
|
458
|
+
? 'add the exact model'
|
|
459
|
+
: `add the exact model to "models" in "${registryPath}"`;
|
|
460
|
+
return `${describeEffectiveModel(stepId, model, cli, modelSource)}, which is not listed in ${source}; `
|
|
461
|
+
+ `${add} only after verifying that project is allowed to use it, or declare an allowed model on the step.`;
|
|
462
|
+
}
|
|
430
463
|
const cliContext = cli === undefined ? '' : ` for CLI "${cli}"`;
|
|
431
464
|
return `Step "${stepId}" declares model "${model}"${cliContext}, but it is not listed in ${source}; add the exact model only after verifying that project is allowed to use it.`;
|
|
432
465
|
}
|
|
@@ -545,7 +578,9 @@ function probeResolvedCli(
|
|
|
545
578
|
stepId: resolution.stepId,
|
|
546
579
|
cli: resolution.cli,
|
|
547
580
|
model: resolution.model,
|
|
548
|
-
message:
|
|
581
|
+
message: describeEffectiveModel(resolution.stepId, resolution.model, resolution.cli, resolution.modelSource)
|
|
582
|
+
+ `, but its model-scoped "${result.modelCommand ?? `${resolution.cli} auth status`}" probe exited non-zero;`
|
|
583
|
+
+ ` verify the model name and this credential's access.`,
|
|
549
584
|
});
|
|
550
585
|
}
|
|
551
586
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of a fixed set of `targets` each of `starts` can reach by following
|
|
3
|
+
* `edgesOf` — the batch form of asking `dependsOn(start, target)` for every
|
|
4
|
+
* pair, with the same answer (a node reaches itself).
|
|
5
|
+
*
|
|
6
|
+
* The completion gate used to ask that pair-by-pair, re-walking each start's
|
|
7
|
+
* whole ancestry every time: operations × combinator members × tracked
|
|
8
|
+
* promises. A flow of ~110 steps whose agents ran for twenty minutes produced
|
|
9
|
+
* enough tracked promises (journal polling while each step waited) that the
|
|
10
|
+
* gate ran for three minutes without yielding, starving the root's lease
|
|
11
|
+
* renewal until the run was reaped as crashed.
|
|
12
|
+
*
|
|
13
|
+
* Here the upward closure of every start is visited once, cycles (a promise
|
|
14
|
+
* resolved from a context created after it) are condensed with an iterative
|
|
15
|
+
* Tarjan pass, and target sets flow as bitsets from ancestors to descendants
|
|
16
|
+
* in the order Tarjan emits components. Cost is O((V + E) · ⌈T / 32⌉) for the
|
|
17
|
+
* visited subgraph, whatever the number of starts.
|
|
18
|
+
*/
|
|
19
|
+
export function reachableTargets(
|
|
20
|
+
starts: readonly number[],
|
|
21
|
+
targets: readonly number[],
|
|
22
|
+
edgesOf: (node: number) => readonly number[],
|
|
23
|
+
): Map<number, ReadonlySet<number>> {
|
|
24
|
+
const bit = new Map<number, number>();
|
|
25
|
+
for (const target of targets) if (!bit.has(target)) bit.set(target, bit.size);
|
|
26
|
+
const words = Math.max(1, Math.ceil(bit.size / 32));
|
|
27
|
+
|
|
28
|
+
const index = new Map<number, number>();
|
|
29
|
+
const low = new Map<number, number>();
|
|
30
|
+
const onStack = new Set<number>();
|
|
31
|
+
const stack: number[] = [];
|
|
32
|
+
const component = new Map<number, number>();
|
|
33
|
+
const labels: Uint32Array[] = [];
|
|
34
|
+
|
|
35
|
+
const closeComponent = (root: number): void => {
|
|
36
|
+
const label = new Uint32Array(words);
|
|
37
|
+
const members: number[] = [];
|
|
38
|
+
let member: number;
|
|
39
|
+
do {
|
|
40
|
+
member = stack.pop()!;
|
|
41
|
+
onStack.delete(member);
|
|
42
|
+
members.push(member);
|
|
43
|
+
} while (member !== root);
|
|
44
|
+
const id = labels.length;
|
|
45
|
+
for (const node of members) component.set(node, id);
|
|
46
|
+
for (const node of members) {
|
|
47
|
+
const own = bit.get(node);
|
|
48
|
+
if (own !== undefined) label[own >>> 5]! |= 1 << (own & 31);
|
|
49
|
+
// Every edge leaving the component points at one Tarjan already closed.
|
|
50
|
+
for (const edge of edgesOf(node)) {
|
|
51
|
+
const target = component.get(edge);
|
|
52
|
+
if (target === undefined || target === id) continue;
|
|
53
|
+
const inherited = labels[target]!;
|
|
54
|
+
for (let word = 0; word < words; word++) label[word]! |= inherited[word]!;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
labels.push(label);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Iterative Tarjan: a deep promise chain must not overflow the JS stack.
|
|
61
|
+
for (const start of starts) {
|
|
62
|
+
if (index.has(start)) continue;
|
|
63
|
+
const frames: Array<{ node: number; edges: readonly number[]; next: number }> = [];
|
|
64
|
+
const open = (node: number): void => {
|
|
65
|
+
index.set(node, index.size);
|
|
66
|
+
low.set(node, index.get(node)!);
|
|
67
|
+
stack.push(node);
|
|
68
|
+
onStack.add(node);
|
|
69
|
+
frames.push({ node, edges: edgesOf(node), next: 0 });
|
|
70
|
+
};
|
|
71
|
+
open(start);
|
|
72
|
+
while (frames.length > 0) {
|
|
73
|
+
const frame = frames.at(-1)!;
|
|
74
|
+
if (frame.next < frame.edges.length) {
|
|
75
|
+
const edge = frame.edges[frame.next++]!;
|
|
76
|
+
if (!index.has(edge)) open(edge);
|
|
77
|
+
else if (onStack.has(edge)) low.set(frame.node, Math.min(low.get(frame.node)!, index.get(edge)!));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
frames.pop();
|
|
81
|
+
const parent = frames.at(-1);
|
|
82
|
+
if (parent !== undefined) low.set(parent.node, Math.min(low.get(parent.node)!, low.get(frame.node)!));
|
|
83
|
+
if (low.get(frame.node) === index.get(frame.node)) closeComponent(frame.node);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const reached = new Map<number, ReadonlySet<number>>();
|
|
88
|
+
for (const start of starts) {
|
|
89
|
+
if (reached.has(start)) continue;
|
|
90
|
+
const label = labels[component.get(start)!]!;
|
|
91
|
+
const found = new Set<number>();
|
|
92
|
+
for (const [target, position] of bit) {
|
|
93
|
+
if ((label[position >>> 5]! & (1 << (position & 31))) !== 0) found.add(target);
|
|
94
|
+
}
|
|
95
|
+
reached.set(start, found);
|
|
96
|
+
}
|
|
97
|
+
return reached;
|
|
98
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -486,6 +486,13 @@ class Validator {
|
|
|
486
486
|
}
|
|
487
487
|
this.validateCli(st.cli, at);
|
|
488
488
|
this.validateModel(st.model, at);
|
|
489
|
+
// The kernel refuses a relative cwd at run.start; refuse it here so `flows
|
|
490
|
+
// check` and the run agree. Not resolved against the checking directory:
|
|
491
|
+
// that would make the compiled spec (and its hash) depend on where the
|
|
492
|
+
// check ran. `f.agent` resolves before compiling, so it never lands here.
|
|
493
|
+
if (st.cwd !== undefined && (!isNonEmptyString(st.cwd) || !st.cwd.startsWith('/'))) {
|
|
494
|
+
this.fail(`${at}.cwd: expected an absolute path (got ${JSON.stringify(st.cwd)})`);
|
|
495
|
+
}
|
|
489
496
|
if (st.surfaces !== undefined) this.validateSurfaces(st.surfaces, `${at}.surfaces`);
|
|
490
497
|
if (st.permissions !== undefined) this.validatePermissions(st.permissions, `${at}.permissions`);
|
|
491
498
|
}
|
package/src/worker-cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
|
-
import { resolve, sep } from 'node:path';
|
|
2
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
3
3
|
import { diffWorkspaceFiles, snapshotWorkspaceFiles } from './agent-artifacts.js';
|
|
4
4
|
import { claudeResultOutcome, decodeProviderResult, decodeWrapperResult, requirePricedUsage } from './worker-usage.js';
|
|
5
5
|
import { openSidechannel, type SidechannelContext } from './pty-sidechannel.js';
|
|
@@ -121,7 +121,7 @@ export async function runAgentCli(
|
|
|
121
121
|
const artifactRoot = mode === 'agent' ? resolve(cwd ?? process.cwd()) : undefined;
|
|
122
122
|
return artifactRoot === undefined
|
|
123
123
|
? execute()
|
|
124
|
-
: serializedByDirectory(artifactRoot, async () => {
|
|
124
|
+
: serializedByDirectory(canonicalTree(artifactRoot), async () => {
|
|
125
125
|
const before = await snapshotWorkspaceFiles(artifactRoot);
|
|
126
126
|
const result = await execute();
|
|
127
127
|
const kernelData = sidechannel === undefined ? undefined : realPath(resolve(sidechannel.dataDir));
|
|
@@ -214,20 +214,52 @@ function under(path: string | undefined, directory: string | undefined): boolean
|
|
|
214
214
|
return path === directory || path.startsWith(directory.endsWith(sep) ? directory : directory + sep);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Symlink-free form of an absolute path that may not exist yet: the deepest
|
|
219
|
+
* existing ancestor is resolved and the missing tail is kept. A plain
|
|
220
|
+
* `realPath` fallback would leave `/link/new` unresolved while `/link`
|
|
221
|
+
* resolves, and the overlap check would then treat them as disjoint trees.
|
|
222
|
+
*/
|
|
223
|
+
export function canonicalTree(path: string): string {
|
|
224
|
+
const missing: string[] = [];
|
|
225
|
+
let current = path;
|
|
226
|
+
for (;;) {
|
|
227
|
+
try {
|
|
228
|
+
return join(realpathSync(current), ...missing.reverse());
|
|
229
|
+
} catch {
|
|
230
|
+
const parent = dirname(current);
|
|
231
|
+
if (parent === current) return path;
|
|
232
|
+
missing.push(basename(current));
|
|
233
|
+
current = parent;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
/** Symlink-free form of a path, or the path itself when it cannot be resolved. */
|
|
218
239
|
function realPath(path: string | undefined): string | undefined {
|
|
219
240
|
if (path === undefined) return undefined;
|
|
220
241
|
try { return realpathSync(path); } catch { return path; }
|
|
221
242
|
}
|
|
222
243
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
244
|
+
/**
|
|
245
|
+
* One agent at a time per overlapping working tree, for the artifact interval.
|
|
246
|
+
*
|
|
247
|
+
* `directory` must be canonical (symlink-free): `/repo` and a `/tmp/link` to it
|
|
248
|
+
* are one tree. Two trees overlap when one contains the other, because the
|
|
249
|
+
* snapshot walks the whole subtree — an agent in `/repo` would otherwise be
|
|
250
|
+
* credited with files an agent in `/repo/.wt/api` wrote meanwhile. Disjoint
|
|
251
|
+
* trees (sibling worktrees) run side by side. Each run waits for every
|
|
252
|
+
* earlier-registered overlapping run, so order within an overlap is arrival.
|
|
253
|
+
*/
|
|
254
|
+
const directoryRuns = new Set<{ readonly directory: string; readonly settled: Promise<void> }>();
|
|
225
255
|
function serializedByDirectory<T>(directory: string, task: () => Promise<T>): Promise<T> {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
const blockers = [...directoryRuns]
|
|
257
|
+
.filter(other => under(other.directory, directory) || under(directory, other.directory))
|
|
258
|
+
.map(other => other.settled);
|
|
259
|
+
const run = Promise.all(blockers).then(task);
|
|
260
|
+
const entry = { directory, settled: run.then(() => undefined, () => undefined) };
|
|
261
|
+
directoryRuns.add(entry);
|
|
262
|
+
void entry.settled.then(() => { directoryRuns.delete(entry); });
|
|
231
263
|
return run;
|
|
232
264
|
}
|
|
233
265
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** The local agent worker's default concurrency, and the ceiling `--agent-capacity` accepts. */
|
|
2
|
+
export const DEFAULT_LOCAL_AGENT_CAPACITY = 4;
|
|
3
|
+
export const MAX_LOCAL_AGENT_CAPACITY = 32;
|
|
4
|
+
|
|
5
|
+
/** A positive integer no larger than the ceiling; anything else is not a capacity. */
|
|
6
|
+
export function isAgentCapacity(value: unknown): value is number {
|
|
7
|
+
return Number.isInteger(value) && (value as number) >= 1 && (value as number) <= MAX_LOCAL_AGENT_CAPACITY;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* First-come admission to a worker that holds `capacity` dispatches at once.
|
|
12
|
+
*
|
|
13
|
+
* The kernel never queues a step for a busy worker: an attempt with no worker
|
|
14
|
+
* below capacity parks the run ("no worker is attached for step type …"). An
|
|
15
|
+
* authored body opens one child run per `f.agent`/`f.llm`, so `Promise.all`
|
|
16
|
+
* over more calls than the worker holds would park the overflow. Holding the
|
|
17
|
+
* overflow here, before `run.start`, keeps the kernel's admission exact and
|
|
18
|
+
* turns "too many at once" into "wait for a slot".
|
|
19
|
+
*/
|
|
20
|
+
export class WorkerSlots {
|
|
21
|
+
private held = 0;
|
|
22
|
+
private readonly waiting: Array<{ resolve: () => void; reject: (reason: unknown) => void }> = [];
|
|
23
|
+
private closed: { readonly reason: unknown } | undefined;
|
|
24
|
+
|
|
25
|
+
constructor(readonly capacity: number) {
|
|
26
|
+
if (!isAgentCapacity(capacity)) throw new RangeError(`worker capacity must be an integer from 1 to ${MAX_LOCAL_AGENT_CAPACITY} (got ${capacity})`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async run<T>(work: () => Promise<T>): Promise<T> {
|
|
30
|
+
if (this.closed !== undefined) throw this.closed.reason;
|
|
31
|
+
if (this.held < this.capacity) this.held++;
|
|
32
|
+
// A released slot is handed straight to the next waiter, so `held` never
|
|
33
|
+
// dips below capacity while anyone is queued and no later caller can jump it.
|
|
34
|
+
else {
|
|
35
|
+
await new Promise<void>((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
36
|
+
// Woken with the slot, but the body may have failed in between (see release).
|
|
37
|
+
const closed = this.closedReason();
|
|
38
|
+
if (closed !== undefined) {
|
|
39
|
+
this.release();
|
|
40
|
+
throw closed.reason;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return await work();
|
|
45
|
+
} finally {
|
|
46
|
+
this.release();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read through a call so a check after an `await` is not narrowed away. */
|
|
51
|
+
private closedReason(): { readonly reason: unknown } | undefined {
|
|
52
|
+
return this.closed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hand the slot to the next waiter on a later macrotask, not synchronously.
|
|
57
|
+
* When the work that held it rejected and that rejection fails the body, the
|
|
58
|
+
* failure reaches `close` through microtasks only (operation → Promise.all →
|
|
59
|
+
* body → executor). Waking the waiter synchronously let it admit a child run
|
|
60
|
+
* before `close` could refuse it; deferring lets the teardown land first.
|
|
61
|
+
*/
|
|
62
|
+
private release(): void {
|
|
63
|
+
const next = this.waiting.shift();
|
|
64
|
+
if (next === undefined) {
|
|
65
|
+
this.held--;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
setImmediate(() => {
|
|
69
|
+
if (this.closed === undefined) {
|
|
70
|
+
next.resolve();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Closed meanwhile. `close` could not see this waiter (already taken off
|
|
74
|
+
// the queue), so refuse it here, and pass the slot on instead of leaking it.
|
|
75
|
+
next.reject(this.closed.reason);
|
|
76
|
+
this.release();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Refuse every queued and future admission with `reason`; work already
|
|
82
|
+
* holding a slot is not interrupted. The authored body calls this when it
|
|
83
|
+
* fails: a queued call is already `running` as an operation, so operation
|
|
84
|
+
* cancellation cannot reach it, and without this it would still be admitted
|
|
85
|
+
* and run after the flow had failed.
|
|
86
|
+
*/
|
|
87
|
+
close(reason: unknown): void {
|
|
88
|
+
if (this.closed !== undefined) return;
|
|
89
|
+
this.closed = { reason };
|
|
90
|
+
for (const waiter of this.waiting.splice(0)) waiter.reject(reason);
|
|
91
|
+
}
|
|
92
|
+
}
|