@relayflows/sdk 2.0.35 → 2.0.37

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 (228) hide show
  1. package/dist/adapters/base.d.ts +16 -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 +15 -0
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/codex.d.ts.map +1 -1
  7. package/dist/adapters/codex.js +4 -0
  8. package/dist/adapters/codex.js.map +1 -1
  9. package/dist/adapters/usage-limit.d.ts +2 -0
  10. package/dist/adapters/usage-limit.d.ts.map +1 -0
  11. package/dist/adapters/usage-limit.js +17 -0
  12. package/dist/adapters/usage-limit.js.map +1 -0
  13. package/dist/authored-activity.d.ts +17 -0
  14. package/dist/authored-activity.d.ts.map +1 -0
  15. package/dist/authored-activity.js +218 -0
  16. package/dist/authored-activity.js.map +1 -0
  17. package/dist/authored-child-run.d.ts +34 -0
  18. package/dist/authored-child-run.d.ts.map +1 -0
  19. package/dist/authored-child-run.js +128 -0
  20. package/dist/authored-child-run.js.map +1 -0
  21. package/dist/authored-flow-error.d.ts +32 -3
  22. package/dist/authored-flow-error.d.ts.map +1 -1
  23. package/dist/authored-flow-error.js +3 -1
  24. package/dist/authored-flow-error.js.map +1 -1
  25. package/dist/authored-flow-executor.d.ts +11 -1
  26. package/dist/authored-flow-executor.d.ts.map +1 -1
  27. package/dist/authored-flow-executor.js +32 -1
  28. package/dist/authored-flow-executor.js.map +1 -1
  29. package/dist/authored-helper-effect.d.ts.map +1 -1
  30. package/dist/authored-helper-effect.js +3 -2
  31. package/dist/authored-helper-effect.js.map +1 -1
  32. package/dist/authored-mcp.d.ts.map +1 -1
  33. package/dist/authored-mcp.js +4 -2
  34. package/dist/authored-mcp.js.map +1 -1
  35. package/dist/authored-node-entry.js +9 -1
  36. package/dist/authored-node-entry.js.map +1 -1
  37. package/dist/authored-node-runner.d.ts.map +1 -1
  38. package/dist/authored-node-runner.js +42 -9
  39. package/dist/authored-node-runner.js.map +1 -1
  40. package/dist/authored-plugin-effect.d.ts.map +1 -1
  41. package/dist/authored-plugin-effect.js +4 -2
  42. package/dist/authored-plugin-effect.js.map +1 -1
  43. package/dist/authored-preflight.d.ts +1 -1
  44. package/dist/authored-preflight.d.ts.map +1 -1
  45. package/dist/authored-preflight.js +2 -2
  46. package/dist/authored-preflight.js.map +1 -1
  47. package/dist/authored-root.d.ts +12 -7
  48. package/dist/authored-root.d.ts.map +1 -1
  49. package/dist/authored-root.js +24 -0
  50. package/dist/authored-root.js.map +1 -1
  51. package/dist/authored-step-output.d.ts +24 -8
  52. package/dist/authored-step-output.d.ts.map +1 -1
  53. package/dist/authored-step-output.js +81 -15
  54. package/dist/authored-step-output.js.map +1 -1
  55. package/dist/authored-subscription-park.d.ts +5 -0
  56. package/dist/authored-subscription-park.d.ts.map +1 -0
  57. package/dist/authored-subscription-park.js +47 -0
  58. package/dist/authored-subscription-park.js.map +1 -0
  59. package/dist/authored-verdict.d.ts +2 -0
  60. package/dist/authored-verdict.d.ts.map +1 -1
  61. package/dist/authored-verdict.js +2 -1
  62. package/dist/authored-verdict.js.map +1 -1
  63. package/dist/authored-worker-step.d.ts +1 -1
  64. package/dist/authored-worker-step.d.ts.map +1 -1
  65. package/dist/authored-worker-step.js +9 -4
  66. package/dist/authored-worker-step.js.map +1 -1
  67. package/dist/cli/check-activities.d.ts +8 -0
  68. package/dist/cli/check-activities.d.ts.map +1 -0
  69. package/dist/cli/check-activities.js +155 -0
  70. package/dist/cli/check-activities.js.map +1 -0
  71. package/dist/cli/check.d.ts +2 -0
  72. package/dist/cli/check.d.ts.map +1 -1
  73. package/dist/cli/check.js +3 -3
  74. package/dist/cli/check.js.map +1 -1
  75. package/dist/cli/cli-probe.d.ts +2 -2
  76. package/dist/cli/cli-probe.d.ts.map +1 -1
  77. package/dist/cli/cli-probe.js +22 -9
  78. package/dist/cli/cli-probe.js.map +1 -1
  79. package/dist/cli/direct-run.d.ts.map +1 -1
  80. package/dist/cli/direct-run.js +17 -4
  81. package/dist/cli/direct-run.js.map +1 -1
  82. package/dist/cli/run.d.ts +18 -5
  83. package/dist/cli/run.d.ts.map +1 -1
  84. package/dist/cli/run.js +37 -12
  85. package/dist/cli/run.js.map +1 -1
  86. package/dist/cli/subscription-report.d.ts +5 -0
  87. package/dist/cli/subscription-report.d.ts.map +1 -0
  88. package/dist/cli/subscription-report.js +48 -0
  89. package/dist/cli/subscription-report.js.map +1 -0
  90. package/dist/cli-adapter.d.ts +4 -2
  91. package/dist/cli-adapter.d.ts.map +1 -1
  92. package/dist/cli-adapter.js +4 -0
  93. package/dist/cli-adapter.js.map +1 -1
  94. package/dist/cli.d.ts +1 -1
  95. package/dist/cli.d.ts.map +1 -1
  96. package/dist/cli.js +13 -5
  97. package/dist/cli.js.map +1 -1
  98. package/dist/communication/local.d.ts +1 -1
  99. package/dist/communication/local.d.ts.map +1 -1
  100. package/dist/communication/local.js +2 -2
  101. package/dist/communication/local.js.map +1 -1
  102. package/dist/communication/worker.d.ts +1 -1
  103. package/dist/communication/worker.d.ts.map +1 -1
  104. package/dist/communication/worker.js +4 -4
  105. package/dist/communication/worker.js.map +1 -1
  106. package/dist/decimal-dollars.d.ts +12 -0
  107. package/dist/decimal-dollars.d.ts.map +1 -0
  108. package/dist/decimal-dollars.js +39 -0
  109. package/dist/decimal-dollars.js.map +1 -0
  110. package/dist/effect-channel.d.ts.map +1 -1
  111. package/dist/effect-channel.js +2 -0
  112. package/dist/effect-channel.js.map +1 -1
  113. package/dist/failure-kinds.d.ts +2 -2
  114. package/dist/failure-kinds.d.ts.map +1 -1
  115. package/dist/failure-kinds.js +2 -0
  116. package/dist/failure-kinds.js.map +1 -1
  117. package/dist/helper-instruction.d.ts +3 -0
  118. package/dist/helper-instruction.d.ts.map +1 -0
  119. package/dist/helper-instruction.js +3 -0
  120. package/dist/helper-instruction.js.map +1 -0
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js.map +1 -1
  124. package/dist/journal-client.d.ts +21 -3
  125. package/dist/journal-client.d.ts.map +1 -1
  126. package/dist/journal-client.js +45 -6
  127. package/dist/journal-client.js.map +1 -1
  128. package/dist/journal-reader.d.ts.map +1 -1
  129. package/dist/journal-reader.js +2 -0
  130. package/dist/journal-reader.js.map +1 -1
  131. package/dist/llm-worker.d.ts +2 -1
  132. package/dist/llm-worker.d.ts.map +1 -1
  133. package/dist/llm-worker.js +7 -2
  134. package/dist/llm-worker.js.map +1 -1
  135. package/dist/local-agent-environment.d.ts +13 -0
  136. package/dist/local-agent-environment.d.ts.map +1 -0
  137. package/dist/local-agent-environment.js +104 -0
  138. package/dist/local-agent-environment.js.map +1 -0
  139. package/dist/local-agent.d.ts +4 -1
  140. package/dist/local-agent.d.ts.map +1 -1
  141. package/dist/local-agent.js +15 -5
  142. package/dist/local-agent.js.map +1 -1
  143. package/dist/preflight.d.ts +6 -0
  144. package/dist/preflight.d.ts.map +1 -1
  145. package/dist/preflight.js +36 -10
  146. package/dist/preflight.js.map +1 -1
  147. package/dist/protocol.d.ts +187 -1
  148. package/dist/protocol.d.ts.map +1 -1
  149. package/dist/reported-cost-total.d.ts +41 -0
  150. package/dist/reported-cost-total.d.ts.map +1 -0
  151. package/dist/reported-cost-total.js +63 -0
  152. package/dist/reported-cost-total.js.map +1 -0
  153. package/dist/reported-cost.d.ts +24 -0
  154. package/dist/reported-cost.d.ts.map +1 -0
  155. package/dist/reported-cost.js +49 -0
  156. package/dist/reported-cost.js.map +1 -0
  157. package/dist/run-state.d.ts +9 -11
  158. package/dist/run-state.d.ts.map +1 -1
  159. package/dist/run-state.js +15 -38
  160. package/dist/run-state.js.map +1 -1
  161. package/dist/step-settling.d.ts +11 -0
  162. package/dist/step-settling.d.ts.map +1 -0
  163. package/dist/step-settling.js +22 -0
  164. package/dist/step-settling.js.map +1 -0
  165. package/dist/worker-cli.d.ts +1 -1
  166. package/dist/worker-cli.d.ts.map +1 -1
  167. package/dist/worker-cli.js +3 -3
  168. package/dist/worker-cli.js.map +1 -1
  169. package/dist/worker.d.ts +2 -0
  170. package/dist/worker.d.ts.map +1 -1
  171. package/dist/worker.js +5 -2
  172. package/dist/worker.js.map +1 -1
  173. package/dist/yaml-helper-effect.d.ts.map +1 -1
  174. package/dist/yaml-helper-effect.js +2 -1
  175. package/dist/yaml-helper-effect.js.map +1 -1
  176. package/dist/yaml-helpers.d.ts.map +1 -1
  177. package/dist/yaml-helpers.js +2 -1
  178. package/dist/yaml-helpers.js.map +1 -1
  179. package/package.json +2 -2
  180. package/src/adapters/base.ts +19 -0
  181. package/src/adapters/claude.ts +15 -0
  182. package/src/adapters/codex.ts +6 -0
  183. package/src/adapters/usage-limit.ts +17 -0
  184. package/src/authored-activity.ts +251 -0
  185. package/src/authored-child-run.ts +138 -0
  186. package/src/authored-flow-error.ts +33 -0
  187. package/src/authored-flow-executor.ts +44 -1
  188. package/src/authored-helper-effect.ts +3 -2
  189. package/src/authored-mcp.ts +4 -2
  190. package/src/authored-node-entry.ts +11 -2
  191. package/src/authored-node-runner.ts +43 -9
  192. package/src/authored-plugin-effect.ts +4 -2
  193. package/src/authored-preflight.ts +2 -2
  194. package/src/authored-root.ts +34 -4
  195. package/src/authored-step-output.ts +88 -15
  196. package/src/authored-subscription-park.ts +46 -0
  197. package/src/authored-verdict.ts +2 -1
  198. package/src/authored-worker-step.ts +9 -3
  199. package/src/cli/check-activities.ts +144 -0
  200. package/src/cli/check.ts +6 -3
  201. package/src/cli/cli-probe.ts +23 -9
  202. package/src/cli/direct-run.ts +16 -1
  203. package/src/cli/run.ts +59 -14
  204. package/src/cli/subscription-report.ts +51 -0
  205. package/src/cli-adapter.ts +8 -2
  206. package/src/cli.ts +13 -6
  207. package/src/communication/local.ts +3 -2
  208. package/src/communication/worker.ts +7 -4
  209. package/src/decimal-dollars.ts +38 -0
  210. package/src/effect-channel.ts +2 -0
  211. package/src/failure-kinds.ts +2 -0
  212. package/src/helper-instruction.ts +2 -0
  213. package/src/index.ts +6 -0
  214. package/src/journal-client.ts +60 -7
  215. package/src/journal-reader.ts +2 -0
  216. package/src/llm-worker.ts +6 -1
  217. package/src/local-agent-environment.ts +92 -0
  218. package/src/local-agent.ts +19 -4
  219. package/src/preflight.ts +46 -10
  220. package/src/protocol.ts +151 -0
  221. package/src/reported-cost-total.ts +82 -0
  222. package/src/reported-cost.ts +51 -0
  223. package/src/run-state.ts +25 -37
  224. package/src/step-settling.ts +19 -0
  225. package/src/worker-cli.ts +3 -2
  226. package/src/worker.ts +9 -2
  227. package/src/yaml-helper-effect.ts +2 -1
  228. package/src/yaml-helpers.ts +2 -1
@@ -43,6 +43,7 @@ import type { LoadedAuthoredFlowNode } from './authored-flow-loader.js';
43
43
  import type { RunLifecycleOptions } from './cli/run.js';
44
44
  import {
45
45
  AuthoredFlowExecutionError,
46
+ type AuthoredFlowSuspension,
46
47
  AuthoredHumanParked,
47
48
  type AuthoredFlowExecutionErrorCode,
48
49
  } from './authored-flow-error.js';
@@ -54,6 +55,7 @@ import {
54
55
  verifyAuthoredOperations,
55
56
  } from './authored-flow-operation.js';
56
57
  import { AuthoredFlowLifecycle } from './authored-flow-lifecycle.js';
58
+ import { AuthoredActivities } from './authored-activity.js';
57
59
  import { displayLabel, type AuthoredStepEdges } from './authored-step-index.js';
58
60
  import { JournalClient } from './journal-client.js';
59
61
  import { PluginError } from './plugin-manifest.js';
@@ -105,6 +107,7 @@ export interface AuthoredExecutionRuntime {
105
107
  }
106
108
 
107
109
  export interface AuthoredFlowExecutionResult {
110
+ readonly state?: undefined;
108
111
  readonly executionRuntime?: AuthoredExecutionRuntime;
109
112
  readonly rootRunId?: string;
110
113
  readonly name: string;
@@ -118,6 +121,14 @@ export interface AuthoredFlowExecutionResult {
118
121
  readonly journalSteps: readonly AuthoredFlowJournalStep[];
119
122
  }
120
123
 
124
+ /** A body reached a durable event boundary and released its worker lease. */
125
+ export interface AuthoredFlowSuspendedResult {
126
+ readonly state: 'suspended';
127
+ readonly name: string;
128
+ readonly suspension: AuthoredFlowSuspension;
129
+ readonly journalSteps: readonly AuthoredFlowJournalStep[];
130
+ }
131
+
121
132
  // The authoring SURFACE stays wide: `done()` accepts every
122
133
  // `FlowCompletionReason` and refuses the kernel-owned ones at runtime with a
123
134
  // diagnostic that names the alternative. The RESULT is narrow, because `done()`
@@ -174,6 +185,8 @@ export interface ExecuteAuthoredFlowOptions {
174
185
  readonly onWait?: RunLifecycleOptions['onWait'];
175
186
  readonly onProgress?: (event: ProgressEvent) => void;
176
187
  readonly localAgentStream?: string;
188
+ /** Environment reserved for provider probes and local agent subprocesses. */
189
+ readonly agentEnvironment?: NodeJS.ProcessEnv;
177
190
  /**
178
191
  * How many agent (and, separately, LLM) dispatches the attached local
179
192
  * workers hold at once. Set, it caps this body's concurrent `f.agent` /
@@ -273,6 +286,7 @@ export async function executeAuthoredFlow<Input = undefined>(
273
286
  const journalSteps: AuthoredFlowJournalStep[] = [];
274
287
  const authoredSteps: AuthoredFlowOperation<unknown>[] = [];
275
288
  const lifecycle = new AuthoredFlowLifecycle();
289
+ const activities = new AuthoredActivities(journal, options.rootRunId);
276
290
  const edgeOverrides = new Map<string, AuthoredStepEdges>();
277
291
  const stepEdges = (step: string): AuthoredStepEdges | undefined => {
278
292
  const overridden = edgeOverrides.get(step);
@@ -300,6 +314,7 @@ export async function executeAuthoredFlow<Input = undefined>(
300
314
  definition.name, journal, journalSteps, budget, {
301
315
  ...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
302
316
  ...(options.dataDir === undefined ? {} : { dataDir: options.dataDir }),
317
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
303
318
  stepEdges,
304
319
  },
305
320
  );
@@ -307,7 +322,7 @@ export async function executeAuthoredFlow<Input = undefined>(
307
322
  const worker = authoredWorkerRunner(
308
323
  definition, journal, flowPath, journalSteps, waitOptions,
309
324
  localAgentStream, budget, definition.header.budget, options.rootRunId, options.workerCapacity, stepEdges,
310
- options.workerSlots,
325
+ options.workerSlots, options.agentEnvironment,
311
326
  );
312
327
 
313
328
  /**
@@ -567,6 +582,17 @@ export async function executeAuthoredFlow<Input = undefined>(
567
582
  );
568
583
  return trackStep(authoredSteps, agentOp);
569
584
  },
585
+ on(source, activityOptions) {
586
+ assertOperationAllowed('on', definition.name, requestedCompletion);
587
+ // Activity ids are root-scoped (`activity-N`); a child's own counter would collide.
588
+ if ((options.dispatchDepth ?? 0) > 0) {
589
+ throw new AuthoredFlowExecutionError(
590
+ 'dispatch_invalid',
591
+ `child flow "${definition.name}" cannot call f.on in this release; activities are root-scoped`,
592
+ );
593
+ }
594
+ return activities.open(source, activityOptions);
595
+ },
570
596
  /**
571
597
  * `f.human` (docs/SURFACE.md §1, §7). The question is not a child run: it
572
598
  * is the ROOT attempt parking on the kernel's `wait.human`. The body
@@ -661,6 +687,7 @@ export async function executeAuthoredFlow<Input = undefined>(
661
687
  dataDir: options.dataDir,
662
688
  flowPath: child.path,
663
689
  ...(localAgentStream === undefined ? {} : { localAgentStream }),
690
+ ...(options.agentEnvironment === undefined ? {} : { agentEnvironment: options.agentEnvironment }),
664
691
  ...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
665
692
  ...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
666
693
  flowGraph: options.flowGraph,
@@ -794,6 +821,11 @@ export async function executeAuthoredFlow<Input = undefined>(
794
821
  try {
795
822
  worker.stop(bodyFailure);
796
823
  await stopAuthoredOperations(authoredSteps, bodyFailure);
824
+ // A durable wait hands execution back to the control plane. Its
825
+ // subscriptions must keep receiving events while no body is running.
826
+ const parked = bodyFailure instanceof AuthoredFlowExecutionError
827
+ && (bodyFailure.code === 'subscription_suspended' || bodyFailure.code === 'human_parked');
828
+ if (!parked) await activities.closeAll('canceled');
797
829
  } finally {
798
830
  lifecycle.close();
799
831
  }
@@ -821,6 +853,7 @@ export async function executeAuthoredFlow<Input = undefined>(
821
853
  try {
822
854
  worker.stop(missingCompletion);
823
855
  await stopAuthoredOperations(authoredSteps, missingCompletion);
856
+ await activities.closeAll('canceled');
824
857
  } finally {
825
858
  lifecycle.close();
826
859
  }
@@ -828,6 +861,16 @@ export async function executeAuthoredFlow<Input = undefined>(
828
861
  }
829
862
  try {
830
863
  await verifyAuthoredOperations(definition.name, authoredSteps, lifecycle);
864
+ } catch (error) {
865
+ try {
866
+ await activities.closeAll('canceled');
867
+ } finally {
868
+ lifecycle.close();
869
+ }
870
+ throw error;
871
+ }
872
+ try {
873
+ await activities.closeAll('run_completed');
831
874
  } finally {
832
875
  lifecycle.close();
833
876
  }
@@ -14,6 +14,7 @@ import { helperWriteback, HelperDeliveryError } from './helper-writeback.js';
14
14
  import { checkSlackHelpers } from './slack-preflight.js';
15
15
  import { atomicJson, readSlackReceipt, receiptPath, slackWriteback, type SlackCall } from './slack-writeback.js';
16
16
  import { authoredChildAdmissionKey } from './authored-admission.js';
17
+ import { NO_MODEL_COST } from './reported-cost.js';
17
18
 
18
19
  type ProviderCall = HelperCall | SlackCall;
19
20
 
@@ -135,11 +136,11 @@ async function completeHelperDispatch(client: JournalClient, dispatch: StepDispa
135
136
  await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt,
136
137
  dispatch.idempotency_key, 'worker_error', {
137
138
  output: { ...call, diagnostic: error.message },
138
- started_pins: dispatch.pins, end_pins: dispatch.pins, effects: [],
139
+ started_pins: dispatch.pins, end_pins: dispatch.pins, effects: [], reported_cost: NO_MODEL_COST,
139
140
  });
140
141
  return;
141
142
  }
142
143
  await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt,
143
- dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins,
144
+ dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins, reported_cost: NO_MODEL_COST,
144
145
  effects: [{ surface_path: `/${call.provider}`, idempotency_key: dispatch.idempotency_key }] });
145
146
  }
@@ -12,6 +12,7 @@ import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
12
12
  import { readCompletedStepOutput } from './authored-step-output.js';
13
13
  import { withWorkerLease } from './worker-lease.js';
14
14
  import { authoredChildAdmissionKey } from './authored-admission.js';
15
+ import { NO_MODEL_COST } from './reported-cost.js';
15
16
 
16
17
  /** Own keys expose precisely the preflight inventory, including prototype-like names. */
17
18
  export function buildMcpProxy(
@@ -116,6 +117,7 @@ export async function runMcpEffect(
116
117
  output: { ...receipt, output: output ?? null, ...(diagnostic ? { diagnostic } : {}) },
117
118
  started_pins: event.pins, end_pins: event.pins,
118
119
  ...(diagnostic ? { trajectory_tail: { ...receipt, diagnostic } } : {}),
120
+ reported_cost: NO_MODEL_COST,
119
121
  effects: confirmed ? [{ surface_path: surfacePath, idempotency_key: event.idempotency_key }] : [],
120
122
  });
121
123
  }
@@ -140,13 +142,13 @@ export async function runMcpEffect(
140
142
  childRunId = outcome.run_id;
141
143
  if (outcome.status !== 'completed') await completed;
142
144
  if (diagnostic !== undefined) {
143
- try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps); }
145
+ try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status); }
144
146
  catch (error) {
145
147
  if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
146
148
  throw new McpStepError(diagnostic, outcome.run_id);
147
149
  }
148
150
  }
149
- const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps) as { output: unknown };
151
+ const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status) as { output: unknown };
150
152
  return receipt.output;
151
153
  } finally {
152
154
  clearTimeout(timer);
@@ -8,6 +8,7 @@ import { assertAuthoredNodeVersion, parseAuthoredParentPid } from './authored-ru
8
8
  import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow-error.js';
9
9
  import { isAgentCapacity } from './worker-slots.js';
10
10
  import type { AuthoredRootMetadata } from './authored-root.js';
11
+ import { validatedLocalAgentEnvironment } from './local-agent-environment.js';
11
12
 
12
13
  let channelKey: string | undefined, sequence = 0;
13
14
  // Capture writers before loading authored modules; credentials never enter env.
@@ -49,7 +50,8 @@ try {
49
50
  send({ type: 'ready', runtime: { kind: 'node', version: process.versions.node,
50
51
  executableSha256: hash(process.execPath), payloadSha256: hash(process.argv[1]!) } });
51
52
  const request = await new Promise<{ channelKey: string; metadata: AuthoredRootMetadata; socketPath: string;
52
- rootRunId: string; dataDir: string; localAgentStream?: string; workerCapacity?: number }>((resolve, reject) => {
53
+ rootRunId: string; dataDir: string; localAgentStream?: string; workerCapacity?: number;
54
+ agentEnvironment?: unknown }>((resolve, reject) => {
53
55
  let buffer = '';
54
56
  process.stdin.setEncoding('utf8');
55
57
  const onData = (chunk: string): void => {
@@ -65,6 +67,10 @@ try {
65
67
  if (typeof request.channelKey !== 'string' || !/^[a-f0-9]{64}$/.test(request.channelKey)) throw new Error('invalid authored channel key');
66
68
  channelKey = request.channelKey;
67
69
  controller.signal.throwIfAborted();
70
+ // Validate the parent handoff before evaluating any authored module. Only
71
+ // provider credentials may cross this channel; controls such as PATH and
72
+ // NODE_OPTIONS remain the child's inherited values.
73
+ const agentEnvironment = validatedLocalAgentEnvironment(request.agentEnvironment);
68
74
  const loaded = await loadPinnedAuthoredSource(request.metadata, true);
69
75
  if (request.localAgentStream !== request.metadata.localAgentStream) throw new Error('authored root local agent surface mismatch');
70
76
  if (request.workerCapacity !== undefined && !isAgentCapacity(request.workerCapacity)) throw new Error('invalid authored worker capacity');
@@ -78,6 +84,7 @@ try {
78
84
  flowGraph: loaded.graph,
79
85
  localAgentStream: request.localAgentStream, signal: controller.signal,
80
86
  ...(request.workerCapacity === undefined ? {} : { workerCapacity: request.workerCapacity }),
87
+ ...(agentEnvironment === undefined ? {} : { agentEnvironment: { ...process.env, ...agentEnvironment } }),
81
88
  onProgress: event => send({ type: 'progress', event }),
82
89
  onWait: event => send({ type: 'wait', event }),
83
90
  });
@@ -94,7 +101,9 @@ try {
94
101
  // the only one that saw the child's classification.
95
102
  ...(error instanceof AuthoredFlowExecutionError ? { code: error.code,
96
103
  completionReason: error.completionReason, runId: error.runId,
97
- details: error.details, parkCause: error.parkCause } : {}),
104
+ details: error.details,
105
+ parkCause: error.parkCause,
106
+ ...(error.suspension === undefined ? {} : { suspension: error.suspension }) } : {}),
98
107
  ...(error instanceof AuthoredHumanParked ? { wait: error.wait } : {}) });
99
108
  process.exitCode = 1;
100
109
  } finally {
@@ -18,6 +18,7 @@ import type {
18
18
  } from './failure-kinds.js';
19
19
  import { HUMAN_WAIT_ID } from './authored-human.js';
20
20
  import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
21
+ import { settlingCompletion } from './step-settling.js';
21
22
 
22
23
  let embeddedSource: string | undefined;
23
24
  /** Installed by the standalone build; never fetched or resolved from a workspace. */
@@ -133,7 +134,8 @@ export async function runAuthoredInNode(
133
134
  // Keep stdin open: EOF tells the child its lease-owning parent died.
134
135
  child.stdin!.write(JSON.stringify({ channelKey, metadata, socketPath, rootRunId,
135
136
  dataDir: options.dataDir, localAgentStream: options.localAgentStream,
136
- workerCapacity: options.workerCapacity }) + '\n');
137
+ workerCapacity: options.workerCapacity,
138
+ agentEnvironment: options.agentEnvironment }) + '\n');
137
139
  } else if (!ready || result) throw new Error('unexpected authored runtime message');
138
140
  else if (message.type === 'progress') options.onProgress?.(message.event);
139
141
  else if (message.type === 'wait') options.onWait?.(message.event);
@@ -144,7 +146,8 @@ export async function runAuthoredInNode(
144
146
  } else if (typeof message.code === 'string') {
145
147
  const authored = new AuthoredFlowExecutionError(message.code as AuthoredFlowExecutionErrorCode,
146
148
  message.message, message.completionReason, message.runId,
147
- stepFailedFrame(message.details));
149
+ stepFailedFrame(message.details),
150
+ isSuspension(message.suspension) ? message.suspension : undefined);
148
151
  // Validated, not trusted, like `details`: an unrecognised cause
149
152
  // is dropped so the boundary says nothing about workers rather
150
153
  // than acting on a value this frame could have invented.
@@ -172,6 +175,35 @@ export async function runAuthoredInNode(
172
175
  } finally { await rm(directory, { recursive: true, force: true }); }
173
176
  }
174
177
 
178
+ function isSuspension(value: unknown): value is import('./authored-flow-error.js').AuthoredFlowSuspension {
179
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
180
+ const record = value as Record<string, unknown>;
181
+ const common = typeof record['subscriptionId'] === 'string' && record['subscriptionId'].length > 0
182
+ && typeof record['stream'] === 'string' && record['stream'].length > 0
183
+ && Number.isSafeInteger(record['deadlineAtMs']) && (record['deadlineAtMs'] as number) >= 0;
184
+ if (!common) return false;
185
+ if (record['kind'] === 'event_wait') return true;
186
+ return record['kind'] === 'activation'
187
+ && Array.isArray(record['eventTypes']) && record['eventTypes'].length > 0
188
+ && record['eventTypes'].every(type => typeof type === 'string' && type.length > 0)
189
+ && (record['pattern'] === undefined || isJsonRecord(record['pattern']))
190
+ && Number.isSafeInteger(record['settleMs']) && (record['settleMs'] as number) >= 0
191
+ && Number.isSafeInteger(record['idleMs']) && (record['idleMs'] as number) > 0
192
+ && typeof record['includeSelf'] === 'boolean';
193
+ }
194
+
195
+ function isJsonRecord(value: unknown): value is Record<string, unknown> {
196
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
197
+ && Object.values(value).every(isJsonValue);
198
+ }
199
+
200
+ function isJsonValue(value: unknown): boolean {
201
+ return value === null || typeof value === 'boolean' || typeof value === 'string'
202
+ || (typeof value === 'number' && Number.isFinite(value))
203
+ || (Array.isArray(value) && value.every(isJsonValue))
204
+ || isJsonRecord(value);
205
+ }
206
+
175
207
  /** A park signal from the child names the question the parent must journal. */
176
208
  function isHumanWaitFrame(value: unknown): value is AuthoredHumanWait {
177
209
  const wait = value as Partial<AuthoredHumanWait> | null;
@@ -342,15 +374,17 @@ export async function verifyAuthoredNodeResult(
342
374
  // step a child spec may carry, and it must have completed too.
343
375
  const lowered = spec?.steps ?? [];
344
376
  const specShape = lowered.length === 1 || (lowered.length === 2 && lowered[1]?.id === `${claimed.id}.gate`);
345
- const completed = entries.filter(entry => entry.entry_type === 'step.completed' && entry.step_id === claimed.id);
346
- const gateCompleted = lowered.length === 2
347
- ? entries.filter(entry => entry.entry_type === 'step.completed' && entry.step_id === `${claimed.id}.gate`) : [];
377
+ // Judged against the step's SETTLING completion, the same one the
378
+ // authored reader returned: a retried attempt (`disposition: retry`,
379
+ // e.g. crash recovery's `crashed`) journals a completion of its own
380
+ // ahead of it, so neither a count nor the first entry is the verdict.
381
+ const completed = settlingCompletion(entries, claimed.id);
382
+ const gateCompleted = lowered.length === 2 ? settlingCompletion(entries, `${claimed.id}.gate`) : undefined;
348
383
  const terminalFacts = entries.filter(entry => entry.entry_type === 'run.completed');
349
384
  if (spec?.name !== `${metadata.flowName}/${claimed.id}` || !specShape
350
- || step?.id !== claimed.id || completed.length !== 1
351
- || completed[0]?.payload?.completionReason !== 'success'
352
- || (lowered.length === 2 && (gateCompleted.length !== 1 || gateCompleted[0]?.payload?.completionReason !== 'success'))
353
- || terminalFacts.length !== 1 || terminalFacts[0]?.payload?.completionReason !== 'success') invalid(`evidence ${claimed.id} spec=${spec?.name} step=${step?.id} completed=${completed.length}`);
385
+ || step?.id !== claimed.id || completed?.payload?.completionReason !== 'success'
386
+ || (lowered.length === 2 && gateCompleted?.payload?.completionReason !== 'success')
387
+ || terminalFacts.length !== 1 || terminalFacts[0]?.payload?.completionReason !== 'success') invalid(`evidence ${claimed.id} spec=${spec?.name} step=${step?.id} completed=${completed?.payload?.completionReason}`);
354
388
  if (claimed === terminal) {
355
389
  // The claimed verdict must match the marker the journal actually
356
390
  // recorded, so an IPC frame cannot claim `success` over a run whose
@@ -11,6 +11,7 @@ import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
11
11
  import { readCompletedStepOutput } from './authored-step-output.js';
12
12
  import { withWorkerLease } from './worker-lease.js';
13
13
  import { authoredChildAdmissionKey } from './authored-admission.js';
14
+ import { NO_MODEL_COST } from './reported-cost.js';
14
15
 
15
16
  export class PluginStepError extends AuthoredFlowExecutionError {
16
17
  constructor(readonly diagnostic: string, runId: string) {
@@ -88,6 +89,7 @@ export async function runPluginEffect(
88
89
  output: { ...receipt, output: output ?? null, ...(diagnostic ? { diagnostic } : {}) },
89
90
  started_pins: event.pins, end_pins: event.pins,
90
91
  ...(diagnostic ? { trajectory_tail: { ...receipt, diagnostic } } : {}),
92
+ reported_cost: NO_MODEL_COST,
91
93
  effects: confirmed ? [{ surface_path: surfacePath, idempotency_key: event.idempotency_key }] : [],
92
94
  });
93
95
  }
@@ -125,13 +127,13 @@ export async function runPluginEffect(
125
127
  if (dispatchExpired) await cancelChildRun();
126
128
  if (outcome.status !== 'completed') await completed;
127
129
  if (diagnostic !== undefined) {
128
- try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps); }
130
+ try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status); }
129
131
  catch (error) {
130
132
  if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
131
133
  throw new PluginStepError(diagnostic, outcome.run_id);
132
134
  }
133
135
  }
134
- const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps) as { output: unknown };
136
+ const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status) as { output: unknown };
135
137
  return receipt.output;
136
138
  }, admissionKey), deadline]);
137
139
  } finally {
@@ -6,7 +6,7 @@ import { cliProbeKey, CliProbeError, resolvePreflight, type CliProbeOutcome } fr
6
6
  import type { FlowSpec } from './spec.js';
7
7
 
8
8
  /** Auth/model facts belong to one execution, including concurrent cold callers. */
9
- export function authoredPreflight(path: string) {
9
+ export function authoredPreflight(path: string, environment?: NodeJS.ProcessEnv) {
10
10
  const probeCache = new Map<string, CliProbeOutcome>();
11
11
  const pending = new Map<string, Promise<void>>();
12
12
  const directory = dirname(resolve(path));
@@ -30,7 +30,7 @@ export function authoredPreflight(path: string) {
30
30
  try {
31
31
  probeCache.set(key, { result: await probeCliAsync(resolution.cli,
32
32
  resolution.source === 'project' ? config.directory : directory,
33
- resolution.model, managed ? 'managed' : undefined) });
33
+ resolution.model, managed ? 'managed' : undefined, environment) });
34
34
  } catch (error) {
35
35
  probeCache.set(key, { failure: error instanceof CliProbeError ? error.detail : null });
36
36
  }
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { canonicalize } from './canonical.js';
6
6
  import { compileSpec, toKernelSpec } from './compile.js';
7
- import { executeAuthoredFlow, type AuthoredFlowExecutionResult } from './authored-flow-executor.js';
7
+ import { executeAuthoredFlow, type AuthoredFlowExecutionResult, type AuthoredFlowSuspendedResult } from './authored-flow-executor.js';
8
8
  import { isDurableCompletionDetail, isLoweredCompletion } from './authored-completion.js';
9
9
  import { AUTHORED_ROOT_KIND } from './authored-verdict.js';
10
10
  import {
@@ -20,6 +20,12 @@ import { withWorkerLease } from './worker-lease.js';
20
20
  import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow-error.js';
21
21
  import { readOpenHumanWaits } from './authored-human.js';
22
22
  import { isSurfaceCompletionReason } from './authored-step-output.js';
23
+ import { readSubscriptionPark } from './authored-subscription-park.js';
24
+ import { localAgentCredentialEnvironment } from './local-agent-environment.js';
25
+
26
+ export type DurableAuthoredFlowResult =
27
+ | (AuthoredFlowExecutionResult & { readonly rootRunId: string })
28
+ | (AuthoredFlowSuspendedResult & { readonly rootRunId: string });
23
29
 
24
30
  /**
25
31
  * Taken from the projection module rather than spelled twice: `flows status`
@@ -55,11 +61,15 @@ export interface AuthoredRootSourceAuthority {
55
61
  }
56
62
 
57
63
  export interface DurableAuthoredOptions {
64
+ /** Preserve root authority even if a child later fails or parks. */
65
+ readonly onAdmitted?: (runId: string) => void;
58
66
  readonly dataDir: string;
59
67
  readonly admissionKey?: string;
60
68
  readonly localAgentStream?: string;
61
69
  /** Concurrency of the attached local workers; see `ExecuteAuthoredFlowOptions.workerCapacity`. */
62
70
  readonly workerCapacity?: number;
71
+ /** Provider-only environment consumed by preflight and local workers. */
72
+ readonly agentEnvironment?: NodeJS.ProcessEnv;
63
73
  readonly lifecycle?: RunLifecycleOptions;
64
74
  }
65
75
 
@@ -69,7 +79,7 @@ export async function executeDurableAuthoredFlow(
69
79
  journal: JournalClient,
70
80
  input: unknown,
71
81
  options: DurableAuthoredOptions,
72
- ): Promise<AuthoredFlowExecutionResult & { readonly rootRunId: string }> {
82
+ ): Promise<DurableAuthoredFlowResult> {
73
83
  assertAuthoredRuntimeAvailable();
74
84
  const source = await readFile(loaded.sourcePath);
75
85
  const sources = await Promise.all(loaded.graph.map(async node => Object.freeze({
@@ -106,6 +116,7 @@ export async function executeDurableAuthoredFlow(
106
116
  workspace: [], streams: [{ stream, read_offset: 0 }],
107
117
  });
108
118
  const outcome = await journal.runStart(spec, undefined, admissionKey);
119
+ options.onAdmitted?.(outcome.run_id);
109
120
  if (outcome.status === 'completed') {
110
121
  dispatchWait.cancel();
111
122
  return await completedRootResult(journal, outcome.run_id);
@@ -119,6 +130,9 @@ export async function executeDurableAuthoredFlow(
119
130
  // redelivers only when the former worker connection is gone.
120
131
  const resumed = await journal.runResume(outcome.run_id);
121
132
  await assertNoOpenHumanWait(journal, resumed);
133
+ const parked = await readSubscriptionPark(journal, outcome.run_id);
134
+ if (parked !== undefined) return { state: 'suspended', name: metadata.flowName,
135
+ suspension: parked, journalSteps: [], rootRunId: outcome.run_id };
122
136
  const dispatch = await dispatchWait.promise;
123
137
  return await driveRoot(loaded, metadata, journal, peer, dispatch, options);
124
138
  } finally {
@@ -132,7 +146,7 @@ export async function resumeDurableAuthoredFlow(
132
146
  rootRunId: string,
133
147
  journal: JournalClient,
134
148
  options: Omit<DurableAuthoredOptions, 'admissionKey'>,
135
- ): Promise<(AuthoredFlowExecutionResult & { readonly rootRunId: string }) | undefined> {
149
+ ): Promise<DurableAuthoredFlowResult | undefined> {
136
150
  const metadata = await readAuthoredRootMetadata(journal, rootRunId);
137
151
  if (metadata === undefined) return undefined;
138
152
  assertAuthoredRuntimeAvailable();
@@ -159,6 +173,9 @@ export async function resumeDurableAuthoredFlow(
159
173
  assertRootCanDispatch(outcome);
160
174
  await assertNoOpenHumanWait(journal, outcome);
161
175
  options.lifecycle?.onRunStarted?.({ runId: rootRunId, flow: metadata.flowName, resumed: true });
176
+ const parked = await readSubscriptionPark(journal, rootRunId);
177
+ if (parked !== undefined) return { state: 'suspended', name: metadata.flowName,
178
+ suspension: parked, journalSteps: [], rootRunId };
162
179
  const dispatch = await dispatchWait.promise;
163
180
  return await driveRoot(loaded, metadata, journal, peer, dispatch, options);
164
181
  } finally {
@@ -204,7 +221,7 @@ async function driveRoot(
204
221
  peer: JournalClient,
205
222
  dispatch: StepDispatchEvent,
206
223
  options: Omit<DurableAuthoredOptions, 'admissionKey'>,
207
- ): Promise<AuthoredFlowExecutionResult & { readonly rootRunId: string }> {
224
+ ): Promise<DurableAuthoredFlowResult> {
208
225
  try {
209
226
  const result = await withWorkerLease(peer, dispatch, async rootSignal => {
210
227
  const callerSignal = options.lifecycle?.signal;
@@ -212,6 +229,9 @@ async function driveRoot(
212
229
  if (process.versions['bun'] !== undefined) {
213
230
  return runAuthoredInNode(metadata, journal.socketPath, dispatch.run_id, {
214
231
  dataDir: options.dataDir, localAgentStream: options.localAgentStream,
232
+ ...(options.agentEnvironment === undefined ? {} : {
233
+ agentEnvironment: localAgentCredentialEnvironment(options.agentEnvironment),
234
+ }),
215
235
  ...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
216
236
  ...options.lifecycle, signal,
217
237
  });
@@ -225,6 +245,7 @@ async function driveRoot(
225
245
  dataDir: options.dataDir,
226
246
  flowPath: metadata.flowPath,
227
247
  localAgentStream: options.localAgentStream,
248
+ ...(options.agentEnvironment === undefined ? {} : { agentEnvironment: options.agentEnvironment }),
228
249
  ...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
229
250
  rootRunId: dispatch.run_id,
230
251
  extensions: loaded.extensions,
@@ -248,6 +269,15 @@ async function driveRoot(
248
269
  );
249
270
  return Object.freeze({ ...result, rootRunId: dispatch.run_id });
250
271
  } catch (error) {
272
+ if (error instanceof AuthoredFlowExecutionError
273
+ && error.code === 'subscription_suspended'
274
+ && error.suspension !== undefined) {
275
+ await peer.subscriptionPark({ run_id: dispatch.run_id, step_id: dispatch.step_id,
276
+ attempt: dispatch.attempt, idempotency_key: dispatch.idempotency_key,
277
+ subscription_id: error.suspension.subscriptionId, phase: error.suspension.kind });
278
+ return Object.freeze({ state: 'suspended' as const, name: metadata.flowName,
279
+ suspension: error.suspension, journalSteps: Object.freeze([]), rootRunId: dispatch.run_id });
280
+ }
251
281
  if (error instanceof AuthoredHumanParked) {
252
282
  // Not a failure: the body reached a question nobody has answered. Park
253
283
  // THIS attempt on the kernel's `wait.human` under the body's own wait