@relayflows/sdk 2.0.50 → 2.0.52
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-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +3 -3
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +3 -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 +2 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +97 -14
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-lifecycle.d.ts +28 -3
- package/dist/authored-flow-lifecycle.d.ts.map +1 -1
- package/dist/authored-flow-lifecycle.js +206 -10
- package/dist/authored-flow-lifecycle.js.map +1 -1
- package/dist/authored-node-entry.js +2 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-parallel-body.d.ts +10 -0
- package/dist/authored-parallel-body.d.ts.map +1 -0
- package/dist/authored-parallel-body.js +68 -0
- package/dist/authored-parallel-body.js.map +1 -0
- package/dist/authored-parallel-eligibility.d.ts +8 -0
- package/dist/authored-parallel-eligibility.d.ts.map +1 -0
- package/dist/authored-parallel-eligibility.js +287 -0
- package/dist/authored-parallel-eligibility.js.map +1 -0
- package/dist/authored-preflight.js +4 -4
- package/dist/authored-preflight.js.map +1 -1
- package/dist/authored-promise-graph.d.ts +16 -0
- package/dist/authored-promise-graph.d.ts.map +1 -1
- package/dist/authored-promise-graph.js +36 -0
- package/dist/authored-promise-graph.js.map +1 -1
- package/dist/authored-root-readback.d.ts +1 -2
- package/dist/authored-root-readback.d.ts.map +1 -1
- package/dist/authored-root-readback.js +1 -1
- package/dist/authored-root-readback.js.map +1 -1
- package/dist/authored-root.d.ts +6 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +55 -8
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-graph.d.ts +19 -4
- package/dist/authored-step-graph.d.ts.map +1 -1
- package/dist/authored-step-graph.js +49 -6
- package/dist/authored-step-graph.js.map +1 -1
- package/dist/authored-step-index.d.ts +2 -1
- package/dist/authored-step-index.d.ts.map +1 -1
- package/dist/authored-step-index.js +2 -1
- package/dist/authored-step-index.js.map +1 -1
- package/dist/authored-verdict.d.ts +2 -0
- package/dist/authored-verdict.d.ts.map +1 -1
- package/dist/authored-verdict.js +4 -1
- package/dist/authored-verdict.js.map +1 -1
- package/dist/authored-worker-step.d.ts +4 -3
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +23 -10
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.js +2 -2
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +1 -1
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +8 -7
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +4 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cloud-http.d.ts +1 -0
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +1 -1
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +45 -6
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-signal.d.ts +12 -0
- package/dist/cloud-signal.d.ts.map +1 -0
- package/dist/cloud-signal.js +117 -0
- package/dist/cloud-signal.js.map +1 -0
- package/dist/cloud-sync.d.ts +8 -1
- package/dist/cloud-sync.d.ts.map +1 -1
- package/dist/cloud-sync.js +36 -4
- package/dist/cloud-sync.js.map +1 -1
- package/dist/communication/environment.d.ts +1 -1
- package/dist/communication/environment.d.ts.map +1 -1
- package/dist/communication/environment.js +26 -3
- package/dist/communication/environment.js.map +1 -1
- package/dist/communication/worker.js +1 -1
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -2
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +5 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +44 -4
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +1 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/preflight.d.ts +2 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +12 -7
- package/dist/preflight.js.map +1 -1
- package/dist/spec.d.ts +4 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.d.ts.map +1 -1
- package/dist/step-fields.js +1 -0
- package/dist/step-fields.js.map +1 -1
- package/dist/step-secrets.d.ts +20 -0
- package/dist/step-secrets.d.ts.map +1 -0
- package/dist/step-secrets.js +146 -0
- package/dist/step-secrets.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +10 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +5 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +43 -14
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +3 -2
- package/src/authored-budget.ts +4 -3
- package/src/authored-flow-error.ts +2 -0
- package/src/authored-flow-executor.ts +89 -13
- package/src/authored-flow-lifecycle.ts +215 -9
- package/src/authored-node-entry.ts +2 -0
- package/src/authored-parallel-body.ts +67 -0
- package/src/authored-parallel-eligibility.ts +245 -0
- package/src/authored-preflight.ts +4 -4
- package/src/authored-promise-graph.ts +35 -0
- package/src/authored-root-readback.ts +1 -2
- package/src/authored-root.ts +62 -9
- package/src/authored-step-graph.ts +50 -5
- package/src/authored-step-index.ts +2 -1
- package/src/authored-verdict.ts +4 -1
- package/src/authored-worker-step.ts +29 -9
- package/src/cli/check.ts +3 -3
- package/src/cli/cli-probe.ts +9 -6
- package/src/cli/run.ts +4 -2
- package/src/cloud-http.ts +1 -1
- package/src/cloud-run.ts +45 -8
- package/src/cloud-signal.ts +110 -0
- package/src/cloud-sync.ts +39 -4
- package/src/communication/environment.ts +28 -3
- package/src/communication/worker.ts +2 -1
- package/src/compile.ts +5 -2
- package/src/failure-kinds.ts +2 -0
- package/src/index.ts +1 -0
- package/src/journal-client.ts +48 -2
- package/src/llm-worker.ts +2 -1
- package/src/preflight.ts +13 -7
- package/src/spec.ts +4 -0
- package/src/step-fields.ts +1 -0
- package/src/step-secrets.ts +145 -0
- package/src/validate.ts +11 -0
- package/src/worker-cli.ts +41 -13
- package/src/worker.ts +3 -2
|
@@ -13,16 +13,16 @@ export function authoredPreflight(path: string, environment?: NodeJS.ProcessEnv)
|
|
|
13
13
|
return async (flow: FlowSpec) => {
|
|
14
14
|
let config: ProjectConfig;
|
|
15
15
|
try { config = readProjectConfig(directory); }
|
|
16
|
-
catch { return checkAuthoredFlow(flow, path); }
|
|
16
|
+
catch { return checkAuthoredFlow(flow, path, undefined, { environment }); }
|
|
17
17
|
const resolved = resolvePreflight(flow, {
|
|
18
18
|
projectCli: config.cli, projectConfigPath: config.path, projectSearchStart: directory,
|
|
19
19
|
models: config.models, modelRegistryPath: config.modelRegistryPath,
|
|
20
20
|
});
|
|
21
|
-
if (!resolved.ok) return checkAuthoredFlow(flow, path, config);
|
|
21
|
+
if (!resolved.ok) return checkAuthoredFlow(flow, path, config, { environment });
|
|
22
22
|
await Promise.all(resolved.resolutions.map(async resolution => {
|
|
23
23
|
const step = resolved.compiled!.steps.find(step => step.id === resolution.stepId)!;
|
|
24
24
|
const managed = step.type === 'agent' && communicationInstruction(step.instruction) !== undefined;
|
|
25
|
-
const key = cliProbeKey(resolution, managed);
|
|
25
|
+
const key = cliProbeKey(resolution, managed, step.secrets);
|
|
26
26
|
if (probeCache.has(key)) return;
|
|
27
27
|
let probing = pending.get(key);
|
|
28
28
|
if (probing === undefined) {
|
|
@@ -30,7 +30,7 @@ export function authoredPreflight(path: string, environment?: NodeJS.ProcessEnv)
|
|
|
30
30
|
try {
|
|
31
31
|
probeCache.set(key, { result: await probeCliAsync(resolution.cli,
|
|
32
32
|
resolution.source === 'project' ? config.directory : directory,
|
|
33
|
-
resolution.model, managed ? 'managed' : undefined, environment) });
|
|
33
|
+
resolution.model, managed ? 'managed' : undefined, environment, step.secrets) });
|
|
34
34
|
} catch (error) {
|
|
35
35
|
probeCache.set(key, { failure: error instanceof CliProbeError ? error.detail : null });
|
|
36
36
|
}
|
|
@@ -30,12 +30,15 @@ export class AuthoredPromiseGraph {
|
|
|
30
30
|
private readonly triggers = new Map<number, number>();
|
|
31
31
|
private readonly creationContexts = new Map<number, number>();
|
|
32
32
|
private readonly resolutionCauses = new Map<number, number>();
|
|
33
|
+
/** Promises attributed only when another promise's reaction resolved them. */
|
|
34
|
+
private readonly adoptedFromResolvingContext = new Set<number>();
|
|
33
35
|
private readonly promiseIds = new WeakMap<object, number>();
|
|
34
36
|
private readonly handles = new Map<number, Promise<unknown>>();
|
|
35
37
|
private readonly unattributedChildren = new Map<number, number[]>();
|
|
36
38
|
private readonly attributedRoots = new Map<number, number>();
|
|
37
39
|
private readonly roots = new Set<number>();
|
|
38
40
|
private readonly pending = new Set<number>();
|
|
41
|
+
private readonly combinatorCaptures: Set<number>[] = [];
|
|
39
42
|
private dependencyCache: { readonly start: number; readonly found: ReadonlySet<number>; readonly version: number } | undefined;
|
|
40
43
|
/** Bumped by every graph change, so derived answers can be cached between changes. */
|
|
41
44
|
private changes = 0;
|
|
@@ -52,6 +55,7 @@ export class AuthoredPromiseGraph {
|
|
|
52
55
|
this.triggers.set(asyncId, triggerAsyncId);
|
|
53
56
|
this.creationContexts.set(asyncId, executionAsyncId());
|
|
54
57
|
this.promiseIds.set(resource, asyncId);
|
|
58
|
+
for (const capture of this.combinatorCaptures) capture.add(asyncId);
|
|
55
59
|
this.handles.set(asyncId, resource as Promise<unknown>);
|
|
56
60
|
this.pending.add(asyncId);
|
|
57
61
|
const root = this.roots.has(triggerAsyncId)
|
|
@@ -77,6 +81,7 @@ export class AuthoredPromiseGraph {
|
|
|
77
81
|
// success. Measured; see ops/probes/pr134-repair-0903/combinators.mjs.
|
|
78
82
|
const adopted = this.attributedRoots.get(cause);
|
|
79
83
|
if (adopted !== undefined) {
|
|
84
|
+
this.adoptedFromResolvingContext.add(asyncId);
|
|
80
85
|
this.attribute(asyncId, adopted);
|
|
81
86
|
return;
|
|
82
87
|
}
|
|
@@ -100,6 +105,18 @@ export class AuthoredPromiseGraph {
|
|
|
100
105
|
return this.promiseIds.get(value);
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
isPending(asyncId: number): boolean {
|
|
109
|
+
return this.pending.has(asyncId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The intrinsic creates per-member reaction promises as well as its aggregate. */
|
|
113
|
+
captureCombinator<T>(call: () => T): { readonly value: T; readonly created: ReadonlySet<number> } {
|
|
114
|
+
const created = new Set<number>();
|
|
115
|
+
this.combinatorCaptures.push(created);
|
|
116
|
+
try { return { value: call(), created }; }
|
|
117
|
+
finally { this.combinatorCaptures.pop(); }
|
|
118
|
+
}
|
|
119
|
+
|
|
103
120
|
/** Mark a promise whose descendants belong to an authored operation. */
|
|
104
121
|
registerRoot(asyncId: number): void {
|
|
105
122
|
if (asyncId <= 0 || this.roots.has(asyncId)) return;
|
|
@@ -145,6 +162,22 @@ export class AuthoredPromiseGraph {
|
|
|
145
162
|
return causes;
|
|
146
163
|
}
|
|
147
164
|
|
|
165
|
+
/**
|
|
166
|
+
* A promise created directly in one async context but settled in another
|
|
167
|
+
* can be a join. Its resolution edge names only the settling member, so a
|
|
168
|
+
* frontier may use it only when a combinator registered all members.
|
|
169
|
+
*/
|
|
170
|
+
hasUnobservedJoinShape(asyncId: number): boolean {
|
|
171
|
+
const trigger = this.triggers.get(asyncId);
|
|
172
|
+
const resolution = this.resolutionCauses.get(asyncId);
|
|
173
|
+
return trigger !== undefined && trigger === this.creationContexts.get(asyncId)
|
|
174
|
+
&& resolution !== undefined && resolution !== trigger;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
wasAdoptedFromResolvingContext(asyncId: number): boolean {
|
|
178
|
+
return this.adoptedFromResolvingContext.has(asyncId);
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
dependsOn(descendant: number, ancestor: number): boolean {
|
|
149
182
|
return this.dependenciesOf(descendant).has(ancestor);
|
|
150
183
|
}
|
|
@@ -167,11 +200,13 @@ export class AuthoredPromiseGraph {
|
|
|
167
200
|
this.triggers.clear();
|
|
168
201
|
this.creationContexts.clear();
|
|
169
202
|
this.resolutionCauses.clear();
|
|
203
|
+
this.adoptedFromResolvingContext.clear();
|
|
170
204
|
this.handles.clear();
|
|
171
205
|
this.unattributedChildren.clear();
|
|
172
206
|
this.attributedRoots.clear();
|
|
173
207
|
this.roots.clear();
|
|
174
208
|
this.pending.clear();
|
|
209
|
+
this.combinatorCaptures.length = 0;
|
|
175
210
|
this.dependencyCache = undefined;
|
|
176
211
|
}
|
|
177
212
|
|
|
@@ -3,7 +3,6 @@ import type { AuthoredFlowExecutionResult } from './authored-flow-executor.js';
|
|
|
3
3
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
4
4
|
import { isSurfaceCompletionReason } from './authored-step-output.js';
|
|
5
5
|
import { JournalFrameError, JournalProtocolError, type JournalClient } from './journal-client.js';
|
|
6
|
-
import type { RunOutcome } from './protocol.js';
|
|
7
6
|
|
|
8
7
|
/*
|
|
9
8
|
* Reading an authored root back from its journal, and what a failed read
|
|
@@ -44,7 +43,7 @@ export async function rootStepIfPresent(journal: JournalClient, runId: string):
|
|
|
44
43
|
* it back is interrupted, the outcome is still known: say so, rather than
|
|
45
44
|
* recasting a finished run as resumable.
|
|
46
45
|
*/
|
|
47
|
-
export function completedResult(journal: JournalClient,
|
|
46
|
+
export function completedResult(journal: JournalClient, rootRunId: string): Promise<AuthoredFlowExecutionResult & { readonly rootRunId: string }> {
|
|
48
47
|
return completedRootResult(journal, rootRunId).catch(error => {
|
|
49
48
|
// A bad frame or a journal with no stored result is a definite fact; any
|
|
50
49
|
// other failure — interrupted, canceled, or a daemon-side read error such
|
package/src/authored-root.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
6
6
|
import { canonicalize } from './canonical.js';
|
|
7
7
|
import { compileSpec, toKernelSpec } from './compile.js';
|
|
8
8
|
import { executeAuthoredFlow, type AuthoredFlowExecutionResult, type AuthoredFlowSuspendedResult } from './authored-flow-executor.js';
|
|
9
|
-
import { AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
9
|
+
import { AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
10
10
|
import {
|
|
11
11
|
loadAuthoredFlow,
|
|
12
12
|
type LoadedAuthoredFlow,
|
|
@@ -23,6 +23,7 @@ import { helperReceiptWaitId, releaseHelperReceiptWaits } from './helper-park.js
|
|
|
23
23
|
import { readSubscriptionPark } from './authored-subscription-park.js';
|
|
24
24
|
import { localAgentCredentialEnvironment } from './local-agent-environment.js';
|
|
25
25
|
import { completedResult, rootKernelStep, rootStepIfPresent } from './authored-root-readback.js';
|
|
26
|
+
import { parallelPromiseIntrinsicsIntact } from './authored-flow-lifecycle.js';
|
|
26
27
|
|
|
27
28
|
export type DurableAuthoredFlowResult =
|
|
28
29
|
| (AuthoredFlowExecutionResult & { readonly rootRunId: string })
|
|
@@ -42,7 +43,7 @@ export interface AuthoredRootExtension {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export interface AuthoredRootMetadata {
|
|
45
|
-
readonly kind: typeof ROOT_KIND;
|
|
46
|
+
readonly kind: typeof ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND;
|
|
46
47
|
readonly flowName: string;
|
|
47
48
|
readonly flowPath: string;
|
|
48
49
|
readonly sourceSha256: string;
|
|
@@ -50,6 +51,10 @@ export interface AuthoredRootMetadata {
|
|
|
50
51
|
readonly sources: readonly AuthoredRootSourceAuthority[];
|
|
51
52
|
/** Plugin-level provenance in lock order; empty when the project declares none. */
|
|
52
53
|
readonly extensions: readonly AuthoredRootExtension[];
|
|
54
|
+
/** Absent on roots admitted before causal identities existed. */
|
|
55
|
+
readonly identityVersion?: 2;
|
|
56
|
+
/** Immutable admission choice; serial uses the 2.0.50 child identity contract. */
|
|
57
|
+
readonly executionMode?: 'serial' | 'parallel';
|
|
53
58
|
readonly localAgentStream?: string;
|
|
54
59
|
readonly inputPresent: boolean;
|
|
55
60
|
readonly input?: unknown;
|
|
@@ -89,8 +94,19 @@ export async function executeDurableAuthoredFlow(
|
|
|
89
94
|
surface: node.surfaceAuthority,
|
|
90
95
|
})));
|
|
91
96
|
const definition = loaded.getDefinition(loaded.handle);
|
|
97
|
+
let parallelEligible = false;
|
|
98
|
+
if (loaded.graph.length === 1 && (loaded.extensions?.length ?? 0) === 0
|
|
99
|
+
&& definition.header.budget === undefined && parallelPromiseIntrinsicsIntact()) {
|
|
100
|
+
// The parser is needed only for an authored run, never for an ordinary
|
|
101
|
+
// CLI check or watch reload. Recheck intrinsics after the async import;
|
|
102
|
+
// author code could have replaced them while it was loading.
|
|
103
|
+
const { parallelRunEligible } = await import('./authored-parallel-eligibility.js');
|
|
104
|
+
parallelEligible = parallelPromiseIntrinsicsIntact()
|
|
105
|
+
&& parallelRunEligible(definition.body, source.toString('utf8'));
|
|
106
|
+
}
|
|
107
|
+
const executionMode = parallelEligible ? 'parallel' : 'serial';
|
|
92
108
|
const metadata: AuthoredRootMetadata = Object.freeze({
|
|
93
|
-
kind: ROOT_KIND,
|
|
109
|
+
kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND : ROOT_KIND,
|
|
94
110
|
flowName: definition.name,
|
|
95
111
|
flowPath: loaded.sourcePath,
|
|
96
112
|
sourceSha256: sha256(source),
|
|
@@ -99,6 +115,8 @@ export async function executeDurableAuthoredFlow(
|
|
|
99
115
|
extensions: Object.freeze((loaded.extensions ?? []).map(extension => Object.freeze({
|
|
100
116
|
name: extension.name, digest: extension.digest, ref: extension.ref,
|
|
101
117
|
}))),
|
|
118
|
+
identityVersion: 2,
|
|
119
|
+
executionMode,
|
|
102
120
|
...(options.localAgentStream === undefined ? {} : { localAgentStream: options.localAgentStream }),
|
|
103
121
|
inputPresent: input !== undefined,
|
|
104
122
|
...(input === undefined ? {} : { input: jsonSnapshot(input, 'authored root input') }),
|
|
@@ -121,7 +139,7 @@ export async function executeDurableAuthoredFlow(
|
|
|
121
139
|
options.lifecycle?.onRunReceipt?.({ runId: outcome.run_id, flow: definition.name });
|
|
122
140
|
if (outcome.status === 'completed') {
|
|
123
141
|
dispatchWait.cancel();
|
|
124
|
-
return await completedResult(journal, outcome
|
|
142
|
+
return await completedResult(journal, outcome.run_id);
|
|
125
143
|
}
|
|
126
144
|
assertRootCanDispatch(outcome);
|
|
127
145
|
await releaseHelperReceiptWaits(journal, outcome.run_id);
|
|
@@ -152,11 +170,23 @@ export async function resumeDurableAuthoredFlow(
|
|
|
152
170
|
): Promise<DurableAuthoredFlowResult | undefined> {
|
|
153
171
|
const metadata = await readAuthoredRootMetadata(journal, rootRunId);
|
|
154
172
|
if (metadata === undefined) return undefined;
|
|
155
|
-
assertAuthoredRuntimeAvailable();
|
|
156
|
-
const loaded = await loadPinnedAuthoredSource(metadata);
|
|
157
173
|
if (metadata.localAgentStream !== options.localAgentStream) {
|
|
158
174
|
throw new Error('authored root local agent surface mismatch');
|
|
159
175
|
}
|
|
176
|
+
const completed = await completedParallelRootOrAssertRuntime(metadata, rootRunId, journal);
|
|
177
|
+
if (completed !== undefined) {
|
|
178
|
+
options.lifecycle?.onRunReceipt?.({ runId: rootRunId, flow: metadata.flowName });
|
|
179
|
+
return completed;
|
|
180
|
+
}
|
|
181
|
+
assertAuthoredRuntimeAvailable();
|
|
182
|
+
const loaded = await loadPinnedAuthoredSource(metadata);
|
|
183
|
+
// Source loading itself can alter a process-wide intrinsic. Refuse before
|
|
184
|
+
// run.resume dispatches a pinned parallel root in that environment.
|
|
185
|
+
const completedAfterLoad = await completedParallelRootOrAssertRuntime(metadata, rootRunId, journal);
|
|
186
|
+
if (completedAfterLoad !== undefined) {
|
|
187
|
+
options.lifecycle?.onRunReceipt?.({ runId: rootRunId, flow: metadata.flowName });
|
|
188
|
+
return completedAfterLoad;
|
|
189
|
+
}
|
|
160
190
|
const stream = rootStreamFromMetadata(await rootKernelStep(journal, rootRunId));
|
|
161
191
|
const peer = journal.createPeer();
|
|
162
192
|
await peer.connect();
|
|
@@ -173,7 +203,7 @@ export async function resumeDurableAuthoredFlow(
|
|
|
173
203
|
options.lifecycle?.onRunReceipt?.({ runId: rootRunId, flow: metadata.flowName });
|
|
174
204
|
if (outcome.status === 'completed') {
|
|
175
205
|
dispatchWait.cancel();
|
|
176
|
-
return await completedResult(journal,
|
|
206
|
+
return await completedResult(journal, rootRunId);
|
|
177
207
|
}
|
|
178
208
|
assertRootCanDispatch(outcome);
|
|
179
209
|
await assertNoOpenHumanWait(journal, outcome);
|
|
@@ -189,6 +219,22 @@ export async function resumeDurableAuthoredFlow(
|
|
|
189
219
|
}
|
|
190
220
|
}
|
|
191
221
|
|
|
222
|
+
async function completedParallelRootOrAssertRuntime(
|
|
223
|
+
metadata: AuthoredRootMetadata, rootRunId: string, journal: JournalClient,
|
|
224
|
+
): Promise<DurableAuthoredFlowResult | undefined> {
|
|
225
|
+
if (metadata.identityVersion !== 2 || metadata.executionMode !== 'parallel'
|
|
226
|
+
|| parallelPromiseIntrinsicsIntact()) return undefined;
|
|
227
|
+
// A completed root has nothing left to dispatch. A read-only snapshot avoids
|
|
228
|
+
// refusing it under a preload while preserving the pre-dispatch guard for
|
|
229
|
+
// an active root. Readback still uses the journaled authored verdict.
|
|
230
|
+
const snapshot = await journal.runGet(rootRunId);
|
|
231
|
+
if (snapshot.status === 'completed') return completedResult(journal, rootRunId);
|
|
232
|
+
const error = new AuthoredFlowExecutionError('unsafe_parallel_runtime',
|
|
233
|
+
`pinned parallel root ${rootRunId} cannot resume while a Promise or Array intrinsic is replaced or a Node preload is active. The run remains resumable in a clean process.`);
|
|
234
|
+
error.rootRunId = rootRunId;
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
|
|
192
238
|
/**
|
|
193
239
|
* A root parked on an unanswered `f.human` will not be dispatched: the
|
|
194
240
|
* kernel holds it in `needs_human` until `event.emit` closes the wait. Report
|
|
@@ -353,6 +399,8 @@ async function driveRoot(
|
|
|
353
399
|
...(options.agentEnvironment === undefined ? {} : { agentEnvironment: options.agentEnvironment }),
|
|
354
400
|
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
355
401
|
rootRunId: dispatch.run_id,
|
|
402
|
+
executionMode: metadata.identityVersion === 2 && metadata.executionMode === 'parallel'
|
|
403
|
+
? 'parallel' : 'serial',
|
|
356
404
|
extensions: loaded.extensions,
|
|
357
405
|
flowGraph: loaded.graph,
|
|
358
406
|
...options.lifecycle,
|
|
@@ -600,7 +648,10 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata {
|
|
|
600
648
|
const root = value as Partial<AuthoredRootMetadata>;
|
|
601
649
|
const surface = root.surface as Partial<SurfaceModuleAuthority> | undefined;
|
|
602
650
|
const sources = root.sources;
|
|
603
|
-
return root.kind === ROOT_KIND
|
|
651
|
+
return (root.kind === ROOT_KIND || root.kind === AUTHORED_PARALLEL_ROOT_KIND)
|
|
652
|
+
&& (root.kind !== AUTHORED_PARALLEL_ROOT_KIND
|
|
653
|
+
|| (root.identityVersion === 2 && root.executionMode === 'parallel'))
|
|
654
|
+
&& typeof root.flowName === 'string'
|
|
604
655
|
&& typeof root.flowPath === 'string' && /^[a-f0-9]{64}$/.test(root.sourceSha256 ?? '')
|
|
605
656
|
&& typeof root.inputPresent === 'boolean' && surface?.packageName === '@relayflows/surface'
|
|
606
657
|
&& typeof surface.version === 'string' && /^[a-f0-9]{64}$/.test(surface.packageSha256 ?? '')
|
|
@@ -608,7 +659,9 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata {
|
|
|
608
659
|
&& Array.isArray(sources) && sources.length > 0 && sources.every(isRootSourceAuthority)
|
|
609
660
|
&& (root.extensions === undefined || (Array.isArray(root.extensions) && root.extensions.every(isRootExtension)))
|
|
610
661
|
&& (root.localAgentStream === undefined
|
|
611
|
-
|| /^local-agent-[a-f0-9-]+$/.test(root.localAgentStream))
|
|
662
|
+
|| /^local-agent-[a-f0-9-]+$/.test(root.localAgentStream))
|
|
663
|
+
&& ((root.identityVersion === undefined && root.executionMode === undefined)
|
|
664
|
+
|| (root.identityVersion === 2 && (root.executionMode === 'serial' || root.executionMode === 'parallel')));
|
|
612
665
|
}
|
|
613
666
|
|
|
614
667
|
function isRootExtension(value: unknown): value is AuthoredRootExtension {
|
|
@@ -32,14 +32,16 @@ import { MAX_AFTER, displayLabel, type AuthoredStepEdges } from './authored-step
|
|
|
32
32
|
* body walks a few nodes per step. A walk that still exceeds `walkLimit` stops
|
|
33
33
|
* and says so (`afterTruncated`) rather than guessing silently.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* Most consumers use this as observational metadata. Parallel f.run follow-up
|
|
36
|
+
* IDs also use the causal frontier; the executor refuses a truncated frontier
|
|
37
|
+
* rather than choosing a replay key from incomplete predecessors.
|
|
37
38
|
*/
|
|
38
39
|
export const MAX_WALK = 10_000;
|
|
39
40
|
|
|
40
41
|
interface Frontier {
|
|
41
42
|
readonly steps: readonly string[];
|
|
42
43
|
readonly truncated: boolean;
|
|
44
|
+
readonly unregisteredAggregate: boolean;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
interface StepNode {
|
|
@@ -62,6 +64,7 @@ const EXPANDS_ON: Readonly<Record<Combinator, Outcome | 'always' | undefined>> =
|
|
|
62
64
|
|
|
63
65
|
interface Aggregate {
|
|
64
66
|
readonly promise: Promise<unknown>;
|
|
67
|
+
readonly combinator: Combinator;
|
|
65
68
|
/** Cached once settled: a settled promise never changes state. */
|
|
66
69
|
outcome?: Outcome;
|
|
67
70
|
readonly expandsOn: Outcome | 'always';
|
|
@@ -71,6 +74,10 @@ interface Aggregate {
|
|
|
71
74
|
export class AuthoredStepGraph {
|
|
72
75
|
private readonly stepPromises = new Map<number, string>();
|
|
73
76
|
private readonly aggregates = new Map<number, Aggregate>();
|
|
77
|
+
private readonly registeredAggregates = new Set<number>();
|
|
78
|
+
private readonly registeredCombinatorInternals = new Set<number>();
|
|
79
|
+
private readonly certifiedMembers = new Set<number>();
|
|
80
|
+
private readonly unsafeAggregates = new Set<number>();
|
|
74
81
|
private readonly contexts = new Map<number, Frontier>();
|
|
75
82
|
private readonly nodes = new Map<string, StepNode>();
|
|
76
83
|
private readonly edges = new Map<string, AuthoredStepEdges>();
|
|
@@ -78,6 +85,7 @@ export class AuthoredStepGraph {
|
|
|
78
85
|
constructor(
|
|
79
86
|
private readonly causesOf: (asyncId: number) => readonly number[],
|
|
80
87
|
private readonly walkLimit = MAX_WALK,
|
|
88
|
+
private readonly unobservedJoinShape: (asyncId: number) => boolean = () => false,
|
|
81
89
|
) {}
|
|
82
90
|
|
|
83
91
|
/** A combinator's aggregate and its members, for expansion once it settles. */
|
|
@@ -86,10 +94,19 @@ export class AuthoredStepGraph {
|
|
|
86
94
|
promise: Promise<unknown>,
|
|
87
95
|
combinator: Combinator,
|
|
88
96
|
members: readonly number[],
|
|
97
|
+
created: ReadonlySet<number> = new Set(),
|
|
89
98
|
): void {
|
|
99
|
+
this.registeredAggregates.add(aggregateId);
|
|
100
|
+
for (const id of created) this.registeredCombinatorInternals.add(id);
|
|
101
|
+
if (combinator === 'race' || combinator === 'any') this.unsafeAggregates.add(aggregateId);
|
|
90
102
|
const expandsOn = EXPANDS_ON[combinator];
|
|
91
103
|
if (expandsOn === undefined || members.length === 0) return;
|
|
92
|
-
this.aggregates.set(aggregateId, { promise, expandsOn, members });
|
|
104
|
+
this.aggregates.set(aggregateId, { promise, combinator, expandsOn, members });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Only direct steps and branch calls proven by the body grammar are safe members. */
|
|
108
|
+
certifyMember(asyncId: number): void {
|
|
109
|
+
this.certifiedMembers.add(asyncId);
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
/**
|
|
@@ -119,10 +136,19 @@ export class AuthoredStepGraph {
|
|
|
119
136
|
return this.edges.get(step);
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
/** Causal predecessors of a step being declared in the current async context. */
|
|
140
|
+
predecessorsAt(contextId: number): Frontier {
|
|
141
|
+
return this.frontierOf(contextId);
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
/** Drop the walk state; recorded edges stay readable for late record writes. */
|
|
123
145
|
release(): void {
|
|
124
146
|
this.stepPromises.clear();
|
|
125
147
|
this.aggregates.clear();
|
|
148
|
+
this.registeredAggregates.clear();
|
|
149
|
+
this.registeredCombinatorInternals.clear();
|
|
150
|
+
this.certifiedMembers.clear();
|
|
151
|
+
this.unsafeAggregates.clear();
|
|
126
152
|
this.contexts.clear();
|
|
127
153
|
this.nodes.clear();
|
|
128
154
|
}
|
|
@@ -132,6 +158,7 @@ export class AuthoredStepGraph {
|
|
|
132
158
|
if (memo !== undefined) return memo;
|
|
133
159
|
const candidates = new Set<string>();
|
|
134
160
|
let truncated = false;
|
|
161
|
+
let unregisteredAggregate = false;
|
|
135
162
|
const visited = new Set<number>([start]);
|
|
136
163
|
const queue = [start];
|
|
137
164
|
// Breadth-first, so that a walk cut short by the limit has already found
|
|
@@ -143,10 +170,24 @@ export class AuthoredStepGraph {
|
|
|
143
170
|
candidates.add(step);
|
|
144
171
|
continue;
|
|
145
172
|
}
|
|
173
|
+
// A join's resolution edge contains only its settling member. In
|
|
174
|
+
// parallel mode the full member list is safe only if our combinator
|
|
175
|
+
// observer registered it before the promise settled. The creation and
|
|
176
|
+
// resolution contexts also catch an unobserved join created after a
|
|
177
|
+
// previous step, where root-adoption alone cannot detect it.
|
|
178
|
+
const aggregate = this.aggregates.get(current);
|
|
179
|
+
const rejectedAll = aggregate?.combinator === 'all'
|
|
180
|
+
&& promiseOutcome(aggregate.promise) === 'rejected';
|
|
181
|
+
const possibleJoin = this.unsafeAggregates.has(current) || rejectedAll
|
|
182
|
+
|| (this.unobservedJoinShape(current)
|
|
183
|
+
&& !this.registeredAggregates.has(current) && !this.certifiedMembers.has(current)
|
|
184
|
+
&& !this.registeredCombinatorInternals.has(current));
|
|
185
|
+
unregisteredAggregate ||= possibleJoin;
|
|
146
186
|
const answered = current === start ? undefined : this.contexts.get(current);
|
|
147
187
|
if (answered !== undefined) {
|
|
148
188
|
for (const found of answered.steps) candidates.add(found);
|
|
149
189
|
truncated ||= answered.truncated;
|
|
190
|
+
unregisteredAggregate ||= answered.unregisteredAggregate;
|
|
150
191
|
continue;
|
|
151
192
|
}
|
|
152
193
|
for (const id of [...this.waitedFor(current), ...this.causesOf(current)]) {
|
|
@@ -162,7 +203,11 @@ export class AuthoredStepGraph {
|
|
|
162
203
|
}
|
|
163
204
|
}
|
|
164
205
|
const reduced = this.reduce(candidates);
|
|
165
|
-
const frontier = Object.freeze({
|
|
206
|
+
const frontier = Object.freeze({
|
|
207
|
+
steps: reduced.steps,
|
|
208
|
+
truncated: truncated || reduced.truncated,
|
|
209
|
+
unregisteredAggregate,
|
|
210
|
+
});
|
|
166
211
|
this.contexts.set(start, frontier);
|
|
167
212
|
return frontier;
|
|
168
213
|
}
|
|
@@ -184,7 +229,7 @@ export class AuthoredStepGraph {
|
|
|
184
229
|
* reduction reads recorded `after` lists; when one it reads is incomplete,
|
|
185
230
|
* a redundant ancestor can survive, and the result says so.
|
|
186
231
|
*/
|
|
187
|
-
private reduce(candidates: ReadonlySet<string>): Frontier {
|
|
232
|
+
private reduce(candidates: ReadonlySet<string>): Pick<Frontier, 'steps' | 'truncated'> {
|
|
188
233
|
const ordered = [...candidates].sort((a, b) => this.orderOf(a) - this.orderOf(b));
|
|
189
234
|
if (ordered.length <= 1) return { steps: ordered, truncated: false };
|
|
190
235
|
// An ancestor is always registered before its descendant, so nothing
|
|
@@ -150,7 +150,8 @@ export async function alsoRecord(
|
|
|
150
150
|
* Fold the root's stream back into one record per authored step.
|
|
151
151
|
*
|
|
152
152
|
* A `completed` record supersedes the `admitted` one for the same step; first
|
|
153
|
-
* admission order is preserved
|
|
153
|
+
* admission order is preserved for each step, while different children can
|
|
154
|
+
* first appear in completion order when they finish concurrently.
|
|
154
155
|
* Records this version does not recognise are skipped rather than trusted.
|
|
155
156
|
*/
|
|
156
157
|
export async function readAuthoredStepIndex(
|
package/src/authored-verdict.ts
CHANGED
|
@@ -21,6 +21,8 @@ import type { JournalEvent } from './journal-reader.js';
|
|
|
21
21
|
|
|
22
22
|
/** The `instruction` discriminator `authored-root.ts` writes into the root step. */
|
|
23
23
|
export const AUTHORED_ROOT_KIND = 'relayflows.authored-root.v1';
|
|
24
|
+
/** Older CLIs do not recognize this root and must refuse a parallel rollback. */
|
|
25
|
+
export const AUTHORED_PARALLEL_ROOT_KIND = 'relayflows.authored-root.parallel.v2';
|
|
24
26
|
|
|
25
27
|
export interface AuthoredVerdict {
|
|
26
28
|
readonly reason: LoweredCompletionReason;
|
|
@@ -76,7 +78,8 @@ export function declaresAuthoredRoot(spawned: JournalEvent): boolean {
|
|
|
76
78
|
if (step === null || step['id'] !== 'authored-root') return false;
|
|
77
79
|
if (typeof step['instruction'] !== 'string') return false;
|
|
78
80
|
try {
|
|
79
|
-
|
|
81
|
+
const kind = record(JSON.parse(step['instruction']))?.['kind'];
|
|
82
|
+
return kind === AUTHORED_ROOT_KIND || kind === AUTHORED_PARALLEL_ROOT_KIND;
|
|
80
83
|
} catch {
|
|
81
84
|
return false;
|
|
82
85
|
}
|
|
@@ -19,7 +19,8 @@ import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
|
19
19
|
import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
20
20
|
import type { StepFailedDetails } from './failure-kinds.js';
|
|
21
21
|
import { LOCAL_AGENT_WORKSPACE } from './local-agent-pins.js';
|
|
22
|
-
import { authoredWorkerSlots, type AuthoredWorkerSlots } from './worker-slots.js';
|
|
22
|
+
import { authoredWorkerSlots, WorkerSlots, type AuthoredWorkerSlots } from './worker-slots.js';
|
|
23
|
+
import { validateStepSecrets } from './step-secrets.js';
|
|
23
24
|
|
|
24
25
|
const AGENT_TIMEOUT = Symbol('declared agent timeout');
|
|
25
26
|
const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
|
|
@@ -177,6 +178,8 @@ export function authoredWorkerRunner(
|
|
|
177
178
|
}
|
|
178
179
|
},
|
|
179
180
|
async agent(id: string, options: AgentOptions, verification?: NamedGate): Promise<AgentResult> {
|
|
181
|
+
const secrets = validateStepSecrets((options as AgentOptions & { secrets?: unknown }).secrets,
|
|
182
|
+
agentEnvironment ?? process.env);
|
|
180
183
|
if (options.workspace !== undefined && localAgentStream !== undefined) {
|
|
181
184
|
throw new AuthoredFlowExecutionError('unsupported_workspace_permission',
|
|
182
185
|
'The local agent worker accepts stream-only steps. Remove workspace or attach a worker that holds its revision pins.');
|
|
@@ -277,6 +280,7 @@ export function authoredWorkerRunner(
|
|
|
277
280
|
: snapshotJsonValue(permissions, 'f.agent options.permissions') as unknown as PermissionsSpec;
|
|
278
281
|
const output = await run({
|
|
279
282
|
id, type: 'agent', instruction: options.task,
|
|
283
|
+
...(secrets.length ? { secrets } : {}),
|
|
280
284
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
281
285
|
...(permissionsSnapshot === undefined ? {} : { permissions: permissionsSnapshot }),
|
|
282
286
|
...(localAgentStream === undefined ? {} : { surfaces: {
|
|
@@ -323,11 +327,14 @@ export function authoredWorkerRunner(
|
|
|
323
327
|
}
|
|
324
328
|
const snapshot = options === undefined ? undefined : snapshotJsonValue(options, 'f.llm options');
|
|
325
329
|
if (snapshot !== undefined && (typeof snapshot !== 'object' || snapshot === null || Array.isArray(snapshot)
|
|
326
|
-
|| Object.keys(snapshot).some(key => !['output', 'cli', 'model'].includes(key)))) {
|
|
327
|
-
throw new AuthoredFlowExecutionError('llm_cli_unresolved', 'f.llm options accepts only output, cli, and
|
|
330
|
+
|| Object.keys(snapshot).some(key => !['output', 'cli', 'model', 'secrets'].includes(key)))) {
|
|
331
|
+
throw new AuthoredFlowExecutionError('llm_cli_unresolved', 'f.llm options accepts only output, cli, model, and secrets.');
|
|
328
332
|
}
|
|
333
|
+
const secrets = validateStepSecrets((snapshot as { secrets?: unknown } | undefined)?.secrets,
|
|
334
|
+
agentEnvironment ?? process.env);
|
|
329
335
|
const output = await run({
|
|
330
336
|
id, type: 'llm', prompt, ...snapshot as unknown as LlmOptions,
|
|
337
|
+
...(secrets.length ? { secrets } : {}),
|
|
331
338
|
...(verification === undefined ? {} : { verification }),
|
|
332
339
|
});
|
|
333
340
|
if (options === undefined && typeof output !== 'string') {
|
|
@@ -349,8 +356,14 @@ export function authoredWorkerRunner(
|
|
|
349
356
|
export function authoredDeterministicRunner(
|
|
350
357
|
name: string, journal: JournalClient, journalSteps: AuthoredFlowJournalStep[], budget: AuthoredBudget,
|
|
351
358
|
context: AuthoredStepContext = {},
|
|
359
|
+
parallel = false,
|
|
352
360
|
) {
|
|
353
361
|
const rootRunId = context.rootRunId;
|
|
362
|
+
// run.start drives a deterministic child to completion before replying. The
|
|
363
|
+
// daemon reads one connection in order, so sharing the body's connection
|
|
364
|
+
// would serialize Promise.all. Each admitted child gets its own connection;
|
|
365
|
+
// this limit bounds both daemon threads and shell commands per authored body.
|
|
366
|
+
const slots = new WorkerSlots(8);
|
|
354
367
|
// One lowering, two readers. `record` changes what the child run's verdict
|
|
355
368
|
// MEANS, so the reader changes with it; everything before the read — spec,
|
|
356
369
|
// admission key, budget, child index — is deliberately shared, because a
|
|
@@ -358,6 +371,7 @@ export function authoredDeterministicRunner(
|
|
|
358
371
|
function lower<T>(
|
|
359
372
|
id: string, command: string, terminal: boolean, leaseMs: number | undefined,
|
|
360
373
|
verification: NamedGate | undefined, onNonZero: 'record' | undefined,
|
|
374
|
+
secrets: string[] | undefined,
|
|
361
375
|
read: (outcome: import('./protocol.js').RunOutcome) => Promise<T>,
|
|
362
376
|
): Promise<T> {
|
|
363
377
|
const spec = toKernelSpec(compileSpec({
|
|
@@ -365,6 +379,7 @@ export function authoredDeterministicRunner(
|
|
|
365
379
|
name: `${name}/${id}`,
|
|
366
380
|
steps: [{
|
|
367
381
|
id, type: 'deterministic', command,
|
|
382
|
+
...(secrets?.length ? { secrets } : {}),
|
|
368
383
|
...(leaseMs === undefined ? {} : { lease_ms: leaseMs }),
|
|
369
384
|
...(onNonZero === undefined ? {} : { onNonZero }),
|
|
370
385
|
...(verification === undefined ? {} : { verification }),
|
|
@@ -377,16 +392,21 @@ export function authoredDeterministicRunner(
|
|
|
377
392
|
});
|
|
378
393
|
return read(outcome);
|
|
379
394
|
};
|
|
380
|
-
|
|
381
|
-
|
|
395
|
+
const admit = async () => terminal
|
|
396
|
+
? consume(await journal.runStart(spec, undefined, admissionKey, false, parallel))
|
|
397
|
+
: budget.execute(journal, spec, consume, admissionKey, parallel);
|
|
398
|
+
return parallel ? slots.run(admit) : admit();
|
|
382
399
|
}
|
|
383
|
-
const lowerRun = (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate
|
|
384
|
-
|
|
400
|
+
const lowerRun = (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate,
|
|
401
|
+
secrets?: string[]): Promise<string> =>
|
|
402
|
+
lower(id, command, terminal, leaseMs, verification, undefined, secrets,
|
|
385
403
|
outcome => readSuccessfulOutput(journal, outcome, id, journalSteps, context));
|
|
386
404
|
/** `f.run(..., { onNonZero: 'record' })`: the exit code is the result, not a throw. */
|
|
387
|
-
lowerRun.recording = (id: string, command: string, leaseMs?: number, verification?: NamedGate
|
|
388
|
-
|
|
405
|
+
lowerRun.recording = (id: string, command: string, leaseMs?: number, verification?: NamedGate,
|
|
406
|
+
secrets?: string[]): Promise<RecordedRunOutcome> =>
|
|
407
|
+
lower(id, command, false, leaseMs, verification, 'record', secrets,
|
|
389
408
|
outcome => readRecordedOutcome(journal, outcome, id, journalSteps, context));
|
|
409
|
+
lowerRun.stop = (reason: unknown): void => slots.close(reason);
|
|
390
410
|
return lowerRun;
|
|
391
411
|
}
|
|
392
412
|
|
package/src/cli/check.ts
CHANGED
|
@@ -492,8 +492,8 @@ function findConfig(start: string): string | undefined {
|
|
|
492
492
|
function systemProbes(flowDirectory: string, config: ProjectConfig, environment?: NodeJS.ProcessEnv): PreflightProbes {
|
|
493
493
|
return {
|
|
494
494
|
helper: helperReady,
|
|
495
|
-
cli: (cli, source, model, execution) => probeCli(cli,
|
|
496
|
-
source === 'project' ? config.directory : flowDirectory, model, execution, environment),
|
|
495
|
+
cli: (cli, source, model, execution, secrets) => probeCli(cli,
|
|
496
|
+
source === 'project' ? config.directory : flowDirectory, model, execution, environment, secrets),
|
|
497
497
|
executor: (trigger) => config.executors.includes(trigger.executor),
|
|
498
498
|
// A deterministic step runs in the daemon's working directory — the
|
|
499
499
|
// directory `flows run` was invoked from, or Cloud's code mount — not in
|
|
@@ -521,7 +521,7 @@ function bindResolvedCliPaths(
|
|
|
521
521
|
if (resolution === undefined) return step;
|
|
522
522
|
const directory = resolution.source === 'project' ? configDirectory : flowDirectory;
|
|
523
523
|
const managed = step.type === 'agent' && communicationInstruction(step.instruction) !== undefined;
|
|
524
|
-
const outcome = probeCache.get(cliProbeKey(resolution, managed));
|
|
524
|
+
const outcome = probeCache.get(cliProbeKey(resolution, managed, step.secrets));
|
|
525
525
|
const executable = outcome !== undefined && 'result' in outcome
|
|
526
526
|
? outcome.result.executable
|
|
527
527
|
: undefined;
|
package/src/cli/cli-probe.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { adapterIdentification, authenticationProbe, classifyModelProbeFailure,
|
|
|
7
7
|
import { MODEL_ENV } from '../worker-cli.js';
|
|
8
8
|
import { CliProbeError, type CliProbeResult } from '../preflight.js';
|
|
9
9
|
import { pinCliAlias, pinCliAliasSync } from './pinned-cli-alias.js';
|
|
10
|
+
import { agentProbeEnvironment } from '../step-secrets.js';
|
|
10
11
|
|
|
11
12
|
interface ProbeRequest {
|
|
12
13
|
argv0?: string;
|
|
@@ -30,7 +31,8 @@ export async function probeCliAsync(...args: Parameters<typeof probeSequence>):
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function resolveExecutable(command: string, directory: string, environment: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
33
|
-
return driveSync(executableSequence(command, directory,
|
|
34
|
+
return driveSync(executableSequence(command, directory,
|
|
35
|
+
agentProbeEnvironment(environment, 'relayflows-wrapper-v1')));
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
|
|
@@ -94,23 +96,24 @@ function* probeSequence(
|
|
|
94
96
|
model?: string,
|
|
95
97
|
execution?: 'managed',
|
|
96
98
|
sourceEnvironment: NodeJS.ProcessEnv = process.env,
|
|
99
|
+
grants?: string[],
|
|
97
100
|
): Generator<ProbeRequest, CliProbeResult, ProbeOutput> {
|
|
98
|
-
const
|
|
101
|
+
const kind = cliAdapterKind(cli);
|
|
102
|
+
const environment = execution === 'managed'
|
|
103
|
+
? { ...brokerEnvironment(sourceEnvironment), ...agentEnvironment(cli, sourceEnvironment, grants) }
|
|
104
|
+
: agentProbeEnvironment(sourceEnvironment, kind, grants);
|
|
105
|
+
const executable = yield* executableSequence(cli, directory, environment);
|
|
99
106
|
if (executable === undefined) return { exists: false, authenticated: false };
|
|
100
107
|
// The declaration selects the adapter; the canonical executable selects
|
|
101
108
|
// the bytes. Package-manager links commonly name `claude` or `codex` while
|
|
102
109
|
// targeting a generic `cli.js`, whose basename must not rewrite the adapter
|
|
103
110
|
// contract after resolution.
|
|
104
|
-
const kind = cliAdapterKind(cli);
|
|
105
111
|
// Relay owns interactive CLI launch/injection. Its generic PTY path is not
|
|
106
112
|
// the headless wrapper protocol; do not demand that protocol from Gemini,
|
|
107
113
|
// Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
|
|
108
114
|
if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
|
|
109
115
|
return { exists: true, supported: true, authenticated: 'unverified', executable };
|
|
110
116
|
}
|
|
111
|
-
const environment = execution === 'managed'
|
|
112
|
-
? { ...brokerEnvironment(sourceEnvironment), ...agentEnvironment(cli, sourceEnvironment) }
|
|
113
|
-
: sourceEnvironment;
|
|
114
117
|
const probe = (invocation: CliInvocation): ProbeRequest => ({
|
|
115
118
|
executable,
|
|
116
119
|
argv0: basename(cli),
|
package/src/cli/run.ts
CHANGED
|
@@ -415,9 +415,11 @@ export async function resumeFlow(
|
|
|
415
415
|
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
416
416
|
diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } };
|
|
417
417
|
}
|
|
418
|
-
if (error instanceof AuthoredFlowExecutionError
|
|
418
|
+
if (error instanceof AuthoredFlowExecutionError
|
|
419
|
+
&& (error.code === 'unsafe_parallel_runtime' || error.code === 'unsupported_promise_lifecycle')) {
|
|
419
420
|
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
420
|
-
diagnostics: [{ severity: 'refusal',
|
|
421
|
+
diagnostics: [{ severity: 'refusal',
|
|
422
|
+
kind: error.code === 'unsafe_parallel_runtime' ? error.code : 'invalid_spec', message: error.message }] } };
|
|
421
423
|
}
|
|
422
424
|
if (error instanceof AuthoredFlowExecutionError
|
|
423
425
|
&& (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required'
|