@relayflows/sdk 2.0.36 → 2.0.38
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/agent-reaper.d.ts +1 -1
- package/dist/agent-reaper.d.ts.map +1 -1
- package/dist/agent-reaper.js +6 -4
- package/dist/agent-reaper.js.map +1 -1
- package/dist/authored-budget.d.ts +26 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +72 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +7 -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 +2 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +3 -2
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +2 -0
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.js +6 -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 +5 -3
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +2 -0
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-preflight.d.ts +1 -1
- package/dist/authored-preflight.d.ts.map +1 -1
- package/dist/authored-preflight.js +2 -2
- package/dist/authored-preflight.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 +146 -6
- package/dist/authored-root.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 +2 -1
- package/dist/authored-verdict.js.map +1 -1
- package/dist/authored-worker-step.d.ts +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +3 -2
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/child-stop.d.ts +3 -1
- package/dist/child-stop.d.ts.map +1 -1
- package/dist/child-stop.js +42 -2
- package/dist/child-stop.js.map +1 -1
- package/dist/cli/add-extension.js +1 -1
- package/dist/cli/add-extension.js.map +1 -1
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +28 -10
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +3 -2
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +79 -42
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +11 -3
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/pinned-cli-alias.d.ts +13 -0
- package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
- package/dist/cli/pinned-cli-alias.js +58 -0
- package/dist/cli/pinned-cli-alias.js.map +1 -0
- package/dist/cli/run.d.ts +8 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +106 -17
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +18 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +9 -2
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -2
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +27 -9
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +16 -7
- package/dist/compile.js.map +1 -1
- package/dist/decimal-dollars.d.ts +12 -0
- package/dist/decimal-dollars.d.ts.map +1 -0
- package/dist/decimal-dollars.js +39 -0
- package/dist/decimal-dollars.js.map +1 -0
- package/dist/effect-channel.d.ts.map +1 -1
- package/dist/effect-channel.js +2 -0
- package/dist/effect-channel.js.map +1 -1
- package/dist/failure-kinds.d.ts +9 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +6 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +1 -1
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +143 -14
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-extension-manifest.d.ts +1 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -1
- package/dist/flow-extension-manifest.js +12 -3
- package/dist/flow-extension-manifest.js.map +1 -1
- package/dist/helper-instruction.d.ts +3 -0
- package/dist/helper-instruction.d.ts.map +1 -0
- package/dist/helper-instruction.js +3 -0
- package/dist/helper-instruction.js.map +1 -0
- package/dist/hosted-extension-manifest.d.ts.map +1 -1
- package/dist/hosted-extension-manifest.js +25 -4
- package/dist/hosted-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/hosted-extension-sandbox.js +2 -2
- package/dist/journal-client.d.ts +5 -2
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +12 -4
- 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 +10 -3
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent-environment.d.ts +13 -0
- package/dist/local-agent-environment.d.ts.map +1 -0
- package/dist/local-agent-environment.js +104 -0
- package/dist/local-agent-environment.js.map +1 -0
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +2 -2
- package/dist/local-agent.js.map +1 -1
- package/dist/mcp-stdio.d.ts +2 -1
- package/dist/mcp-stdio.d.ts.map +1 -1
- package/dist/mcp-stdio.js +38 -21
- package/dist/mcp-stdio.js.map +1 -1
- package/dist/preflight.d.ts +2 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +30 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/reported-cost-total.d.ts +41 -0
- package/dist/reported-cost-total.d.ts.map +1 -0
- package/dist/reported-cost-total.js +63 -0
- package/dist/reported-cost-total.js.map +1 -0
- package/dist/reported-cost.d.ts +24 -0
- package/dist/reported-cost.d.ts.map +1 -0
- package/dist/reported-cost.js +49 -0
- package/dist/reported-cost.js.map +1 -0
- package/dist/resolved-cli-identity.d.ts +14 -0
- package/dist/resolved-cli-identity.d.ts.map +1 -0
- package/dist/resolved-cli-identity.js +19 -0
- package/dist/resolved-cli-identity.js.map +1 -0
- package/dist/run-state.d.ts +9 -11
- package/dist/run-state.d.ts.map +1 -1
- package/dist/run-state.js +15 -38
- package/dist/run-state.js.map +1 -1
- package/dist/worker-cli.d.ts +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +118 -42
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts +3 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +47 -9
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +8 -3
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +3 -1
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +93 -53
- package/dist/wrapper-session.js.map +1 -1
- package/dist/yaml-helper-effect.d.ts.map +1 -1
- package/dist/yaml-helper-effect.js +2 -1
- package/dist/yaml-helper-effect.js.map +1 -1
- package/dist/yaml-helpers.d.ts.map +1 -1
- package/dist/yaml-helpers.js +2 -1
- package/dist/yaml-helpers.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-reaper.ts +7 -4
- package/src/authored-budget.ts +73 -1
- package/src/authored-flow-error.ts +6 -0
- package/src/authored-flow-executor.ts +4 -1
- package/src/authored-helper-effect.ts +3 -2
- package/src/authored-mcp.ts +2 -0
- package/src/authored-node-entry.ts +8 -1
- package/src/authored-node-runner.ts +5 -3
- package/src/authored-plugin-effect.ts +2 -0
- package/src/authored-preflight.ts +2 -2
- package/src/authored-root.ts +157 -7
- package/src/authored-verdict.ts +2 -1
- package/src/authored-worker-step.ts +3 -1
- package/src/child-stop.ts +40 -1
- package/src/cli/add-extension.ts +1 -1
- package/src/cli/check.ts +31 -7
- package/src/cli/cli-probe.ts +80 -42
- package/src/cli/direct-run.ts +11 -2
- package/src/cli/pinned-cli-alias.ts +74 -0
- package/src/cli/run.ts +118 -14
- package/src/cli/step-failure.ts +18 -0
- package/src/cloud-run.ts +9 -2
- package/src/communication/local.ts +3 -2
- package/src/communication/worker.ts +29 -11
- package/src/compile.ts +28 -13
- package/src/decimal-dollars.ts +38 -0
- package/src/effect-channel.ts +2 -0
- package/src/failure-kinds.ts +14 -0
- package/src/flow-extension-loader.ts +154 -14
- package/src/flow-extension-manifest.ts +6 -3
- package/src/helper-instruction.ts +2 -0
- package/src/hosted-extension-manifest.ts +28 -5
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/hosted-extension-sandbox.ts +2 -2
- package/src/journal-client.ts +14 -5
- package/src/llm-worker.ts +11 -4
- package/src/local-agent-environment.ts +92 -0
- package/src/local-agent.ts +2 -1
- package/src/mcp-stdio.ts +34 -20
- package/src/preflight.ts +2 -0
- package/src/protocol.ts +31 -0
- package/src/reported-cost-total.ts +82 -0
- package/src/reported-cost.ts +51 -0
- package/src/resolved-cli-identity.ts +42 -0
- package/src/run-state.ts +25 -37
- package/src/worker-cli.ts +126 -39
- package/src/worker-lease.ts +50 -9
- package/src/worker.ts +14 -5
- package/src/wrapper-session.ts +89 -56
- package/src/yaml-helper-effect.ts +2 -1
- package/src/yaml-helpers.ts +2 -1
package/src/child-stop.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { execFileSync, type ChildProcess } from 'node:child_process';
|
|
|
6
6
|
* the `flows run` event loop alive long after the step itself has settled.
|
|
7
7
|
*/
|
|
8
8
|
export const FORCE_KILL_DELAY_MS = 1_000;
|
|
9
|
+
/** Bound for proving that a forced process group has actually disappeared. */
|
|
10
|
+
export const GROUP_EXIT_CONFIRM_TIMEOUT_MS = 1_000;
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* The one stop path for a spawned agent process.
|
|
@@ -65,6 +67,7 @@ export function childStop(
|
|
|
65
67
|
child: ChildProcess,
|
|
66
68
|
ownsGroup: boolean,
|
|
67
69
|
forceKillDelayMs: number = FORCE_KILL_DELAY_MS,
|
|
70
|
+
onStopped: (error?: Error) => void = () => {},
|
|
68
71
|
): ChildStop {
|
|
69
72
|
// Pinned at the spawn rather than read per signal. Every interesting use of
|
|
70
73
|
// this id happens AFTER the direct child has been reaped — the escalation
|
|
@@ -74,6 +77,17 @@ export function childStop(
|
|
|
74
77
|
// which is exactly the window both of those need to address.
|
|
75
78
|
const pid = child.pid;
|
|
76
79
|
let forceTimer: NodeJS.Timeout | undefined;
|
|
80
|
+
let groupPoll: NodeJS.Timeout | undefined;
|
|
81
|
+
let forced = false;
|
|
82
|
+
let forcedAt: number | undefined;
|
|
83
|
+
let stopped = false;
|
|
84
|
+
const notifyStopped = (error?: Error): void => {
|
|
85
|
+
if (stopped) return;
|
|
86
|
+
stopped = true;
|
|
87
|
+
if (groupPoll !== undefined) clearTimeout(groupPoll);
|
|
88
|
+
groupPoll = undefined;
|
|
89
|
+
onStopped(error);
|
|
90
|
+
};
|
|
77
91
|
const cancel = (): void => {
|
|
78
92
|
if (forceTimer !== undefined) clearTimeout(forceTimer);
|
|
79
93
|
forceTimer = undefined;
|
|
@@ -106,6 +120,21 @@ export function childStop(
|
|
|
106
120
|
}
|
|
107
121
|
});
|
|
108
122
|
};
|
|
123
|
+
const awaitGroupExit = (): void => {
|
|
124
|
+
if (!groupAnswers()) {
|
|
125
|
+
notifyStopped();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (forcedAt !== undefined && Date.now() - forcedAt >= GROUP_EXIT_CONFIRM_TIMEOUT_MS) {
|
|
129
|
+
notifyStopped(new Error(`Process groups did not stop answering within ${GROUP_EXIT_CONFIRM_TIMEOUT_MS}ms after SIGKILL.`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (groupPoll !== undefined) return;
|
|
133
|
+
groupPoll = setTimeout(() => {
|
|
134
|
+
groupPoll = undefined;
|
|
135
|
+
awaitGroupExit();
|
|
136
|
+
}, 10);
|
|
137
|
+
};
|
|
109
138
|
const signalTree = (name: NodeJS.Signals): void => {
|
|
110
139
|
if (ownsGroup && pid !== undefined) {
|
|
111
140
|
escapedGroups ??= descendantGroups(pid);
|
|
@@ -127,6 +156,9 @@ export function childStop(
|
|
|
127
156
|
kill: (): void => {
|
|
128
157
|
cancel();
|
|
129
158
|
signalTree('SIGKILL');
|
|
159
|
+
forced = true;
|
|
160
|
+
forcedAt ??= Date.now();
|
|
161
|
+
awaitGroupExit();
|
|
130
162
|
},
|
|
131
163
|
terminate: (): void => {
|
|
132
164
|
cancel();
|
|
@@ -134,6 +166,9 @@ export function childStop(
|
|
|
134
166
|
forceTimer = setTimeout(() => {
|
|
135
167
|
forceTimer = undefined;
|
|
136
168
|
signalTree('SIGKILL');
|
|
169
|
+
forced = true;
|
|
170
|
+
forcedAt ??= Date.now();
|
|
171
|
+
awaitGroupExit();
|
|
137
172
|
}, forceKillDelayMs);
|
|
138
173
|
// Deliberately REFERENCED, unlike every other timer we arm. The survivor
|
|
139
174
|
// this escalation exists for is the one that ignored `SIGTERM` and holds
|
|
@@ -144,9 +179,13 @@ export function childStop(
|
|
|
144
179
|
// `maySettleOnChildExit` confirms the group is empty.
|
|
145
180
|
},
|
|
146
181
|
maySettleOnChildExit: (): boolean => {
|
|
147
|
-
if (forceTimer === undefined)
|
|
182
|
+
if (forceTimer === undefined && !forced) {
|
|
183
|
+
notifyStopped();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
148
186
|
if (groupAnswers()) return false;
|
|
149
187
|
cancel();
|
|
188
|
+
notifyStopped();
|
|
150
189
|
return true;
|
|
151
190
|
},
|
|
152
191
|
};
|
package/src/cli/add-extension.ts
CHANGED
|
@@ -36,7 +36,7 @@ function eventKeys(manifest: FlowExtensionManifest): readonly string[] {
|
|
|
36
36
|
|
|
37
37
|
function formatBudget(budget: FlowExtensionManifest['permissions']['budget']): string {
|
|
38
38
|
if (budget === undefined) return 'inherits base';
|
|
39
|
-
return [budget.dollars === undefined ? '' : `$${budget.dollars}`, budget.wallclock ?? ''].filter(Boolean).join(' / ') || 'inherits base';
|
|
39
|
+
return [budget.tokens === undefined ? '' : `${budget.tokens} tokens`, budget.dollars === undefined ? '' : `$${budget.dollars}`, budget.wallclock ?? ''].filter(Boolean).join(' / ') || 'inherits base';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function describeExtension(manifest: FlowExtensionManifest): string[] {
|
package/src/cli/check.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { communicationInstruction } from '../communication/spec.js';
|
|
2
|
+
import { rememberResolvedCliIdentities } from '../resolved-cli-identity.js';
|
|
2
3
|
import { checkCommunicationEnvironment } from '../communication/preflight.js';
|
|
3
4
|
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
4
5
|
import { dirname, isAbsolute, join, parse as parsePath, resolve } from 'node:path';
|
|
@@ -15,6 +16,7 @@ import { parseMcpConfig } from '../mcp-config.js';
|
|
|
15
16
|
import type { StepGateInspection } from '../gate-contract.js';
|
|
16
17
|
import type { CheckFailureKind, CheckWarningKind } from '../failure-kinds.js';
|
|
17
18
|
import {
|
|
19
|
+
cliProbeKey,
|
|
18
20
|
preflight,
|
|
19
21
|
type CliResolution,
|
|
20
22
|
type CliProbeResult,
|
|
@@ -123,6 +125,8 @@ type CliProbeOutcomeMap = Map<string, CliProbeOutcome>;
|
|
|
123
125
|
* anything that depends on how the flow is about to be invoked opts in here.
|
|
124
126
|
*/
|
|
125
127
|
export interface CheckInvocation {
|
|
128
|
+
/** Environment used only by provider executable/auth/model probes. */
|
|
129
|
+
environment?: NodeJS.ProcessEnv;
|
|
126
130
|
/**
|
|
127
131
|
* Report `agent_worker_unresolved` when the spec has `agent` steps.
|
|
128
132
|
*
|
|
@@ -200,11 +204,15 @@ export function checkAuthoredFlow(
|
|
|
200
204
|
const projectConfig = options !== undefined
|
|
201
205
|
? options.projectConfig
|
|
202
206
|
: projectConfigOrOptions as ProjectConfig | undefined;
|
|
203
|
-
|
|
207
|
+
// Keep the exact probe result so the executable identity selected during
|
|
208
|
+
// readiness is the one written onto the admitted step. Without an
|
|
209
|
+
// explicit map, preflight would allocate a private cache and a bare CLI
|
|
210
|
+
// name could be resolved again under the later worker cwd.
|
|
211
|
+
const effectiveProbeCache = cliProbeCache ?? options?.probeCache ?? new Map<string, CliProbeOutcome>();
|
|
204
212
|
const config = projectConfig ?? readProjectConfig(dirname(absolutePath));
|
|
205
|
-
const probes = systemProbes(dirname(absolutePath), config);
|
|
213
|
+
const probes = systemProbes(dirname(absolutePath), config, invocation.environment);
|
|
206
214
|
const result = preflight(authoring, {
|
|
207
|
-
|
|
215
|
+
cliProbeCache: effectiveProbeCache,
|
|
208
216
|
projectCli: config.cli,
|
|
209
217
|
projectConfigPath: config.path,
|
|
210
218
|
projectSearchStart: dirname(absolutePath),
|
|
@@ -218,6 +226,7 @@ export function checkAuthoredFlow(
|
|
|
218
226
|
result.resolutions,
|
|
219
227
|
dirname(absolutePath),
|
|
220
228
|
config.directory,
|
|
229
|
+
effectiveProbeCache,
|
|
221
230
|
)
|
|
222
231
|
: undefined;
|
|
223
232
|
if (flow?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
@@ -450,10 +459,11 @@ function findConfig(start: string): string | undefined {
|
|
|
450
459
|
}
|
|
451
460
|
}
|
|
452
461
|
|
|
453
|
-
function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightProbes {
|
|
462
|
+
function systemProbes(flowDirectory: string, config: ProjectConfig, environment?: NodeJS.ProcessEnv): PreflightProbes {
|
|
454
463
|
return {
|
|
455
464
|
helper: helperReady,
|
|
456
|
-
cli: (cli, source, model, execution) => probeCli(cli,
|
|
465
|
+
cli: (cli, source, model, execution) => probeCli(cli,
|
|
466
|
+
source === 'project' ? config.directory : flowDirectory, model, execution, environment),
|
|
457
467
|
executor: (trigger) => config.executors.includes(trigger.executor),
|
|
458
468
|
// A deterministic step runs in the daemon's working directory — the
|
|
459
469
|
// directory `flows run` was invoked from, or Cloud's code mount — not in
|
|
@@ -469,20 +479,34 @@ function bindResolvedCliPaths(
|
|
|
469
479
|
resolutions: readonly CliResolution[],
|
|
470
480
|
flowDirectory: string,
|
|
471
481
|
configDirectory: string,
|
|
482
|
+
probeCache: ReadonlyMap<string, CliProbeOutcome>,
|
|
472
483
|
): FlowSpec {
|
|
473
484
|
const byStep = new Map(resolutions.map((resolution) => [resolution.stepId, resolution]));
|
|
474
|
-
|
|
485
|
+
const identities = new Map<string, string>();
|
|
486
|
+
const bound: FlowSpec = {
|
|
475
487
|
...flow,
|
|
476
488
|
steps: flow.steps.map((step) => {
|
|
477
489
|
if (step.type === 'deterministic') return step;
|
|
478
490
|
const resolution = byStep.get(step.id);
|
|
479
491
|
if (resolution === undefined) return step;
|
|
480
492
|
const directory = resolution.source === 'project' ? configDirectory : flowDirectory;
|
|
481
|
-
|
|
493
|
+
const managed = step.type === 'agent' && communicationInstruction(step.instruction) !== undefined;
|
|
494
|
+
const outcome = probeCache.get(cliProbeKey(resolution, managed));
|
|
495
|
+
const executable = outcome !== undefined && 'result' in outcome
|
|
496
|
+
? outcome.result.executable
|
|
497
|
+
: undefined;
|
|
498
|
+
const boundCli = executable !== undefined && isAbsolute(executable)
|
|
499
|
+
? executable
|
|
500
|
+
: canonicalCli(resolution.cli, directory);
|
|
501
|
+
if (executable !== undefined && isAbsolute(executable)) {
|
|
502
|
+
identities.set(step.id, resolution.cli);
|
|
503
|
+
}
|
|
504
|
+
return { ...step, cli: boundCli,
|
|
482
505
|
...(resolution.modelSource === 'adapter' && resolution.model !== undefined
|
|
483
506
|
? { model: resolution.model } : {}) };
|
|
484
507
|
}),
|
|
485
508
|
};
|
|
509
|
+
return rememberResolvedCliIdentities(bound, identities);
|
|
486
510
|
}
|
|
487
511
|
|
|
488
512
|
function canonicalCli(cli: string, directory: string): string {
|
package/src/cli/cli-probe.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { accessSync, constants } from 'node:fs';
|
|
2
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
1
|
+
import { accessSync, constants, realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { execFile, spawnSync } from 'node:child_process';
|
|
4
4
|
import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
|
|
5
5
|
import { adapterIdentification, authenticationProbe, classifyModelProbeFailure, cliAdapterKind,
|
|
6
6
|
displayInvocation, modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
|
|
7
7
|
import { MODEL_ENV } from '../worker-cli.js';
|
|
8
8
|
import { CliProbeError, type CliProbeResult } from '../preflight.js';
|
|
9
|
+
import { pinCliAlias, pinCliAliasSync } from './pinned-cli-alias.js';
|
|
9
10
|
|
|
10
11
|
interface ProbeRequest {
|
|
12
|
+
argv0?: string;
|
|
11
13
|
executable: string;
|
|
12
14
|
directory: string;
|
|
13
15
|
invocation: CliInvocation;
|
|
@@ -27,20 +29,27 @@ export async function probeCliAsync(...args: Parameters<typeof probeSequence>):
|
|
|
27
29
|
return next.value;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
export function resolveExecutable(command: string, directory: string): string | undefined {
|
|
31
|
-
return driveSync(executableSequence(command, directory));
|
|
32
|
+
export function resolveExecutable(command: string, directory: string, environment: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
33
|
+
return driveSync(executableSequence(command, directory, environment));
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
|
|
35
37
|
let next = sequence.next();
|
|
36
38
|
while (!next.done) {
|
|
37
39
|
const request = next.value;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const pinned = pinCliAliasSync(request.executable, request.argv0 ?? basename(request.executable));
|
|
41
|
+
const result = (() => {
|
|
42
|
+
try {
|
|
43
|
+
return spawnSync(pinned.executable, request.invocation.args, {
|
|
44
|
+
...probeOptions(request),
|
|
45
|
+
// Preserve the old synchronous probe contract: provider CLIs must not
|
|
46
|
+
// inherit a readable stdin that can block auth/identify probes.
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
pinned.release();
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
44
53
|
const failure = classifySpawnFailure(result.error, result.signal, request.invocation.timeoutMs);
|
|
45
54
|
if (failure !== undefined) throw failure;
|
|
46
55
|
next = sequence.next({ status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' });
|
|
@@ -48,30 +57,35 @@ function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
|
|
|
48
57
|
return next.value;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
function probeOptions({ directory, invocation, environment }: ProbeRequest) {
|
|
60
|
+
function probeOptions({ argv0, directory, invocation, environment }: ProbeRequest) {
|
|
52
61
|
const env = { ...environment };
|
|
53
62
|
delete env[MODEL_ENV];
|
|
54
63
|
if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
|
|
55
64
|
return { cwd: directory, encoding: 'utf8' as const, timeout: invocation.timeoutMs,
|
|
56
|
-
maxBuffer: 1024 * 1024, env };
|
|
65
|
+
maxBuffer: 1024 * 1024, env, ...(argv0 === undefined ? {} : { argv0 }) };
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
error
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
async function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
|
|
69
|
+
const pinned = await pinCliAlias(request.executable, request.argv0 ?? basename(request.executable));
|
|
70
|
+
try {
|
|
71
|
+
return await new Promise((resolve, reject) => {
|
|
72
|
+
const child = execFile(pinned.executable, request.invocation.args, probeOptions(request),
|
|
73
|
+
(error, stdout, stderr) => {
|
|
74
|
+
// Numeric exit codes are probe results; launch, timeout, signal and buffer
|
|
75
|
+
// errors are failures to collect a fact, just as in the synchronous driver.
|
|
76
|
+
if (error?.killed && typeof error.code !== 'string') return reject(new CliProbeError(`timeout:${request.invocation.timeoutMs}ms`));
|
|
77
|
+
const failure = classifySpawnFailure(
|
|
78
|
+
error !== null && typeof error.code !== 'number' && error.signal == null ? error : undefined,
|
|
79
|
+
error?.signal ?? null, request.invocation.timeoutMs);
|
|
80
|
+
if (failure !== undefined) return reject(failure);
|
|
81
|
+
resolve({ status: error === null ? 0 : typeof error.code === 'number' ? error.code : null,
|
|
82
|
+
stdout, stderr });
|
|
83
|
+
});
|
|
84
|
+
child.stdin?.end();
|
|
85
|
+
});
|
|
86
|
+
} finally {
|
|
87
|
+
await pinned.release();
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
function* probeSequence(
|
|
@@ -79,19 +93,31 @@ function* probeSequence(
|
|
|
79
93
|
directory: string,
|
|
80
94
|
model?: string,
|
|
81
95
|
execution?: 'managed',
|
|
96
|
+
sourceEnvironment: NodeJS.ProcessEnv = process.env,
|
|
82
97
|
): Generator<ProbeRequest, CliProbeResult, ProbeOutput> {
|
|
83
|
-
const executable = yield* executableSequence(cli, directory);
|
|
98
|
+
const executable = yield* executableSequence(cli, directory, sourceEnvironment);
|
|
84
99
|
if (executable === undefined) return { exists: false, authenticated: false };
|
|
85
|
-
|
|
100
|
+
// The declaration selects the adapter; the canonical executable selects
|
|
101
|
+
// the bytes. Package-manager links commonly name `claude` or `codex` while
|
|
102
|
+
// targeting a generic `cli.js`, whose basename must not rewrite the adapter
|
|
103
|
+
// contract after resolution.
|
|
104
|
+
const kind = cliAdapterKind(cli);
|
|
86
105
|
// Relay owns interactive CLI launch/injection. Its generic PTY path is not
|
|
87
106
|
// the headless wrapper protocol; do not demand that protocol from Gemini,
|
|
88
107
|
// Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
|
|
89
108
|
if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
|
|
90
|
-
return { exists: true, supported: true, authenticated: 'unverified' };
|
|
109
|
+
return { exists: true, supported: true, authenticated: 'unverified', executable };
|
|
91
110
|
}
|
|
92
111
|
const environment = execution === 'managed'
|
|
93
|
-
? { ...brokerEnvironment(
|
|
94
|
-
|
|
112
|
+
? { ...brokerEnvironment(sourceEnvironment), ...agentEnvironment(cli, sourceEnvironment) }
|
|
113
|
+
: sourceEnvironment;
|
|
114
|
+
const probe = (invocation: CliInvocation): ProbeRequest => ({
|
|
115
|
+
executable,
|
|
116
|
+
argv0: basename(cli),
|
|
117
|
+
directory,
|
|
118
|
+
invocation,
|
|
119
|
+
environment,
|
|
120
|
+
});
|
|
95
121
|
const identification = adapterIdentification(kind);
|
|
96
122
|
const identified = yield probe(identification.invocation);
|
|
97
123
|
if (
|
|
@@ -108,6 +134,7 @@ function* probeSequence(
|
|
|
108
134
|
exists: true,
|
|
109
135
|
supported: true,
|
|
110
136
|
authenticated: (yield probe(auth)).status === 0,
|
|
137
|
+
executable,
|
|
111
138
|
authCommand,
|
|
112
139
|
};
|
|
113
140
|
}
|
|
@@ -124,6 +151,7 @@ function* probeSequence(
|
|
|
124
151
|
supported: true,
|
|
125
152
|
authenticated: true,
|
|
126
153
|
modelAvailable: true,
|
|
154
|
+
executable,
|
|
127
155
|
authCommand,
|
|
128
156
|
modelCommand,
|
|
129
157
|
};
|
|
@@ -143,6 +171,7 @@ function* probeSequence(
|
|
|
143
171
|
supported: true,
|
|
144
172
|
authenticated,
|
|
145
173
|
modelAvailable: false,
|
|
174
|
+
executable,
|
|
146
175
|
authCommand,
|
|
147
176
|
modelCommand,
|
|
148
177
|
modelExitCode: scopedProbe.status,
|
|
@@ -176,19 +205,28 @@ function redactProbeOutput(text: string): string {
|
|
|
176
205
|
.replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
|
|
177
206
|
}
|
|
178
207
|
|
|
179
|
-
function* executableSequence(command: string, directory: string): Generator<ProbeRequest, string | undefined, ProbeOutput> {
|
|
208
|
+
function* executableSequence(command: string, directory: string, environment: NodeJS.ProcessEnv): Generator<ProbeRequest, string | undefined, ProbeOutput> {
|
|
180
209
|
if (command.includes('/') || isAbsolute(command)) {
|
|
181
210
|
const path = isAbsolute(command) ? command : resolve(directory, command);
|
|
182
|
-
|
|
183
|
-
accessSync(path, constants.X_OK);
|
|
184
|
-
return path;
|
|
185
|
-
} catch {
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
211
|
+
return canonicalExecutable(path);
|
|
188
212
|
}
|
|
189
213
|
const result = yield { executable: 'which', directory: process.cwd(),
|
|
190
|
-
invocation: { args: [command], timeoutMs: 5_000 }, environment
|
|
191
|
-
return result.status === 0
|
|
214
|
+
invocation: { args: [command], timeoutMs: 5_000 }, environment };
|
|
215
|
+
return result.status === 0
|
|
216
|
+
? canonicalExecutable(resolve(process.cwd(), result.stdout.trim()))
|
|
217
|
+
: undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function canonicalExecutable(path: string): string | undefined {
|
|
221
|
+
try {
|
|
222
|
+
accessSync(path, constants.X_OK);
|
|
223
|
+
// Dispatch must retain the exact target that readiness probed. Keeping a
|
|
224
|
+
// symlink path would allow the link to be retargeted between admission and
|
|
225
|
+
// worker spawn even though the compiled step carries an absolute path.
|
|
226
|
+
return realpathSync(path);
|
|
227
|
+
} catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
function classifySpawnFailure(
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { McpPreflightError } from './check-typescript.js';
|
|
|
6
6
|
import { attachLocalAgent } from '../local-agent.js';
|
|
7
7
|
import { LlmWorker } from '../llm-worker.js';
|
|
8
8
|
import { DEFAULT_LOCAL_AGENT_CAPACITY } from '../worker-slots.js';
|
|
9
|
+
import { localAgentEnvironment } from '../local-agent-environment.js';
|
|
9
10
|
import {
|
|
10
11
|
AuthoredFlowExecutionError,
|
|
11
12
|
} from '../authored-flow-executor.js';
|
|
@@ -20,7 +21,7 @@ import { authoredInput, authoredWorkerRemedy, localAgentRemedy } from './local-a
|
|
|
20
21
|
import {
|
|
21
22
|
authoredCompletion,
|
|
22
23
|
authoredHumanParked,
|
|
23
|
-
authoredStepFailure,
|
|
24
|
+
authoredStepFailure, rootLeaseLostReport,
|
|
24
25
|
connect,
|
|
25
26
|
emptyReport,
|
|
26
27
|
fromCheckReport,
|
|
@@ -38,6 +39,9 @@ export async function runDirectFlow(
|
|
|
38
39
|
dataDir: string,
|
|
39
40
|
options: RunLifecycleOptions = {},
|
|
40
41
|
): Promise<RunExecution> {
|
|
42
|
+
// Consume and close the agent-only descriptor before authored source is
|
|
43
|
+
// imported by trigger preflight. Authored code never receives this object.
|
|
44
|
+
const agentEnvironment = localAgentEnvironment();
|
|
41
45
|
let input: unknown;
|
|
42
46
|
try {
|
|
43
47
|
input = parseDirectInput(inputArgument);
|
|
@@ -78,13 +82,14 @@ export async function runDirectFlow(
|
|
|
78
82
|
if (options.localAgent) {
|
|
79
83
|
localAgent = await attachLocalAgent(
|
|
80
84
|
client, dataDir, options.onPtyReady, authoredLocalAgentStream(admissionIdentity), workerCapacity,
|
|
85
|
+
undefined, [], agentEnvironment,
|
|
81
86
|
);
|
|
82
87
|
// A session owns one worker registration. Keep the workspace-free LLM
|
|
83
88
|
// worker on its own connection so it cannot replace the agent worker.
|
|
84
89
|
llmClient = new JournalClient(socketPath);
|
|
85
90
|
await llmClient.connect();
|
|
86
91
|
await llmClient.hello('flows-local-llm');
|
|
87
|
-
localLlm = new LlmWorker(llmClient, `${localAgent.stream}-llm`, workerCapacity);
|
|
92
|
+
localLlm = new LlmWorker(llmClient, `${localAgent.stream}-llm`, workerCapacity, agentEnvironment);
|
|
88
93
|
localLlm.on('error', onWorkerFailure('local-llm', error => { llmFailure = error; client.close(); }));
|
|
89
94
|
await localLlm.attach();
|
|
90
95
|
}
|
|
@@ -96,6 +101,7 @@ export async function runDirectFlow(
|
|
|
96
101
|
onAdmitted: runId => { base.rootRunId = runId; },
|
|
97
102
|
localAgentStream: localAgent?.stream,
|
|
98
103
|
...(localAgent === undefined ? {} : { workerCapacity }),
|
|
104
|
+
...(agentEnvironment === undefined ? {} : { agentEnvironment }),
|
|
99
105
|
lifecycle: {
|
|
100
106
|
onProgress: options.onProgress,
|
|
101
107
|
...(options.onRunStarted !== undefined ? { onRunStarted: options.onRunStarted } : {}),
|
|
@@ -206,6 +212,9 @@ export async function runDirectFlow(
|
|
|
206
212
|
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
207
213
|
return authoredStepFailure('run', base, socketPath, error);
|
|
208
214
|
}
|
|
215
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'root_lease_lost') {
|
|
216
|
+
return rootLeaseLostReport('run', base, socketPath, error);
|
|
217
|
+
}
|
|
209
218
|
if (error instanceof AuthoredHumanParked) {
|
|
210
219
|
return authoredHumanParked('run', base, socketPath, error, { dataDir, localAgent: options.localAgent === true });
|
|
211
220
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
rmSync,
|
|
4
|
+
symlinkSync,
|
|
5
|
+
} from 'node:fs';
|
|
6
|
+
import {
|
|
7
|
+
mkdtemp,
|
|
8
|
+
rm,
|
|
9
|
+
symlink,
|
|
10
|
+
} from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { basename, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export interface PinnedCliAlias {
|
|
15
|
+
executable: string;
|
|
16
|
+
release(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PinnedCliAliasSync {
|
|
20
|
+
executable: string;
|
|
21
|
+
release(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function aliasName(identity: string): string {
|
|
25
|
+
const name = basename(identity);
|
|
26
|
+
if (name.length === 0 || name === '.' || name === '..') {
|
|
27
|
+
throw new Error(`Invalid CLI identity basename: ${JSON.stringify(identity)}`);
|
|
28
|
+
}
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Execute pinned canonical bytes through the proved basename, including shebang scripts. */
|
|
33
|
+
export async function pinCliAlias(executable: string, identity: string): Promise<PinnedCliAlias> {
|
|
34
|
+
const name = aliasName(identity);
|
|
35
|
+
if (basename(executable) === name) {
|
|
36
|
+
return { executable, release() {} };
|
|
37
|
+
}
|
|
38
|
+
const directory = await mkdtemp(join(tmpdir(), 'relayflow-cli-'));
|
|
39
|
+
const alias = join(directory, name);
|
|
40
|
+
try {
|
|
41
|
+
await symlink(executable, alias, 'file');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await rm(directory, { recursive: true, force: true });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
let released = false;
|
|
47
|
+
return { executable: alias, release: () => {
|
|
48
|
+
if (released) return;
|
|
49
|
+
rmSync(directory, { recursive: true, force: true });
|
|
50
|
+
released = true;
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Synchronous counterpart for the synchronous preflight contract. */
|
|
55
|
+
export function pinCliAliasSync(executable: string, identity: string): PinnedCliAliasSync {
|
|
56
|
+
const name = aliasName(identity);
|
|
57
|
+
if (basename(executable) === name) {
|
|
58
|
+
return { executable, release() {} };
|
|
59
|
+
}
|
|
60
|
+
const directory = mkdtempSync(join(tmpdir(), 'relayflow-cli-'));
|
|
61
|
+
const alias = join(directory, name);
|
|
62
|
+
try {
|
|
63
|
+
symlinkSync(executable, alias, 'file');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
rmSync(directory, { recursive: true, force: true });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
let released = false;
|
|
69
|
+
return { executable: alias, release: () => {
|
|
70
|
+
if (released) return;
|
|
71
|
+
rmSync(directory, { recursive: true, force: true });
|
|
72
|
+
released = true;
|
|
73
|
+
} };
|
|
74
|
+
}
|