@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.
Files changed (180) hide show
  1. package/dist/adapters/base.d.ts +6 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +1 -0
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/authored-admission.d.ts +5 -0
  7. package/dist/authored-admission.d.ts.map +1 -0
  8. package/dist/authored-admission.js +14 -0
  9. package/dist/authored-admission.js.map +1 -0
  10. package/dist/authored-budget.d.ts +1 -1
  11. package/dist/authored-budget.d.ts.map +1 -1
  12. package/dist/authored-budget.js +18 -9
  13. package/dist/authored-budget.js.map +1 -1
  14. package/dist/authored-flow-error.d.ts +1 -1
  15. package/dist/authored-flow-error.d.ts.map +1 -1
  16. package/dist/authored-flow-error.js.map +1 -1
  17. package/dist/authored-flow-executor.d.ts +10 -0
  18. package/dist/authored-flow-executor.d.ts.map +1 -1
  19. package/dist/authored-flow-executor.js +15 -7
  20. package/dist/authored-flow-executor.js.map +1 -1
  21. package/dist/authored-flow-loader.d.ts +20 -0
  22. package/dist/authored-flow-loader.d.ts.map +1 -1
  23. package/dist/authored-flow-loader.js +86 -11
  24. package/dist/authored-flow-loader.js.map +1 -1
  25. package/dist/authored-helper-effect.d.ts +1 -1
  26. package/dist/authored-helper-effect.d.ts.map +1 -1
  27. package/dist/authored-helper-effect.js +5 -3
  28. package/dist/authored-helper-effect.js.map +1 -1
  29. package/dist/authored-mcp.d.ts +1 -1
  30. package/dist/authored-mcp.d.ts.map +1 -1
  31. package/dist/authored-mcp.js +7 -4
  32. package/dist/authored-mcp.js.map +1 -1
  33. package/dist/authored-node-entry.d.ts +2 -0
  34. package/dist/authored-node-entry.d.ts.map +1 -0
  35. package/dist/authored-node-entry.js +111 -0
  36. package/dist/authored-node-entry.js.map +1 -0
  37. package/dist/authored-node-runner.d.ts +11 -0
  38. package/dist/authored-node-runner.d.ts.map +1 -0
  39. package/dist/authored-node-runner.js +246 -0
  40. package/dist/authored-node-runner.js.map +1 -0
  41. package/dist/authored-plugin-effect.d.ts +1 -1
  42. package/dist/authored-plugin-effect.d.ts.map +1 -1
  43. package/dist/authored-plugin-effect.js +7 -4
  44. package/dist/authored-plugin-effect.js.map +1 -1
  45. package/dist/authored-root.d.ts +38 -0
  46. package/dist/authored-root.d.ts.map +1 -0
  47. package/dist/authored-root.js +329 -0
  48. package/dist/authored-root.js.map +1 -0
  49. package/dist/authored-runtime-capability.d.ts +6 -0
  50. package/dist/authored-runtime-capability.d.ts.map +1 -0
  51. package/dist/authored-runtime-capability.js +40 -0
  52. package/dist/authored-runtime-capability.js.map +1 -0
  53. package/dist/authored-source-authority.d.ts +4 -0
  54. package/dist/authored-source-authority.d.ts.map +1 -0
  55. package/dist/authored-source-authority.js +61 -0
  56. package/dist/authored-source-authority.js.map +1 -0
  57. package/dist/authored-worker-step.d.ts +2 -2
  58. package/dist/authored-worker-step.d.ts.map +1 -1
  59. package/dist/authored-worker-step.js +11 -6
  60. package/dist/authored-worker-step.js.map +1 -1
  61. package/dist/budget-preflight.d.ts +28 -3
  62. package/dist/budget-preflight.d.ts.map +1 -1
  63. package/dist/budget-preflight.js +34 -24
  64. package/dist/budget-preflight.js.map +1 -1
  65. package/dist/cli/build.d.ts.map +1 -1
  66. package/dist/cli/build.js +8 -0
  67. package/dist/cli/build.js.map +1 -1
  68. package/dist/cli/check.js +3 -1
  69. package/dist/cli/check.js.map +1 -1
  70. package/dist/cli/deploy.d.ts.map +1 -1
  71. package/dist/cli/deploy.js +5 -0
  72. package/dist/cli/deploy.js.map +1 -1
  73. package/dist/cli/direct-run.d.ts.map +1 -1
  74. package/dist/cli/direct-run.js +46 -14
  75. package/dist/cli/direct-run.js.map +1 -1
  76. package/dist/cli/run.d.ts +10 -0
  77. package/dist/cli/run.d.ts.map +1 -1
  78. package/dist/cli/run.js +90 -12
  79. package/dist/cli/run.js.map +1 -1
  80. package/dist/cli/step-failure.d.ts +40 -0
  81. package/dist/cli/step-failure.d.ts.map +1 -0
  82. package/dist/cli/step-failure.js +150 -0
  83. package/dist/cli/step-failure.js.map +1 -0
  84. package/dist/cli-adapter.d.ts +16 -0
  85. package/dist/cli-adapter.d.ts.map +1 -1
  86. package/dist/cli-adapter.js +17 -0
  87. package/dist/cli-adapter.js.map +1 -1
  88. package/dist/cli.d.ts.map +1 -1
  89. package/dist/cli.js +8 -8
  90. package/dist/cli.js.map +1 -1
  91. package/dist/cloud-run.d.ts +11 -1
  92. package/dist/cloud-run.d.ts.map +1 -1
  93. package/dist/cloud-run.js +65 -19
  94. package/dist/cloud-run.js.map +1 -1
  95. package/dist/daemon-connection.d.ts +7 -0
  96. package/dist/daemon-connection.d.ts.map +1 -1
  97. package/dist/daemon-connection.js +7 -0
  98. package/dist/daemon-connection.js.map +1 -1
  99. package/dist/failure-kinds.d.ts +30 -4
  100. package/dist/failure-kinds.d.ts.map +1 -1
  101. package/dist/failure-kinds.js +8 -1
  102. package/dist/failure-kinds.js.map +1 -1
  103. package/dist/journal-client.d.ts +3 -7
  104. package/dist/journal-client.d.ts.map +1 -1
  105. package/dist/journal-client.js +6 -2
  106. package/dist/journal-client.js.map +1 -1
  107. package/dist/llm-worker.d.ts.map +1 -1
  108. package/dist/llm-worker.js +4 -2
  109. package/dist/llm-worker.js.map +1 -1
  110. package/dist/local-agent.d.ts +1 -1
  111. package/dist/local-agent.d.ts.map +1 -1
  112. package/dist/local-agent.js +2 -2
  113. package/dist/local-agent.js.map +1 -1
  114. package/dist/model-pricing.d.ts +7 -6
  115. package/dist/model-pricing.d.ts.map +1 -1
  116. package/dist/model-pricing.js +8 -6
  117. package/dist/model-pricing.js.map +1 -1
  118. package/dist/preflight.d.ts +9 -1
  119. package/dist/preflight.d.ts.map +1 -1
  120. package/dist/preflight.js +36 -18
  121. package/dist/preflight.js.map +1 -1
  122. package/dist/protocol.d.ts +24 -5
  123. package/dist/protocol.d.ts.map +1 -1
  124. package/dist/spec.d.ts +27 -11
  125. package/dist/spec.d.ts.map +1 -1
  126. package/dist/spec.js.map +1 -1
  127. package/dist/worker-cli.d.ts.map +1 -1
  128. package/dist/worker-cli.js +6 -5
  129. package/dist/worker-cli.js.map +1 -1
  130. package/dist/worker-spend.d.ts +13 -8
  131. package/dist/worker-spend.d.ts.map +1 -1
  132. package/dist/worker-spend.js +27 -4
  133. package/dist/worker-spend.js.map +1 -1
  134. package/dist/worker.d.ts.map +1 -1
  135. package/dist/worker.js +4 -2
  136. package/dist/worker.js.map +1 -1
  137. package/package.json +2 -2
  138. package/src/adapters/base.ts +7 -0
  139. package/src/adapters/claude.ts +1 -0
  140. package/src/authored-admission.ts +17 -0
  141. package/src/authored-budget.ts +38 -11
  142. package/src/authored-flow-error.ts +0 -1
  143. package/src/authored-flow-executor.ts +32 -7
  144. package/src/authored-flow-loader.ts +96 -13
  145. package/src/authored-helper-effect.ts +5 -3
  146. package/src/authored-mcp.ts +6 -4
  147. package/src/authored-node-entry.ts +90 -0
  148. package/src/authored-node-runner.ts +219 -0
  149. package/src/authored-plugin-effect.ts +6 -3
  150. package/src/authored-root.ts +396 -0
  151. package/src/authored-runtime-capability.ts +40 -0
  152. package/src/authored-source-authority.ts +62 -0
  153. package/src/authored-worker-step.ts +15 -4
  154. package/src/budget-preflight.ts +52 -25
  155. package/src/cli/build.ts +7 -0
  156. package/src/cli/check.ts +3 -1
  157. package/src/cli/deploy.ts +4 -0
  158. package/src/cli/direct-run.ts +52 -16
  159. package/src/cli/run.ts +101 -12
  160. package/src/cli/step-failure.ts +160 -0
  161. package/src/cli-adapter.ts +27 -0
  162. package/src/cli.ts +9 -9
  163. package/src/cloud-run.ts +74 -21
  164. package/src/daemon-connection.ts +8 -0
  165. package/src/failure-kinds.ts +28 -2
  166. package/src/journal-client.ts +8 -3
  167. package/src/llm-worker.ts +4 -2
  168. package/src/local-agent.ts +7 -2
  169. package/src/model-pricing.ts +8 -6
  170. package/src/preflight.ts +45 -18
  171. package/src/protocol.ts +17 -2
  172. package/src/spec.ts +28 -5
  173. package/src/worker-cli.ts +7 -5
  174. package/src/worker-spend.ts +32 -5
  175. package/src/worker.ts +4 -2
  176. package/dist/cli/deterministic-failure.d.ts +0 -5
  177. package/dist/cli/deterministic-failure.d.ts.map +0 -1
  178. package/dist/cli/deterministic-failure.js +0 -66
  179. package/dist/cli/deterministic-failure.js.map +0 -1
  180. 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 === 'budget_missing_price' || refusal?.kind === 'budget_syntax_invalid' ? 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
- return budget === undefined ? consume(await journal.runStart(spec)) : budget.execute(journal, spec, consume);
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
- if (terminal) return readSuccessfulOutput(journal, await journal.runStart(spec), id, journalSteps);
179
- return budget.execute(journal, spec, outcome => readSuccessfulOutput(journal, outcome, id, journalSteps));
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
  }
@@ -1,31 +1,58 @@
1
1
  import type { CompiledFlowSpec } from './compile.js';
2
- import type { PreflightRefusal } from './preflight.js';
3
- import { MODEL_PRICING } from './model-pricing.js';
2
+ import type { PreflightWarning } from './preflight.js';
3
+ import { hasPricing } from './model-pricing.js';
4
+ import { resolveAdapterKind } from './adapters/index.js';
4
5
 
5
- /** Legacy explicit envelopes keep their worker-supplied pricing contract. */
6
- export function budgetDiagnostics(flow: CompiledFlowSpec): PreflightRefusal[] {
7
- if (flow.budget?.pricing !== 'frozen') return [];
8
- const diagnostics: PreflightRefusal[] = [];
9
- const declared = [
10
- ...Object.entries(flow.agents ?? {}).map(([agent, d]) => ({ agent, model: d.model })),
11
- ...flow.steps.flatMap(s => s.type !== 'deterministic' && s.model !== undefined
12
- ? [{ stepId: s.id, model: s.model }] : []),
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' || flow.budget.maxDollars === undefined) continue;
16
- const model = step.model ?? (step.type === 'agent' && step.agent !== undefined
17
- ? flow.agents?.[step.agent]?.model : undefined);
18
- if (model === undefined) diagnostics.push({
19
- severity: 'refusal', kind: 'budget_missing_price', stepId: step.id,
20
- message: `Step "${step.id}" needs a declared, priced model for its dollar budget.`,
21
- });
22
- }
23
- for (const declaration of declared) {
24
- if (Object.hasOwn(MODEL_PRICING, declaration.model)) continue;
25
- diagnostics.push({
26
- severity: 'refusal', kind: 'budget_missing_price', ...declaration,
27
- message: `Model "${declaration.model}" has no frozen price for budget accounting.`,
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 diagnostics;
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);
@@ -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(client, dataDir, options.onPtyReady);
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 executeAuthoredFlow(handle, client, input, {
78
- getDefinition,
79
- dataDir,
80
- flowPath: path,
81
- onProgress: options.onProgress,
82
- localAgentStream: localAgent?.stream,
83
- ...(options.signal !== undefined ? { signal: options.signal } : {}),
84
- ...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
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: terminal.runId, socketPath, status: 'parked',
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: terminal.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 === 'budget_missing_price'
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 { deterministicFailureDetails } from './deterministic-failure.js';
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
- client.close();
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
- const details = await deterministicFailureDetails(client, current.run_id);
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 command failure: ${errorMessage(error)}`;
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
  }