@relayflows/sdk 2.0.11 → 2.0.13
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/adapters/base.d.ts +6 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +1 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/authored-admission.d.ts +5 -0
- package/dist/authored-admission.d.ts.map +1 -0
- package/dist/authored-admission.js +14 -0
- package/dist/authored-admission.js.map +1 -0
- package/dist/authored-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +18 -9
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +10 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +15 -7
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +20 -0
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +86 -11
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.d.ts +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +5 -3
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +7 -4
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.d.ts +2 -0
- package/dist/authored-node-entry.d.ts.map +1 -0
- package/dist/authored-node-entry.js +111 -0
- package/dist/authored-node-entry.js.map +1 -0
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -0
- package/dist/authored-node-runner.js +246 -0
- package/dist/authored-node-runner.js.map +1 -0
- package/dist/authored-plugin-effect.d.ts +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +7 -4
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-root.d.ts +38 -0
- package/dist/authored-root.d.ts.map +1 -0
- package/dist/authored-root.js +329 -0
- package/dist/authored-root.js.map +1 -0
- package/dist/authored-runtime-capability.d.ts +6 -0
- package/dist/authored-runtime-capability.d.ts.map +1 -0
- package/dist/authored-runtime-capability.js +40 -0
- package/dist/authored-runtime-capability.js.map +1 -0
- package/dist/authored-source-authority.d.ts +4 -0
- package/dist/authored-source-authority.d.ts.map +1 -0
- package/dist/authored-source-authority.js +61 -0
- package/dist/authored-source-authority.js.map +1 -0
- package/dist/authored-worker-step.d.ts +2 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +11 -6
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts +28 -3
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +34 -24
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +8 -0
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check.js +3 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +5 -0
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +46 -14
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +90 -12
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts +40 -0
- package/dist/cli/step-failure.d.ts.map +1 -0
- package/dist/cli/step-failure.js +150 -0
- package/dist/cli/step-failure.js.map +1 -0
- package/dist/cli-adapter.d.ts +16 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +17 -0
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -8
- package/dist/cli.js.map +1 -1
- package/dist/cloud-run.d.ts +11 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +65 -19
- package/dist/cloud-run.js.map +1 -1
- package/dist/daemon-connection.d.ts +7 -0
- package/dist/daemon-connection.d.ts.map +1 -1
- package/dist/daemon-connection.js +7 -0
- package/dist/daemon-connection.js.map +1 -1
- package/dist/failure-kinds.d.ts +30 -4
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +8 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +3 -7
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +6 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -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 +2 -2
- package/dist/local-agent.js.map +1 -1
- package/dist/model-pricing.d.ts +7 -6
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +8 -6
- package/dist/model-pricing.js.map +1 -1
- package/dist/preflight.d.ts +9 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +36 -18
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +24 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +27 -11
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +6 -5
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts +13 -8
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +27 -4
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -2
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/claude.ts +1 -0
- package/src/authored-admission.ts +17 -0
- package/src/authored-budget.ts +38 -11
- package/src/authored-flow-error.ts +0 -1
- package/src/authored-flow-executor.ts +32 -7
- package/src/authored-flow-loader.ts +96 -13
- package/src/authored-helper-effect.ts +5 -3
- package/src/authored-mcp.ts +6 -4
- package/src/authored-node-entry.ts +90 -0
- package/src/authored-node-runner.ts +219 -0
- package/src/authored-plugin-effect.ts +6 -3
- package/src/authored-root.ts +396 -0
- package/src/authored-runtime-capability.ts +40 -0
- package/src/authored-source-authority.ts +62 -0
- package/src/authored-worker-step.ts +15 -4
- package/src/budget-preflight.ts +52 -25
- package/src/cli/build.ts +7 -0
- package/src/cli/check.ts +3 -1
- package/src/cli/deploy.ts +4 -0
- package/src/cli/direct-run.ts +52 -16
- package/src/cli/run.ts +101 -12
- package/src/cli/step-failure.ts +160 -0
- package/src/cli-adapter.ts +27 -0
- package/src/cli.ts +9 -9
- package/src/cloud-run.ts +74 -21
- package/src/daemon-connection.ts +8 -0
- package/src/failure-kinds.ts +28 -2
- package/src/journal-client.ts +8 -3
- package/src/llm-worker.ts +4 -2
- package/src/local-agent.ts +7 -2
- package/src/model-pricing.ts +8 -6
- package/src/preflight.ts +45 -18
- package/src/protocol.ts +17 -2
- package/src/spec.ts +28 -5
- package/src/worker-cli.ts +7 -5
- package/src/worker-spend.ts +32 -5
- package/src/worker.ts +4 -2
- package/dist/cli/deterministic-failure.d.ts +0 -5
- package/dist/cli/deterministic-failure.d.ts.map +0 -1
- package/dist/cli/deterministic-failure.js +0 -66
- package/dist/cli/deterministic-failure.js.map +0 -1
- package/src/cli/deterministic-failure.ts +0 -69
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { stripTypeScriptTypes, register } from 'node:module';
|
|
2
|
+
import { createHook } from 'node:async_hooks';
|
|
3
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
4
|
+
|
|
5
|
+
/** A runtime with no-op promise hooks cannot distinguish await from ignored then. */
|
|
6
|
+
export function assertAuthoredPromiseHooks(): void {
|
|
7
|
+
let initialized = false;
|
|
8
|
+
let resolved = false;
|
|
9
|
+
const hook = createHook({
|
|
10
|
+
init(_id, type) { if (type === 'PROMISE') initialized = true; },
|
|
11
|
+
promiseResolve() { resolved = true; },
|
|
12
|
+
});
|
|
13
|
+
try {
|
|
14
|
+
hook.enable();
|
|
15
|
+
void new Promise<void>((resolve) => resolve());
|
|
16
|
+
} finally { hook.disable(); }
|
|
17
|
+
if (!initialized || !resolved) throw new AuthoredFlowExecutionError(
|
|
18
|
+
'unsupported_promise_lifecycle',
|
|
19
|
+
'authored execution requires working Node promise hooks; use the standalone Node-authored runner with Node >=22.14',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function assertAuthoredNodeVersion(version = process.versions.node): void {
|
|
24
|
+
const [major, minor] = version.split('.').map(Number);
|
|
25
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor)
|
|
26
|
+
|| major! < 22 || (major === 22 && minor! < 14)
|
|
27
|
+
|| process.versions['bun'] !== undefined
|
|
28
|
+
|| typeof stripTypeScriptTypes !== 'function' || typeof register !== 'function') {
|
|
29
|
+
throw new AuthoredFlowExecutionError('unsupported_promise_lifecycle',
|
|
30
|
+
'the authored runner requires Node >=22.14; no workflow body was executed');
|
|
31
|
+
}
|
|
32
|
+
assertAuthoredPromiseHooks();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** PID 1 is valid when the standalone CLI is a container's init process. */
|
|
36
|
+
export function parseAuthoredParentPid(value: string | undefined): number {
|
|
37
|
+
const pid = Number(value);
|
|
38
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('invalid authored parent identity');
|
|
39
|
+
return pid;
|
|
40
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { register } from 'node:module';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { canonicalize } from './canonical.js';
|
|
6
|
+
import { loadAuthoredFlow } from './authored-flow-loader.js';
|
|
7
|
+
import type { AuthoredRootMetadata } from './authored-root.js';
|
|
8
|
+
const sha256 = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
|
|
10
|
+
/** Re-open the exact source graph and Surface instance pinned by the durable root. */
|
|
11
|
+
export async function loadPinnedAuthoredSource(metadata: AuthoredRootMetadata, immutableNodeImport = false) {
|
|
12
|
+
const source = await readFile(metadata.flowPath);
|
|
13
|
+
if (sha256(source) !== metadata.sourceSha256) {
|
|
14
|
+
throw new Error('authored root source authority mismatch');
|
|
15
|
+
}
|
|
16
|
+
const sources = new Map([[pathToFileURL(metadata.flowPath).href, source.toString('utf8')]]);
|
|
17
|
+
for (const pinned of metadata.sources) {
|
|
18
|
+
const bytes = await readFile(pinned.path);
|
|
19
|
+
if (sha256(bytes) !== pinned.sourceSha256) {
|
|
20
|
+
throw new Error(`authored root source authority mismatch for "${pinned.path}"`);
|
|
21
|
+
}
|
|
22
|
+
sources.set(pathToFileURL(pinned.path).href, bytes.toString('utf8'));
|
|
23
|
+
}
|
|
24
|
+
if (immutableNodeImport) installPinnedSourceLoader(sources);
|
|
25
|
+
const loaded = await loadAuthoredFlow(metadata.flowPath);
|
|
26
|
+
if (canonicalize(loaded.surfaceAuthority) !== canonicalize(metadata.surface)
|
|
27
|
+
|| loaded.getDefinition(loaded.handle).name !== metadata.flowName) {
|
|
28
|
+
throw new Error('authored root Surface module authority mismatch');
|
|
29
|
+
}
|
|
30
|
+
const loadedSources = loaded.graph.map(node => ({
|
|
31
|
+
path: node.path,
|
|
32
|
+
sourceSha256: metadata.sources.find(source => source.path === node.path)?.sourceSha256 ?? '',
|
|
33
|
+
surface: node.surfaceAuthority,
|
|
34
|
+
}));
|
|
35
|
+
if (canonicalize(loadedSources) !== canonicalize(metadata.sources)) {
|
|
36
|
+
throw new Error('authored root declared source graph authority mismatch');
|
|
37
|
+
}
|
|
38
|
+
return loaded;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let pinnedSourceLoaderInstalled = false;
|
|
42
|
+
|
|
43
|
+
/** Isolated Node child only: preserve original URLs/resolution, load verified bytes. */
|
|
44
|
+
function installPinnedSourceLoader(sources: Map<string, string>): void {
|
|
45
|
+
if (pinnedSourceLoaderInstalled) throw new Error('authored Node process already owns a source graph');
|
|
46
|
+
pinnedSourceLoaderInstalled = true;
|
|
47
|
+
// Node >=22.14 supports register and stripTypeScriptTypes. A loader thread
|
|
48
|
+
// receives the captured bytes, never reopening mutable authored paths.
|
|
49
|
+
const loader = `
|
|
50
|
+
import {stripTypeScriptTypes} from 'node:module';
|
|
51
|
+
let sources;
|
|
52
|
+
export function initialize(data){sources=new Map(data);}
|
|
53
|
+
export async function load(url,context,next){
|
|
54
|
+
if(sources.has(url))return {format:'module',shortCircuit:true,
|
|
55
|
+
source:stripTypeScriptTypes(sources.get(url),{mode:'transform',sourceUrl:url})};
|
|
56
|
+
if(url.startsWith('file:') && new URL(url).pathname.endsWith('.flow.ts'))
|
|
57
|
+
throw new Error('authored root declared source graph authority mismatch');
|
|
58
|
+
return next(url,context);
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
register('data:text/javascript,' + encodeURIComponent(loader), { data: [...sources] });
|
|
62
|
+
}
|
|
@@ -11,6 +11,7 @@ import { SPEC_SCHEMA_VERSION, type FlowSpec, type StepSpec } from './spec.js';
|
|
|
11
11
|
import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput } from './authored-step-output.js';
|
|
12
12
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
13
13
|
import { snapshotJsonValue } from './json-value.js';
|
|
14
|
+
import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
14
15
|
|
|
15
16
|
const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
|
|
16
17
|
|
|
@@ -19,6 +20,7 @@ export function authoredWorkerRunner(
|
|
|
19
20
|
definition: { name: string }, journal: JournalClient, flowPath: string,
|
|
20
21
|
journalSteps: AuthoredFlowJournalStep[], waitOptions: RunLifecycleOptions,
|
|
21
22
|
localAgentStream?: string, budget?: AuthoredBudget, headerBudget?: unknown,
|
|
23
|
+
rootRunId?: string,
|
|
22
24
|
) {
|
|
23
25
|
async function run(step: StepSpec): Promise<unknown> {
|
|
24
26
|
const id = step.id;
|
|
@@ -35,7 +37,7 @@ export function authoredWorkerRunner(
|
|
|
35
37
|
diagnostic.severity === 'refusal',
|
|
36
38
|
);
|
|
37
39
|
throw new AuthoredFlowExecutionError(
|
|
38
|
-
refusal?.kind === '
|
|
40
|
+
refusal?.kind === 'budget_syntax_invalid' ? refusal.kind
|
|
39
41
|
: step.type === 'llm' ? 'llm_cli_unresolved' : 'agent_cli_unresolved',
|
|
40
42
|
refusal?.message
|
|
41
43
|
?? `flow "${definition.name}" step "${id}": no CLI could be resolved for f.${step.type} `
|
|
@@ -79,7 +81,10 @@ export function authoredWorkerRunner(
|
|
|
79
81
|
}
|
|
80
82
|
return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps);
|
|
81
83
|
};
|
|
82
|
-
|
|
84
|
+
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
85
|
+
return budget === undefined
|
|
86
|
+
? consume(await journal.runStart(spec, undefined, admissionKey))
|
|
87
|
+
: budget.execute(journal, spec, consume, admissionKey);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
return {
|
|
@@ -164,6 +169,7 @@ export function authoredWorkerRunner(
|
|
|
164
169
|
/** Deterministic commands execute inline under their per-invocation lease. */
|
|
165
170
|
export function authoredDeterministicRunner(
|
|
166
171
|
name: string, journal: JournalClient, journalSteps: AuthoredFlowJournalStep[], budget: AuthoredBudget,
|
|
172
|
+
rootRunId?: string,
|
|
167
173
|
) {
|
|
168
174
|
return async (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate): Promise<string> => {
|
|
169
175
|
const spec = toKernelSpec(compileSpec({
|
|
@@ -175,7 +181,12 @@ export function authoredDeterministicRunner(
|
|
|
175
181
|
...(verification === undefined ? {} : { verification }),
|
|
176
182
|
}],
|
|
177
183
|
}));
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
185
|
+
if (terminal) return readSuccessfulOutput(
|
|
186
|
+
journal, await journal.runStart(spec, undefined, admissionKey), id, journalSteps,
|
|
187
|
+
);
|
|
188
|
+
return budget.execute(
|
|
189
|
+
journal, spec, outcome => readSuccessfulOutput(journal, outcome, id, journalSteps), admissionKey,
|
|
190
|
+
);
|
|
180
191
|
};
|
|
181
192
|
}
|
package/src/budget-preflight.ts
CHANGED
|
@@ -1,31 +1,58 @@
|
|
|
1
1
|
import type { CompiledFlowSpec } from './compile.js';
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { PreflightWarning } from './preflight.js';
|
|
3
|
+
import { hasPricing } from './model-pricing.js';
|
|
4
|
+
import { resolveAdapterKind } from './adapters/index.js';
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The resolved CLI/model pair budget pricing needs, deliberately narrower than
|
|
8
|
+
* `ResolvedCliModel`: pricing depends only on *which* model will run (priced or
|
|
9
|
+
* not) and on the CLI for the Codex wording — not on whether the model came
|
|
10
|
+
* from authoring or an adapter default, which `ResolvedCliModel.source` records
|
|
11
|
+
* for `flows check` reporting. Keeping `source` out means a future resolution
|
|
12
|
+
* rung cannot change pricing by provenance alone.
|
|
13
|
+
*/
|
|
14
|
+
export interface BudgetStepResolution {
|
|
15
|
+
readonly cli?: string;
|
|
16
|
+
readonly model?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A missing price never refuses a run. Under a frozen dollar budget:
|
|
21
|
+
* - a priced step journals exact dollars and a crossed `maxDollars` stops the
|
|
22
|
+
* run in the kernel;
|
|
23
|
+
* - an unmetered step (no model, or no frozen price) runs and journals its
|
|
24
|
+
* tokens with `dollars_unmetered: true` (see `workerSpend`), so its unknown
|
|
25
|
+
* cost cannot cross `maxDollars` while its tokens still count toward token
|
|
26
|
+
* ceilings.
|
|
27
|
+
* This warning names each unmetered step so the gap is reported, not silent.
|
|
28
|
+
* Preflight `ok` stays true: warnings never refuse.
|
|
29
|
+
*
|
|
30
|
+
* Codex selects its own model when none is declared, so a Codex step is
|
|
31
|
+
* expected to be unmetered and says so rather than asking for a fake price.
|
|
32
|
+
*/
|
|
33
|
+
export function budgetDiagnostics(
|
|
34
|
+
flow: CompiledFlowSpec,
|
|
35
|
+
resolved: ReadonlyMap<string, BudgetStepResolution> = new Map(),
|
|
36
|
+
): PreflightWarning[] {
|
|
37
|
+
if (flow.budget?.pricing !== 'frozen' || flow.budget.maxDollars === undefined) return [];
|
|
38
|
+
const warnings: PreflightWarning[] = [];
|
|
14
39
|
for (const step of flow.steps) {
|
|
15
|
-
if (step.type === 'deterministic'
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
if (step.type === 'deterministic') continue;
|
|
41
|
+
const resolution = resolved.get(step.id);
|
|
42
|
+
const model = resolution?.model
|
|
43
|
+
?? step.model ?? (step.type === 'agent' && step.agent !== undefined
|
|
44
|
+
? flow.agents?.[step.agent]?.model : undefined);
|
|
45
|
+
if (hasPricing(model)) continue;
|
|
46
|
+
const cli = resolution?.cli ?? step.cli;
|
|
47
|
+
const reason = model !== undefined
|
|
48
|
+
? `model "${model}" has no frozen price`
|
|
49
|
+
: cli !== undefined && resolveAdapterKind(cli) === 'codex'
|
|
50
|
+
? 'Codex selects its own model'
|
|
51
|
+
: 'no model is declared';
|
|
52
|
+
warnings.push({
|
|
53
|
+
severity: 'warning', kind: 'budget_unmetered', stepId: step.id,
|
|
54
|
+
message: `Step "${step.id}" is unmetered (${reason}); it does not count toward the dollar budget.`,
|
|
28
55
|
});
|
|
29
56
|
}
|
|
30
|
-
return
|
|
57
|
+
return warnings;
|
|
31
58
|
}
|
package/src/cli/build.ts
CHANGED
|
@@ -62,6 +62,13 @@ export async function runBuild(args: BuildArgs, io: CliIo): Promise<0 | 2> {
|
|
|
62
62
|
emitBuildCheckReport(gate.report, args.json, io);
|
|
63
63
|
return 2;
|
|
64
64
|
}
|
|
65
|
+
// `ok` means "no refusal", not "no diagnostics". Build defers environment
|
|
66
|
+
// probes, so probe-shaped warnings (`command_unprovable`, ...) are expected
|
|
67
|
+
// here and stay in preflight.json; `budget_unmetered` does not depend on a
|
|
68
|
+
// probe and changes what the dollar budget enforces, so it is printed.
|
|
69
|
+
for (const diagnostic of gate.report.diagnostics) {
|
|
70
|
+
if (diagnostic.kind === 'budget_unmetered') io.stderr(`WARNING [${diagnostic.kind}] ${diagnostic.message}`);
|
|
71
|
+
}
|
|
65
72
|
io.stdout(await buildFlow(args.value, args.out ?? 'dist/flows', io.stderr));
|
|
66
73
|
return 0;
|
|
67
74
|
} catch (error) {
|
package/src/cli/check.ts
CHANGED
|
@@ -358,7 +358,9 @@ function bindResolvedCliPaths(
|
|
|
358
358
|
const resolution = byStep.get(step.id);
|
|
359
359
|
if (resolution === undefined) return step;
|
|
360
360
|
const directory = resolution.source === 'project' ? configDirectory : flowDirectory;
|
|
361
|
-
return { ...step, cli: canonicalCli(resolution.cli, directory)
|
|
361
|
+
return { ...step, cli: canonicalCli(resolution.cli, directory),
|
|
362
|
+
...(resolution.modelSource === 'adapter' && resolution.model !== undefined
|
|
363
|
+
? { model: resolution.model } : {}) };
|
|
362
364
|
}),
|
|
363
365
|
};
|
|
364
366
|
}
|
package/src/cli/deploy.ts
CHANGED
|
@@ -31,6 +31,10 @@ export async function runDeploy(args: DeployArgs, io: CliIo): Promise<0 | 1 | 2>
|
|
|
31
31
|
for (const diagnostic of checked.report.diagnostics) io.stderr(`${diagnostic.severity.toUpperCase()} [${diagnostic.kind}] ${diagnostic.message}`);
|
|
32
32
|
return 2;
|
|
33
33
|
}
|
|
34
|
+
// `ok` may still carry warnings (e.g. `budget_unmetered`); report them.
|
|
35
|
+
for (const diagnostic of checked.report.diagnostics) {
|
|
36
|
+
if (diagnostic.severity === 'warning') io.stderr(`WARNING [${diagnostic.kind}] ${diagnostic.message}`);
|
|
37
|
+
}
|
|
34
38
|
const target = bucketDirectory(args.to, ref);
|
|
35
39
|
if (await exists(target)) {
|
|
36
40
|
await verifyDigest(target, ref.digest);
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { McpStepError } from '../authored-mcp.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { authoredLocalAgentStream } from '../authored-admission.js';
|
|
2
4
|
import { McpPreflightError } from './check-typescript.js';
|
|
3
5
|
import { attachLocalAgent } from '../local-agent.js';
|
|
4
6
|
import { LlmWorker } from '../llm-worker.js';
|
|
5
7
|
import {
|
|
6
8
|
AuthoredFlowExecutionError,
|
|
7
|
-
executeAuthoredFlow,
|
|
8
9
|
} from '../authored-flow-executor.js';
|
|
10
|
+
import { executeDurableAuthoredFlow } from '../authored-root.js';
|
|
9
11
|
import { AuthoredFlowLoadError } from '../authored-flow-loader.js';
|
|
10
12
|
import { DirectInputError, parseDirectInput } from '../direct-input.js';
|
|
11
13
|
import { JournalClient } from '../journal-client.js';
|
|
@@ -61,10 +63,13 @@ export async function runDirectFlow(
|
|
|
61
63
|
let localLlm: LlmWorker | undefined;
|
|
62
64
|
let llmClient: JournalClient | undefined;
|
|
63
65
|
let llmFailure: unknown;
|
|
66
|
+
const admissionIdentity = options.authoredAdmissionKey
|
|
67
|
+
?? process.env['RELAYFLOW_AUTHORED_ADMISSION_KEY'] ?? randomUUID();
|
|
64
68
|
try {
|
|
65
|
-
const { handle, getDefinition } = checked.loaded;
|
|
66
69
|
if (options.localAgent) {
|
|
67
|
-
localAgent = await attachLocalAgent(
|
|
70
|
+
localAgent = await attachLocalAgent(
|
|
71
|
+
client, dataDir, options.onPtyReady, authoredLocalAgentStream(admissionIdentity),
|
|
72
|
+
);
|
|
68
73
|
// A session owns one worker registration. Keep the workspace-free LLM
|
|
69
74
|
// worker on its own connection so it cannot replace the agent worker.
|
|
70
75
|
llmClient = new JournalClient(socketPath);
|
|
@@ -74,15 +79,19 @@ export async function runDirectFlow(
|
|
|
74
79
|
localLlm.on('error', error => { llmFailure = error; client.close(); });
|
|
75
80
|
await localLlm.attach();
|
|
76
81
|
}
|
|
77
|
-
const result = await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
const result = await executeDurableAuthoredFlow(
|
|
83
|
+
checked.loaded, client, input,
|
|
84
|
+
{
|
|
85
|
+
dataDir,
|
|
86
|
+
admissionKey: admissionIdentity,
|
|
87
|
+
localAgentStream: localAgent?.stream,
|
|
88
|
+
lifecycle: {
|
|
89
|
+
onProgress: options.onProgress,
|
|
90
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
91
|
+
...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
);
|
|
86
95
|
const terminal = result.journalSteps.at(-1);
|
|
87
96
|
if (terminal === undefined) {
|
|
88
97
|
return protocolFailure('run', base, socketPath, new Error(
|
|
@@ -93,7 +102,7 @@ export async function runDirectFlow(
|
|
|
93
102
|
return {
|
|
94
103
|
exitCode: 3,
|
|
95
104
|
report: {
|
|
96
|
-
...base, ok: false, runId:
|
|
105
|
+
...base, ok: false, runId: result.rootRunId, socketPath, status: 'parked',
|
|
97
106
|
completedSteps: result.journalSteps.length,
|
|
98
107
|
diagnostics: [...base.diagnostics, {
|
|
99
108
|
severity: 'parked', kind: 'run_parked',
|
|
@@ -107,7 +116,7 @@ export async function runDirectFlow(
|
|
|
107
116
|
report: {
|
|
108
117
|
...base,
|
|
109
118
|
ok: true,
|
|
110
|
-
runId:
|
|
119
|
+
runId: result.rootRunId,
|
|
111
120
|
socketPath,
|
|
112
121
|
status: 'completed',
|
|
113
122
|
completionReason: result.completionReason,
|
|
@@ -144,7 +153,7 @@ export async function runDirectFlow(
|
|
|
144
153
|
&& (error.code === 'helper_slack.credential_missing'
|
|
145
154
|
|| error.code === 'helper_slack.mount_required'
|
|
146
155
|
|| error.code === 'budget_syntax_invalid'
|
|
147
|
-
|| error.code === '
|
|
156
|
+
|| error.code === 'unsupported_promise_lifecycle'
|
|
148
157
|
|| error.code === 'unsupported_header'
|
|
149
158
|
|| error.code === 'agent_cli_unresolved'
|
|
150
159
|
|| error.code === 'llm_cli_unresolved'
|
|
@@ -157,7 +166,6 @@ export async function runDirectFlow(
|
|
|
157
166
|
error.code === 'helper_slack.credential_missing'
|
|
158
167
|
|| error.code === 'helper_slack.mount_required'
|
|
159
168
|
|| error.code === 'budget_syntax_invalid'
|
|
160
|
-
|| error.code === 'budget_missing_price'
|
|
161
169
|
) ? error.code : 'invalid_spec',
|
|
162
170
|
message: error.message,
|
|
163
171
|
}, path)),
|
|
@@ -182,6 +190,34 @@ export async function runDirectFlow(
|
|
|
182
190
|
},
|
|
183
191
|
};
|
|
184
192
|
}
|
|
193
|
+
// A step that ran and failed is a run failure, not a protocol failure.
|
|
194
|
+
// Routing it through `protocolFailure` reported `relayflowd could not
|
|
195
|
+
// complete the run request` — which says the daemon broke — and left the
|
|
196
|
+
// report with no `status`, so the summary line printed `RUN <id> unknown`
|
|
197
|
+
// about a run whose outcome was known exactly. The diagnostic carried up
|
|
198
|
+
// from `classifyOutcome` already names the step, its exit code and its
|
|
199
|
+
// output tail; this branch is what lets it reach the terminal. Mirrors
|
|
200
|
+
// the `McpStepError` branch above, which had this shape all along.
|
|
201
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
|
|
202
|
+
return {
|
|
203
|
+
exitCode: 1,
|
|
204
|
+
report: {
|
|
205
|
+
...base,
|
|
206
|
+
ok: false,
|
|
207
|
+
runId: error.runId,
|
|
208
|
+
socketPath,
|
|
209
|
+
status: 'failed',
|
|
210
|
+
completionReason: 'step_failed',
|
|
211
|
+
diagnostics: [...base.diagnostics, {
|
|
212
|
+
severity: 'failure',
|
|
213
|
+
kind: 'step_failed',
|
|
214
|
+
// The `code: ` prefix `AuthoredFlowExecutionError` adds is
|
|
215
|
+
// redundant once the diagnostic is labelled `[step_failed]`.
|
|
216
|
+
message: error.message.replace(/^step_failed: /, ''),
|
|
217
|
+
}],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
185
221
|
const runId = error instanceof AuthoredFlowExecutionError ? error.runId : undefined;
|
|
186
222
|
return protocolFailure('run', base, socketPath, error, runId);
|
|
187
223
|
} finally {
|
package/src/cli/run.ts
CHANGED
|
@@ -11,9 +11,11 @@ import { ensureDaemon, type EnsureDaemonOptions } from '../daemon-lifecycle.js';
|
|
|
11
11
|
import { isAuthoredFlowPath } from '../direct-input.js';
|
|
12
12
|
import { daemonRefusal } from './daemon-refusal.js';
|
|
13
13
|
import type { RunFailureKind, RunWarningKind, StepFailedDetails } from '../failure-kinds.js';
|
|
14
|
-
import {
|
|
14
|
+
import { inspectionHint, stepFailureDetails } from './step-failure.js';
|
|
15
15
|
import { JournalClient, JournalProtocolError } from '../journal-client.js';
|
|
16
16
|
import { attachLocalAgent } from '../local-agent.js';
|
|
17
|
+
import { LlmWorker } from '../llm-worker.js';
|
|
18
|
+
import { readAuthoredRootMetadata, resumeDurableAuthoredFlow } from '../authored-root.js';
|
|
17
19
|
import type {
|
|
18
20
|
RunCompletionReason,
|
|
19
21
|
RunOutcome,
|
|
@@ -68,6 +70,8 @@ export interface RunProgress {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export interface RunLifecycleOptions {
|
|
73
|
+
/** Caller-owned hosted invocation identity for authored-root start recovery. */
|
|
74
|
+
authoredAdmissionKey?: string;
|
|
71
75
|
bucket?: string;
|
|
72
76
|
allowHumanInfluenced?: boolean;
|
|
73
77
|
onPtyReady?: (path: string) => void;
|
|
@@ -76,6 +80,14 @@ export interface RunLifecycleOptions {
|
|
|
76
80
|
localAgent?: boolean;
|
|
77
81
|
signal?: AbortSignal;
|
|
78
82
|
onWait?: (progress: RunProgress) => void;
|
|
83
|
+
/**
|
|
84
|
+
* The daemon data dir, carried so a failure diagnostic can name the journal
|
|
85
|
+
* holding the evidence (`<dataDir>/runs/<runId>.sqlite3`) and emit a
|
|
86
|
+
* `flows replay` invocation that will actually resolve. Each verb sets it
|
|
87
|
+
* from its own `--data-dir`; absent only where no data dir exists, and the
|
|
88
|
+
* diagnostic then omits the path rather than guessing one.
|
|
89
|
+
*/
|
|
90
|
+
dataDir?: string;
|
|
79
91
|
/**
|
|
80
92
|
* Attach-or-spawn policy for the daemon this command needs
|
|
81
93
|
* (kernel/DAEMON-LIFECYCLE.md §3). `{ spawn: false }` is `--no-spawn`:
|
|
@@ -119,7 +131,7 @@ async function executeCheckedFlow(
|
|
|
119
131
|
// advertises its existing pins; the daemon still owns surface matching.
|
|
120
132
|
if (options.localAgent) localAgent = await attachLocalAgent(client, dataDir, options.onPtyReady);
|
|
121
133
|
const outcome = await client.runStart(spec, options.reuseFromRunId);
|
|
122
|
-
const execution = await classifyOutcome(client, 'run', outcome, base, socketPath, options);
|
|
134
|
+
const execution = await classifyOutcome(client, 'run', outcome, base, socketPath, { ...options, dataDir });
|
|
123
135
|
if (options.reuseFromRunId !== undefined) {
|
|
124
136
|
execution.report.reuse = await reuseSummary(client, outcome.run_id, options.reuseFromRunId);
|
|
125
137
|
}
|
|
@@ -151,7 +163,50 @@ export async function resumeFlow(
|
|
|
151
163
|
const connected = await connect(client, 'resume', dataDir, base, options);
|
|
152
164
|
if (connected !== undefined) return connected;
|
|
153
165
|
|
|
166
|
+
let authoredAgent: Awaited<ReturnType<typeof attachLocalAgent>> | undefined;
|
|
167
|
+
let authoredLlm: LlmWorker | undefined;
|
|
168
|
+
let authoredLlmClient: JournalClient | undefined;
|
|
154
169
|
try {
|
|
170
|
+
const authoredRoot = await readAuthoredRootMetadata(client, runId);
|
|
171
|
+
if (authoredRoot !== undefined) {
|
|
172
|
+
if (authoredRoot.localAgentStream !== undefined && !options.localAgent) {
|
|
173
|
+
throw new Error('authored root requires --local-agent to resume its pinned worker surface');
|
|
174
|
+
}
|
|
175
|
+
if (authoredRoot.localAgentStream === undefined && options.localAgent) {
|
|
176
|
+
throw new Error('authored root was started without a local agent worker surface');
|
|
177
|
+
}
|
|
178
|
+
if (options.localAgent) {
|
|
179
|
+
authoredAgent = await attachLocalAgent(
|
|
180
|
+
client, dataDir, options.onPtyReady, authoredRoot.localAgentStream,
|
|
181
|
+
);
|
|
182
|
+
authoredLlmClient = new JournalClient(socketPath);
|
|
183
|
+
await authoredLlmClient.connect();
|
|
184
|
+
await authoredLlmClient.hello('flows-authored-resume-llm');
|
|
185
|
+
authoredLlm = new LlmWorker(authoredLlmClient, `${authoredAgent.stream}-llm`);
|
|
186
|
+
await authoredLlm.attach();
|
|
187
|
+
}
|
|
188
|
+
const result = await resumeDurableAuthoredFlow(runId, client, {
|
|
189
|
+
dataDir,
|
|
190
|
+
localAgentStream: authoredAgent?.stream,
|
|
191
|
+
lifecycle: options,
|
|
192
|
+
});
|
|
193
|
+
if (result === undefined) throw new Error('authored root disappeared during resume');
|
|
194
|
+
return {
|
|
195
|
+
exitCode: result.completionReason === 'needs_human' ? 3 : 0,
|
|
196
|
+
report: {
|
|
197
|
+
...base,
|
|
198
|
+
ok: result.completionReason === 'success',
|
|
199
|
+
runId,
|
|
200
|
+
socketPath,
|
|
201
|
+
status: result.completionReason === 'needs_human' ? 'parked' : 'completed',
|
|
202
|
+
...(result.completionReason === 'success'
|
|
203
|
+
? { completionReason: 'success' as const }
|
|
204
|
+
: { diagnostics: [{ severity: 'parked' as const, kind: 'run_parked' as const,
|
|
205
|
+
message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.` }] }),
|
|
206
|
+
completedSteps: result.journalSteps.length,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
155
210
|
// resumeHelperEffect subsumes the old resumeSlackEffect: it handles the
|
|
156
211
|
// slack effect resume plus every other provider from N's codegen. The
|
|
157
212
|
// second call the earlier rebase left is a stale reference from before
|
|
@@ -160,12 +215,16 @@ export async function resumeFlow(
|
|
|
160
215
|
if (await resumeHelperEffect(client, runId, dataDir)) {
|
|
161
216
|
outcome = await client.runResume(runId, options.allowHumanInfluenced);
|
|
162
217
|
}
|
|
163
|
-
return await classifyOutcome(client, 'resume', outcome, base, socketPath, options);
|
|
218
|
+
return await classifyOutcome(client, 'resume', outcome, base, socketPath, { ...options, dataDir });
|
|
164
219
|
} catch (error) {
|
|
165
220
|
if (error instanceof JournalProtocolError && error.code === 'human_influenced_run') {
|
|
166
221
|
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
167
222
|
diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } };
|
|
168
223
|
}
|
|
224
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'unsupported_promise_lifecycle') {
|
|
225
|
+
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
226
|
+
diagnostics: [{ severity: 'refusal', kind: 'invalid_spec', message: error.message }] } };
|
|
227
|
+
}
|
|
169
228
|
if (error instanceof AuthoredFlowExecutionError
|
|
170
229
|
&& (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required'
|
|
171
230
|
|| error.code === 'helper_provider.mount_required' || error.code === 'helper_provider.unsupported')) {
|
|
@@ -189,7 +248,13 @@ export async function resumeFlow(
|
|
|
189
248
|
},
|
|
190
249
|
};
|
|
191
250
|
} finally {
|
|
192
|
-
|
|
251
|
+
try {
|
|
252
|
+
await authoredLlm?.close();
|
|
253
|
+
authoredLlmClient?.close();
|
|
254
|
+
await authoredAgent?.close();
|
|
255
|
+
} finally {
|
|
256
|
+
client.close();
|
|
257
|
+
}
|
|
193
258
|
}
|
|
194
259
|
}
|
|
195
260
|
|
|
@@ -352,18 +417,24 @@ export async function classifyOutcome(
|
|
|
352
417
|
message: `Run "${current.run_id}" failed with completionReason: ${current.completion_reason}.`,
|
|
353
418
|
};
|
|
354
419
|
if (current.completion_reason === 'step_failed') {
|
|
420
|
+
let details: StepFailedDetails | undefined;
|
|
355
421
|
try {
|
|
356
|
-
|
|
357
|
-
if (details !== undefined) {
|
|
358
|
-
Object.assign(diagnostic, details);
|
|
359
|
-
diagnostic.message += ` Step ${JSON.stringify(details.stepId)} exit=${details.exitCode}.`
|
|
360
|
-
+ (details.stderrTail ? `\nStderr (last 1,024 bytes):\n${details.stderrTail}` : '')
|
|
361
|
-
+ `\nInspect: ${details.hint}`;
|
|
362
|
-
}
|
|
422
|
+
details = await stepFailureDetails(client, current.run_id);
|
|
363
423
|
} catch (error) {
|
|
364
424
|
// Inspection must not erase the already known run failure.
|
|
365
|
-
diagnostic.message += ` Could not inspect
|
|
425
|
+
diagnostic.message += ` Could not inspect the failed step: ${errorMessage(error)}`;
|
|
426
|
+
}
|
|
427
|
+
if (details !== undefined) {
|
|
428
|
+
Object.assign(diagnostic, details);
|
|
429
|
+
diagnostic.message += renderStepEvidence(details);
|
|
366
430
|
}
|
|
431
|
+
// Appended whatever the inspection found — including nothing. A failure
|
|
432
|
+
// shape this reader does not recognise, or a journal it could not read,
|
|
433
|
+
// must still end with somewhere to go rather than with a dead end.
|
|
434
|
+
const where = inspectionHint(current.run_id, details?.stepId, options.dataDir);
|
|
435
|
+
Object.assign(diagnostic, where);
|
|
436
|
+
diagnostic.message += `\nInspect: ${where.hint}`
|
|
437
|
+
+ (where.journalPath === undefined ? '' : `\nJournal: ${where.journalPath}`);
|
|
367
438
|
}
|
|
368
439
|
return {
|
|
369
440
|
exitCode: 1,
|
|
@@ -552,6 +623,24 @@ function errorMessage(error: unknown): string {
|
|
|
552
623
|
return error instanceof Error ? error.message : 'unknown protocol error';
|
|
553
624
|
}
|
|
554
625
|
|
|
626
|
+
/**
|
|
627
|
+
* The evidence half of a `step_failed` diagnostic; `inspectionHint` adds the
|
|
628
|
+
* rest. Each field is printed only when the journal actually carried it — an
|
|
629
|
+
* agent step has no exit code to report, and inventing `exit=undefined` (which
|
|
630
|
+
* is what the deterministic-only version printed for one) is worse than
|
|
631
|
+
* silence on that field.
|
|
632
|
+
*/
|
|
633
|
+
function renderStepEvidence(details: StepFailedDetails): string {
|
|
634
|
+
return ` Step ${JSON.stringify(details.stepId)}`
|
|
635
|
+
+ (details.stepType === undefined ? '' : ` (${details.stepType})`)
|
|
636
|
+
+ ` completionReason: ${details.completionReason}`
|
|
637
|
+
+ (details.exitCode === undefined ? '' : ` exit=${details.exitCode}`)
|
|
638
|
+
+ '.'
|
|
639
|
+
+ (details.detail === undefined ? '' : `\nDetail: ${details.detail}`)
|
|
640
|
+
+ (details.stdoutTail ? `\nStdout (last 1,024 bytes):\n${details.stdoutTail}` : '')
|
|
641
|
+
+ (details.stderrTail ? `\nStderr (last 1,024 bytes):\n${details.stderrTail}` : '');
|
|
642
|
+
}
|
|
643
|
+
|
|
555
644
|
function throwIfCanceled(signal: AbortSignal | undefined, stepId: string): void {
|
|
556
645
|
if (signal?.aborted === true) throw new Error(`waiting for running step "${stepId}" was canceled`);
|
|
557
646
|
}
|