@relayflows/sdk 2.0.41 → 2.0.43

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 (370) hide show
  1. package/dist/adapters/base.d.ts +12 -1
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/base.js +11 -1
  4. package/dist/adapters/base.js.map +1 -1
  5. package/dist/adapters/claude.d.ts.map +1 -1
  6. package/dist/adapters/claude.js +8 -4
  7. package/dist/adapters/claude.js.map +1 -1
  8. package/dist/adapters/codex.d.ts.map +1 -1
  9. package/dist/adapters/codex.js +15 -6
  10. package/dist/adapters/codex.js.map +1 -1
  11. package/dist/agent-resume.d.ts +8 -0
  12. package/dist/agent-resume.d.ts.map +1 -0
  13. package/dist/agent-resume.js +37 -0
  14. package/dist/agent-resume.js.map +1 -0
  15. package/dist/agent-transcript.d.ts.map +1 -1
  16. package/dist/agent-transcript.js +5 -3
  17. package/dist/agent-transcript.js.map +1 -1
  18. package/dist/authored-flow-error.d.ts +10 -2
  19. package/dist/authored-flow-error.d.ts.map +1 -1
  20. package/dist/authored-flow-error.js.map +1 -1
  21. package/dist/authored-flow-executor.d.ts.map +1 -1
  22. package/dist/authored-flow-executor.js +16 -2
  23. package/dist/authored-flow-executor.js.map +1 -1
  24. package/dist/authored-flow-loader.d.ts +6 -0
  25. package/dist/authored-flow-loader.d.ts.map +1 -1
  26. package/dist/authored-flow-loader.js +18 -6
  27. package/dist/authored-flow-loader.js.map +1 -1
  28. package/dist/authored-helper-effect.js +3 -2
  29. package/dist/authored-helper-effect.js.map +1 -1
  30. package/dist/authored-human.d.ts +2 -0
  31. package/dist/authored-human.d.ts.map +1 -1
  32. package/dist/authored-human.js +4 -0
  33. package/dist/authored-human.js.map +1 -1
  34. package/dist/authored-memory.d.ts +47 -2
  35. package/dist/authored-memory.d.ts.map +1 -1
  36. package/dist/authored-memory.js +394 -15
  37. package/dist/authored-memory.js.map +1 -1
  38. package/dist/authored-node-entry.js +6 -1
  39. package/dist/authored-node-entry.js.map +1 -1
  40. package/dist/authored-node-runner.d.ts +1 -1
  41. package/dist/authored-node-runner.d.ts.map +1 -1
  42. package/dist/authored-node-runner.js +6 -3
  43. package/dist/authored-node-runner.js.map +1 -1
  44. package/dist/authored-root-readback.d.ts +21 -0
  45. package/dist/authored-root-readback.d.ts.map +1 -0
  46. package/dist/authored-root-readback.js +90 -0
  47. package/dist/authored-root-readback.js.map +1 -0
  48. package/dist/authored-root.d.ts.map +1 -1
  49. package/dist/authored-root.js +53 -74
  50. package/dist/authored-root.js.map +1 -1
  51. package/dist/authored-step-output.d.ts +2 -0
  52. package/dist/authored-step-output.d.ts.map +1 -1
  53. package/dist/authored-step-output.js +1 -0
  54. package/dist/authored-step-output.js.map +1 -1
  55. package/dist/authored-worker-step.d.ts.map +1 -1
  56. package/dist/authored-worker-step.js +26 -4
  57. package/dist/authored-worker-step.js.map +1 -1
  58. package/dist/cli/check-helper-body.d.ts +2 -2
  59. package/dist/cli/check-helper-body.d.ts.map +1 -1
  60. package/dist/cli/check-helper-body.js +7 -2
  61. package/dist/cli/check-helper-body.js.map +1 -1
  62. package/dist/cli/check-helper-surface.d.ts +8 -0
  63. package/dist/cli/check-helper-surface.d.ts.map +1 -0
  64. package/dist/cli/check-helper-surface.js +37 -0
  65. package/dist/cli/check-helper-surface.js.map +1 -0
  66. package/dist/cli/check-triggers.d.ts +4 -3
  67. package/dist/cli/check-triggers.d.ts.map +1 -1
  68. package/dist/cli/check-triggers.js +7 -3
  69. package/dist/cli/check-triggers.js.map +1 -1
  70. package/dist/cli/check.d.ts +7 -1
  71. package/dist/cli/check.d.ts.map +1 -1
  72. package/dist/cli/check.js +35 -8
  73. package/dist/cli/check.js.map +1 -1
  74. package/dist/cli/cloud-answer.d.ts +9 -0
  75. package/dist/cli/cloud-answer.d.ts.map +1 -0
  76. package/dist/cli/cloud-answer.js +39 -0
  77. package/dist/cli/cloud-answer.js.map +1 -0
  78. package/dist/cli/cloud-connect-cli.d.ts +2 -1
  79. package/dist/cli/cloud-connect-cli.d.ts.map +1 -1
  80. package/dist/cli/cloud-connect-cli.js +10 -2
  81. package/dist/cli/cloud-connect-cli.js.map +1 -1
  82. package/dist/cli/cloud-human.d.ts +7 -0
  83. package/dist/cli/cloud-human.d.ts.map +1 -0
  84. package/dist/cli/cloud-human.js +22 -0
  85. package/dist/cli/cloud-human.js.map +1 -0
  86. package/dist/cli/cloud-live.d.ts +2 -2
  87. package/dist/cli/cloud-live.d.ts.map +1 -1
  88. package/dist/cli/cloud-live.js +9 -2
  89. package/dist/cli/cloud-live.js.map +1 -1
  90. package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
  91. package/dist/cli/cloud-mirror-session.js +3 -2
  92. package/dist/cli/cloud-mirror-session.js.map +1 -1
  93. package/dist/cli/cloud-run.d.ts +1 -1
  94. package/dist/cli/cloud-run.d.ts.map +1 -1
  95. package/dist/cli/cloud-run.js +12 -0
  96. package/dist/cli/cloud-run.js.map +1 -1
  97. package/dist/cli/detached-record.d.ts +22 -0
  98. package/dist/cli/detached-record.d.ts.map +1 -0
  99. package/dist/cli/detached-record.js +57 -0
  100. package/dist/cli/detached-record.js.map +1 -0
  101. package/dist/cli/detached-run.d.ts +28 -0
  102. package/dist/cli/detached-run.d.ts.map +1 -0
  103. package/dist/cli/detached-run.js +122 -0
  104. package/dist/cli/detached-run.js.map +1 -0
  105. package/dist/cli/diagnostic-fold.d.ts +19 -0
  106. package/dist/cli/diagnostic-fold.d.ts.map +1 -0
  107. package/dist/cli/diagnostic-fold.js +19 -0
  108. package/dist/cli/diagnostic-fold.js.map +1 -0
  109. package/dist/cli/direct-run.d.ts.map +1 -1
  110. package/dist/cli/direct-run.js +16 -2
  111. package/dist/cli/direct-run.js.map +1 -1
  112. package/dist/cli/eval.d.ts +22 -0
  113. package/dist/cli/eval.d.ts.map +1 -0
  114. package/dist/cli/eval.js +131 -0
  115. package/dist/cli/eval.js.map +1 -0
  116. package/dist/cli/journal-timeout.d.ts +12 -0
  117. package/dist/cli/journal-timeout.d.ts.map +1 -0
  118. package/dist/cli/journal-timeout.js +42 -0
  119. package/dist/cli/journal-timeout.js.map +1 -0
  120. package/dist/cli/local-agent-remedy.d.ts.map +1 -1
  121. package/dist/cli/local-agent-remedy.js +5 -4
  122. package/dist/cli/local-agent-remedy.js.map +1 -1
  123. package/dist/cli/observer-session.d.ts +1 -0
  124. package/dist/cli/observer-session.d.ts.map +1 -1
  125. package/dist/cli/observer-session.js +8 -1
  126. package/dist/cli/observer-session.js.map +1 -1
  127. package/dist/cli/reuse.d.ts +8 -0
  128. package/dist/cli/reuse.d.ts.map +1 -1
  129. package/dist/cli/reuse.js +24 -0
  130. package/dist/cli/reuse.js.map +1 -1
  131. package/dist/cli/run.d.ts +8 -2
  132. package/dist/cli/run.d.ts.map +1 -1
  133. package/dist/cli/run.js +72 -56
  134. package/dist/cli/run.js.map +1 -1
  135. package/dist/cli/running-step.d.ts +9 -0
  136. package/dist/cli/running-step.d.ts.map +1 -0
  137. package/dist/cli/running-step.js +156 -0
  138. package/dist/cli/running-step.js.map +1 -0
  139. package/dist/cli-adapter.d.ts +3 -1
  140. package/dist/cli-adapter.d.ts.map +1 -1
  141. package/dist/cli-adapter.js +6 -2
  142. package/dist/cli-adapter.js.map +1 -1
  143. package/dist/cli-commands.d.ts +77 -11
  144. package/dist/cli-commands.d.ts.map +1 -1
  145. package/dist/cli-commands.js +41 -7
  146. package/dist/cli-commands.js.map +1 -1
  147. package/dist/cli-watch.d.ts +6 -2
  148. package/dist/cli-watch.d.ts.map +1 -1
  149. package/dist/cli-watch.js +7 -6
  150. package/dist/cli-watch.js.map +1 -1
  151. package/dist/cli.d.ts +22 -1
  152. package/dist/cli.d.ts.map +1 -1
  153. package/dist/cli.js +154 -50
  154. package/dist/cli.js.map +1 -1
  155. package/dist/cloud-answer.d.ts +35 -0
  156. package/dist/cloud-answer.d.ts.map +1 -0
  157. package/dist/cloud-answer.js +194 -0
  158. package/dist/cloud-answer.js.map +1 -0
  159. package/dist/cloud-auth-store.d.ts +46 -0
  160. package/dist/cloud-auth-store.d.ts.map +1 -0
  161. package/dist/cloud-auth-store.js +171 -0
  162. package/dist/cloud-auth-store.js.map +1 -0
  163. package/dist/cloud-http.d.ts +7 -16
  164. package/dist/cloud-http.d.ts.map +1 -1
  165. package/dist/cloud-http.js +59 -40
  166. package/dist/cloud-http.js.map +1 -1
  167. package/dist/cloud-human-wait.d.ts +10 -0
  168. package/dist/cloud-human-wait.d.ts.map +1 -0
  169. package/dist/cloud-human-wait.js +17 -0
  170. package/dist/cloud-human-wait.js.map +1 -0
  171. package/dist/cloud-mirror-transport.d.ts.map +1 -1
  172. package/dist/cloud-mirror-transport.js +1 -0
  173. package/dist/cloud-mirror-transport.js.map +1 -1
  174. package/dist/cloud-run-record.d.ts +7 -2
  175. package/dist/cloud-run-record.d.ts.map +1 -1
  176. package/dist/cloud-run-record.js +13 -5
  177. package/dist/cloud-run-record.js.map +1 -1
  178. package/dist/cloud-run.d.ts +9 -0
  179. package/dist/cloud-run.d.ts.map +1 -1
  180. package/dist/cloud-run.js +36 -2
  181. package/dist/cloud-run.js.map +1 -1
  182. package/dist/communication/local.d.ts.map +1 -1
  183. package/dist/communication/local.js +6 -1
  184. package/dist/communication/local.js.map +1 -1
  185. package/dist/communication/worker.d.ts.map +1 -1
  186. package/dist/communication/worker.js +12 -2
  187. package/dist/communication/worker.js.map +1 -1
  188. package/dist/compile.d.ts.map +1 -1
  189. package/dist/compile.js +5 -3
  190. package/dist/compile.js.map +1 -1
  191. package/dist/daemon-spec-validation.d.ts +31 -0
  192. package/dist/daemon-spec-validation.d.ts.map +1 -0
  193. package/dist/daemon-spec-validation.js +115 -0
  194. package/dist/daemon-spec-validation.js.map +1 -0
  195. package/dist/failure-kinds.d.ts +6 -4
  196. package/dist/failure-kinds.d.ts.map +1 -1
  197. package/dist/failure-kinds.js +17 -1
  198. package/dist/failure-kinds.js.map +1 -1
  199. package/dist/flow-eval-cloud.d.ts +25 -0
  200. package/dist/flow-eval-cloud.d.ts.map +1 -0
  201. package/dist/flow-eval-cloud.js +108 -0
  202. package/dist/flow-eval-cloud.js.map +1 -0
  203. package/dist/flow-eval-error.d.ts +7 -0
  204. package/dist/flow-eval-error.d.ts.map +1 -0
  205. package/dist/flow-eval-error.js +10 -0
  206. package/dist/flow-eval-error.js.map +1 -0
  207. package/dist/flow-eval-gate.d.ts +15 -0
  208. package/dist/flow-eval-gate.d.ts.map +1 -0
  209. package/dist/flow-eval-gate.js +102 -0
  210. package/dist/flow-eval-gate.js.map +1 -0
  211. package/dist/flow-eval-local.d.ts +54 -0
  212. package/dist/flow-eval-local.d.ts.map +1 -0
  213. package/dist/flow-eval-local.js +220 -0
  214. package/dist/flow-eval-local.js.map +1 -0
  215. package/dist/flow-eval-outcome.d.ts +22 -0
  216. package/dist/flow-eval-outcome.d.ts.map +1 -0
  217. package/dist/flow-eval-outcome.js +37 -0
  218. package/dist/flow-eval-outcome.js.map +1 -0
  219. package/dist/flow-eval-report.d.ts +106 -0
  220. package/dist/flow-eval-report.d.ts.map +1 -0
  221. package/dist/flow-eval-report.js +4 -0
  222. package/dist/flow-eval-report.js.map +1 -0
  223. package/dist/flow-eval-suite.d.ts +46 -0
  224. package/dist/flow-eval-suite.d.ts.map +1 -0
  225. package/dist/flow-eval-suite.js +144 -0
  226. package/dist/flow-eval-suite.js.map +1 -0
  227. package/dist/flow-eval-version.d.ts +59 -0
  228. package/dist/flow-eval-version.d.ts.map +1 -0
  229. package/dist/flow-eval-version.js +303 -0
  230. package/dist/flow-eval-version.js.map +1 -0
  231. package/dist/flow-eval.d.ts +41 -0
  232. package/dist/flow-eval.d.ts.map +1 -0
  233. package/dist/flow-eval.js +201 -0
  234. package/dist/flow-eval.js.map +1 -0
  235. package/dist/helper-operation-use.d.ts +18 -0
  236. package/dist/helper-operation-use.d.ts.map +1 -0
  237. package/dist/helper-operation-use.js +336 -0
  238. package/dist/helper-operation-use.js.map +1 -0
  239. package/dist/helper-preflight.d.ts.map +1 -1
  240. package/dist/helper-preflight.js +16 -2
  241. package/dist/helper-preflight.js.map +1 -1
  242. package/dist/helper-reference.d.ts +33 -0
  243. package/dist/helper-reference.d.ts.map +1 -1
  244. package/dist/helper-reference.js +5 -5
  245. package/dist/helper-reference.js.map +1 -1
  246. package/dist/index.d.ts +6 -1
  247. package/dist/index.d.ts.map +1 -1
  248. package/dist/index.js +5 -1
  249. package/dist/index.js.map +1 -1
  250. package/dist/journal-budgeted-reads.d.ts +41 -0
  251. package/dist/journal-budgeted-reads.d.ts.map +1 -0
  252. package/dist/journal-budgeted-reads.js +142 -0
  253. package/dist/journal-budgeted-reads.js.map +1 -0
  254. package/dist/journal-client.d.ts +34 -29
  255. package/dist/journal-client.d.ts.map +1 -1
  256. package/dist/journal-client.js +45 -151
  257. package/dist/journal-client.js.map +1 -1
  258. package/dist/journal-connection.d.ts +42 -0
  259. package/dist/journal-connection.d.ts.map +1 -0
  260. package/dist/journal-connection.js +209 -0
  261. package/dist/journal-connection.js.map +1 -0
  262. package/dist/journal-read-policy.d.ts +38 -0
  263. package/dist/journal-read-policy.d.ts.map +1 -0
  264. package/dist/journal-read-policy.js +126 -0
  265. package/dist/journal-read-policy.js.map +1 -0
  266. package/dist/named-gate-lowering.d.ts.map +1 -1
  267. package/dist/named-gate-lowering.js +10 -7
  268. package/dist/named-gate-lowering.js.map +1 -1
  269. package/dist/re2js-embedded.d.ts +3 -0
  270. package/dist/re2js-embedded.d.ts.map +1 -0
  271. package/dist/re2js-embedded.js +4 -0
  272. package/dist/re2js-embedded.js.map +1 -0
  273. package/dist/spec.d.ts +10 -0
  274. package/dist/spec.d.ts.map +1 -1
  275. package/dist/spec.js.map +1 -1
  276. package/dist/step-fields.d.ts +1 -1
  277. package/dist/step-fields.js +1 -1
  278. package/dist/step-fields.js.map +1 -1
  279. package/dist/validate.d.ts.map +1 -1
  280. package/dist/validate.js +15 -0
  281. package/dist/validate.js.map +1 -1
  282. package/dist/worker-cli.d.ts +3 -1
  283. package/dist/worker-cli.d.ts.map +1 -1
  284. package/dist/worker-cli.js +15 -5
  285. package/dist/worker-cli.js.map +1 -1
  286. package/dist/worker-lease.d.ts.map +1 -1
  287. package/dist/worker-lease.js +11 -3
  288. package/dist/worker-lease.js.map +1 -1
  289. package/dist/worker.js +1 -1
  290. package/dist/worker.js.map +1 -1
  291. package/package.json +5 -3
  292. package/src/adapters/base.ts +16 -1
  293. package/src/adapters/claude.ts +8 -4
  294. package/src/adapters/codex.ts +15 -6
  295. package/src/agent-resume.ts +36 -0
  296. package/src/agent-transcript.ts +5 -3
  297. package/src/authored-flow-error.ts +9 -0
  298. package/src/authored-flow-executor.ts +18 -1
  299. package/src/authored-flow-loader.ts +27 -10
  300. package/src/authored-helper-effect.ts +3 -2
  301. package/src/authored-human.ts +5 -0
  302. package/src/authored-memory.ts +445 -15
  303. package/src/authored-node-entry.ts +6 -1
  304. package/src/authored-node-runner.ts +6 -3
  305. package/src/authored-root-readback.ts +100 -0
  306. package/src/authored-root.ts +58 -85
  307. package/src/authored-step-output.ts +3 -0
  308. package/src/authored-worker-step.ts +29 -4
  309. package/src/cli/check-helper-body.ts +9 -3
  310. package/src/cli/check-helper-surface.ts +43 -0
  311. package/src/cli/check-triggers.ts +9 -4
  312. package/src/cli/check.ts +37 -7
  313. package/src/cli/cloud-answer.ts +40 -0
  314. package/src/cli/cloud-connect-cli.ts +9 -2
  315. package/src/cli/cloud-human.ts +24 -0
  316. package/src/cli/cloud-live.ts +10 -5
  317. package/src/cli/cloud-mirror-session.ts +3 -2
  318. package/src/cli/cloud-run.ts +10 -1
  319. package/src/cli/detached-record.ts +71 -0
  320. package/src/cli/detached-run.ts +131 -0
  321. package/src/cli/diagnostic-fold.ts +28 -0
  322. package/src/cli/direct-run.ts +14 -1
  323. package/src/cli/eval.ts +141 -0
  324. package/src/cli/journal-timeout.ts +49 -0
  325. package/src/cli/local-agent-remedy.ts +6 -4
  326. package/src/cli/observer-session.ts +7 -1
  327. package/src/cli/reuse.ts +25 -0
  328. package/src/cli/run.ts +75 -67
  329. package/src/cli/running-step.ts +140 -0
  330. package/src/cli-adapter.ts +7 -1
  331. package/src/cli-commands.ts +43 -7
  332. package/src/cli-watch.ts +8 -5
  333. package/src/cli.ts +158 -51
  334. package/src/cloud-answer.ts +202 -0
  335. package/src/cloud-auth-store.ts +185 -0
  336. package/src/cloud-http.ts +61 -50
  337. package/src/cloud-human-wait.ts +23 -0
  338. package/src/cloud-mirror-transport.ts +1 -0
  339. package/src/cloud-run-record.ts +14 -5
  340. package/src/cloud-run.ts +46 -2
  341. package/src/communication/local.ts +6 -1
  342. package/src/communication/worker.ts +11 -3
  343. package/src/compile.ts +5 -3
  344. package/src/daemon-spec-validation.ts +126 -0
  345. package/src/failure-kinds.ts +17 -1
  346. package/src/flow-eval-cloud.ts +114 -0
  347. package/src/flow-eval-error.ts +10 -0
  348. package/src/flow-eval-gate.ts +114 -0
  349. package/src/flow-eval-local.ts +236 -0
  350. package/src/flow-eval-outcome.ts +43 -0
  351. package/src/flow-eval-report.ts +102 -0
  352. package/src/flow-eval-suite.ts +169 -0
  353. package/src/flow-eval-version.ts +316 -0
  354. package/src/flow-eval.ts +234 -0
  355. package/src/helper-operation-use.ts +285 -0
  356. package/src/helper-preflight.ts +17 -2
  357. package/src/helper-reference.ts +6 -6
  358. package/src/index.ts +15 -1
  359. package/src/journal-budgeted-reads.ts +134 -0
  360. package/src/journal-client.ts +59 -166
  361. package/src/journal-connection.ts +222 -0
  362. package/src/journal-read-policy.ts +123 -0
  363. package/src/named-gate-lowering.ts +10 -7
  364. package/src/re2js-embedded.ts +3 -0
  365. package/src/spec.ts +10 -0
  366. package/src/step-fields.ts +1 -1
  367. package/src/validate.ts +14 -0
  368. package/src/worker-cli.ts +14 -3
  369. package/src/worker-lease.ts +11 -3
  370. package/src/worker.ts +1 -1
package/src/cloud-run.ts CHANGED
@@ -10,7 +10,7 @@ import { snapshotJsonValue, type JsonValue } from './json-value.js';
10
10
  import { loadAuthoredFlow, type SurfaceModuleAuthority } from './authored-flow-loader.js';
11
11
  import { assertNoUseDependencies, collectExtensionSubmissions, type FlowExtensionSubmission } from './flow-extension-submit.js';
12
12
  import {
13
- CloudFlowError, cloudConnection, cloudFetch, cloudRequest, cloudRunId, isCloudRecord,
13
+ CloudFlowError, resolveCloudConnection, cloudFetch, cloudRequest, cloudRunId, isCloudRecord,
14
14
  type CloudConnectionOptions,
15
15
  } from './cloud-http.js';
16
16
  import { cloudRunState, isCloudRunActive, type CloudRunState } from './cloud-run-record.js';
@@ -37,6 +37,15 @@ export interface RunInCloudOptions extends CloudConnectionOptions {
37
37
  * admitted" from "admission unknown".
38
38
  */
39
39
  onSubmit?: () => void;
40
+ /**
41
+ * Per-run environment variables for the hosted sandbox, e.g. a short-lived
42
+ * token for the caller's own API. Sent with this run's submission only: never
43
+ * part of {@link cloudSubmissionBody}, so a schedule never stores them, and
44
+ * never echoed in errors or the receipt. Names must match
45
+ * `^[A-Za-z_][A-Za-z0-9_]*$`; Cloud refuses names it owns (run identity,
46
+ * Relay/Relayfile auth, provider keys such as `ANTHROPIC_API_KEY`).
47
+ */
48
+ envSecrets?: Readonly<Record<string, string>>;
40
49
  }
41
50
  export interface CloudRunReceipt {
42
51
  runId: string;
@@ -68,6 +77,37 @@ export function cloudSubmissionBody(submission: CloudSubmission): Record<string,
68
77
 
69
78
  export type { CloudRunState };
70
79
 
80
+ const ENV_SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
81
+
82
+ /**
83
+ * Validate and copy per-run env secrets before any network call. Errors name
84
+ * the offending variable, never its value.
85
+ */
86
+ function snapshotEnvSecrets(value: unknown): Record<string, string> | undefined {
87
+ if (value === undefined) return undefined;
88
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
89
+ throw new CloudFlowError('invalid_input', 'envSecrets must be an object mapping variable names to string values.');
90
+ }
91
+ // Null prototype: an own `__proto__` key (JSON.parse creates one) must not
92
+ // hit the inherited setter and vanish from the copy.
93
+ const copy: Record<string, string> = Object.create(null) as Record<string, string>;
94
+ for (const [name, secret] of Object.entries(value)) {
95
+ if (!ENV_SECRET_NAME.test(name)) {
96
+ throw new CloudFlowError('invalid_input', `envSecrets name "${name}" must match ^[A-Za-z_][A-Za-z0-9_]*$.`);
97
+ }
98
+ if (name === '__proto__') {
99
+ // Valid shell syntax, but Cloud's plain-object env handling cannot carry
100
+ // it and refuses it; fail here, by name, instead of losing it silently.
101
+ throw new CloudFlowError('invalid_input', 'envSecrets name "__proto__" cannot be delivered to a hosted run.');
102
+ }
103
+ if (typeof secret !== 'string') {
104
+ throw new CloudFlowError('invalid_input', `envSecrets value for "${name}" must be a string.`);
105
+ }
106
+ copy[name] = secret;
107
+ }
108
+ return Object.keys(copy).length === 0 ? undefined : copy;
109
+ }
110
+
71
111
  /**
72
112
  * Submit a declarative flow to Cloud's pinned v2 runtime. Compilation only
73
113
  * happens here; Cloud owns provisioning and the Rust engine owns execution.
@@ -210,8 +250,11 @@ export async function runInCloud(
210
250
  flow: CloudFlowSource,
211
251
  options: RunInCloudOptions = {},
212
252
  ): Promise<CloudRunReceipt> {
213
- const { baseUrl } = cloudConnection(options);
253
+ // Validate locally first: resolving the connection may refresh a stored
254
+ // login, which posts the refresh token and rotates the credential file.
255
+ const envSecrets = snapshotEnvSecrets(options.envSecrets);
214
256
  const submission = await prepareCloudSubmission(flow, options);
257
+ const { baseUrl } = await resolveCloudConnection(options);
215
258
  const hash = submission.specHash;
216
259
  // Sync before submission: `prepare` reserves the run ID and the upload lands
217
260
  // under it, so the run request below names code Cloud already holds. A
@@ -239,6 +282,7 @@ export async function runInCloud(
239
282
  ...cloudSubmissionBody(submission),
240
283
  ...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
241
284
  ...(synced === undefined ? {} : { runId: synced.runId, s3CodeKey: synced.codeKey }),
285
+ ...(envSecrets === undefined ? {} : { envSecrets }),
242
286
  }) });
243
287
  } catch (error) {
244
288
  // Cloud accepts exactly one authored Surface (the one its sandbox runs).
@@ -1,5 +1,6 @@
1
1
  import { onWorkerFailure } from '../worker-lease.js';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { FLOW_READ_BUDGET_MS } from '../journal-read-policy.js';
3
4
  import { JournalClient } from '../journal-client.js';
4
5
  import { AgentWorker } from '../worker.js';
5
6
  import type { KernelRunSpec } from '../spec.js';
@@ -18,6 +19,9 @@ export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath
18
19
  let failure: unknown;
19
20
  const close = async () => {
20
21
  const outcomes = await Promise.allSettled(workers.map(async ({ client, worker }) => {
22
+ // Stop budgeted reads first: draining the worker must not wait out a
23
+ // delayed history read on shutdown.
24
+ client.cancelReads();
21
25
  try { await worker.close(); } finally { client.close(); }
22
26
  }));
23
27
  const rejected = outcomes.find(result => result.status === 'rejected');
@@ -28,7 +32,8 @@ export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath
28
32
  if (step.type !== 'agent') continue;
29
33
  requireCommunicationCli(step.cli ?? spec.cli);
30
34
  if (step.surfaces?.workspace?.length) throw new Error('Local communication workers support stream surfaces only');
31
- const client = new JournalClient(socketPath);
35
+ // History reads on a retried attempt meet the same CPU load as flow reads.
36
+ const client = new JournalClient(socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS });
32
37
  const worker = new AgentWorker(client, { workerId: `communication-${randomUUID()}`, dataDir, environment,
33
38
  requiredStreams: [channelName(step.id, '$receipts')],
34
39
  pins: { workspace: [], streams: step.surfaces?.streams?.map(({ stream }) => ({ stream, read_offset: 0 })) ?? [] } });
@@ -5,7 +5,8 @@ import { setTimeout as delay } from 'node:timers/promises';
5
5
  import type { JournalClient } from '../journal-client.js';
6
6
  import type { StepDispatchEvent } from '../protocol.js';
7
7
  import type { ResolvedKernelAgentStep } from '../resolved-cli-identity.js';
8
- import { withWorkerLease } from '../worker-lease.js';
8
+ import { isLeaseLost, withWorkerLease } from '../worker-lease.js';
9
+ import { isReadInterruptionError } from '../journal-read-policy.js';
9
10
  import { workerInstruction } from '../worker-input.js';
10
11
  import { resolveCliModel } from '../cli-adapter.js';
11
12
  import { resolveAgentCwd } from '../agent-cwd.js';
@@ -26,9 +27,16 @@ export async function completeCommunicationDispatch(client: JournalClient, dispa
26
27
  let output: unknown;
27
28
  let completionReason: 'success' | 'worker_error' = 'success';
28
29
  try {
29
- output = await withWorkerLease(client, dispatch, signal =>
30
- run(client, dispatch, instruction, spec, dataDir, signal, runRoot, environment));
30
+ output = await withWorkerLease(client, dispatch, signal => {
31
+ // A lost lease stops this attempt's history reads instead of letting
32
+ // them run out the budget beside the kernel's retry.
33
+ const releaseReads = client.scopeReads?.(signal) ?? (() => {});
34
+ return run(client, dispatch, instruction, spec, dataDir, signal, runRoot, environment).finally(releaseReads);
35
+ });
31
36
  } catch (error) {
37
+ // A lost lease or an unanswered read belongs to the kernel's sweep and
38
+ // retry: never complete the attempt here as a worker failure.
39
+ if (isLeaseLost(error) || isReadInterruptionError(error)) throw error;
32
40
  completionReason = 'worker_error';
33
41
  output = { error: error instanceof Error ? error.message : String(error) };
34
42
  }
package/src/compile.ts CHANGED
@@ -253,6 +253,7 @@ function compileStep(step: StepSpec): StepSpec {
253
253
  ...(s.cli !== undefined ? { cli: s.cli } : {}),
254
254
  ...(s.model !== undefined ? { model: s.model } : {}),
255
255
  ...(s.cwd !== undefined ? { cwd: s.cwd } : {}),
256
+ ...(s.resume !== undefined ? { resume: s.resume } : {}),
256
257
  ...(s.transport !== undefined ? { transport: s.transport } : {}),
257
258
  recoveryMode,
258
259
  ...(s.surfaces !== undefined ? { surfaces: s.surfaces } : {}),
@@ -464,7 +465,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
464
465
  const unionKeys = [
465
466
  'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input',
466
467
  'command', 'timeout_ms', 'lease_ms', 'on_non_zero', 'prompt', 'model', 'cli', 'instruction',
467
- 'cwd', 'recovery_mode', 'surfaces', 'permissions', 'transport', 'cli_identity',
468
+ 'cwd', 'resume', 'recovery_mode', 'surfaces', 'permissions', 'transport', 'cli_identity',
468
469
  ] as const;
469
470
  const step = requireKernelObject(value, unionKeys, at);
470
471
  if (step['cli_identity'] !== undefined) {
@@ -479,7 +480,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
479
480
  : type === 'llm'
480
481
  ? ['prompt', 'model', 'cli', 'cli_identity'] as const
481
482
  : type === 'agent'
482
- ? ['timeout_ms', 'instruction', 'cli', 'model', 'recovery_mode', 'surfaces', 'permissions', 'cwd', 'transport', 'cli_identity'] as const
483
+ ? ['timeout_ms', 'instruction', 'cli', 'model', 'recovery_mode', 'surfaces', 'permissions', 'cwd', 'resume', 'transport', 'cli_identity'] as const
483
484
  : [];
484
485
  assertKernelKeys(step, [...commonKeys, ...typeKeys], at);
485
486
  const transportRetries = step['retry'] === undefined
@@ -516,7 +517,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
516
517
  instruction: step['instruction'],
517
518
  ...(step['timeout_ms'] === undefined ? {} : { timeoutMs: step['timeout_ms'] }),
518
519
  ...(step['recovery_mode'] !== undefined ? { recoveryMode: step['recovery_mode'] } : {}),
519
- ...copyDefined(step, ['cli', 'model', 'surfaces', 'cwd', 'transport']),
520
+ ...copyDefined(step, ['cli', 'model', 'surfaces', 'cwd', 'resume', 'transport']),
520
521
  ...(step['permissions'] !== undefined
521
522
  ? { permissions: kernelPermissionsToAuthoring(step['permissions'], `${at}.permissions`) }
522
523
  : {}),
@@ -691,6 +692,7 @@ function toKernelStep(step: StepSpec, cliIdentity?: string): ResolvedKernelStepS
691
692
  ...(step.cli !== undefined ? { cli: step.cli } : {}),
692
693
  ...(step.model !== undefined ? { model: step.model } : {}),
693
694
  ...(step.cwd !== undefined ? { cwd: step.cwd } : {}),
695
+ ...(step.resume !== undefined ? { resume: step.resume } : {}),
694
696
  ...(step.transport !== undefined ? { transport: step.transport } : {}),
695
697
  recovery_mode: step.recoveryMode ?? 'reset',
696
698
  };
@@ -0,0 +1,126 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { canonicalize } from './canonical.js';
3
+ import { CompileError, toKernelSpec } from './compile.js';
4
+ import { PROTOCOL_VERSION } from './protocol.js';
5
+ import { RelayflowdNotFoundError, resolveRelayflowdBinary } from './relayflowd-path.js';
6
+ import type { FlowSpec } from './spec.js';
7
+ import type { CheckReport } from './cli/check.js';
8
+
9
+ export type DaemonValidationMode = 'auto' | 'required' | 'off';
10
+ type Unavailable = 'disabled' | 'not_reached' | 'authored_body' | 'relayflowd_not_found'
11
+ | 'relayflowd_bin_invalid' | 'no_verdict' | 'spawn_failed' | 'timed_out' | 'protocol_mismatch' | 'abnormal_exit';
12
+ export interface DaemonValidation {
13
+ mode: 'daemon' | 'local';
14
+ binary?: string;
15
+ protocol?: number;
16
+ specVersion?: string;
17
+ reason?: Unavailable;
18
+ }
19
+ export interface DaemonValidationDeps {
20
+ resolveBinary(): string;
21
+ run(binary: string, input: string): {
22
+ status: number | null; signal?: NodeJS.Signals | null; stdout: string; stderr: string; error?: Error;
23
+ };
24
+ }
25
+ type Result = { validation: DaemonValidation; diagnostics: CheckReport['diagnostics'] };
26
+ // Cache discovery, never verdicts. Changed resolution inputs get a fresh lookup.
27
+ const binaries = new Map<string, string | Error>();
28
+ const defaults: DaemonValidationDeps = {
29
+ resolveBinary() {
30
+ const key = JSON.stringify([process.env['RELAYFLOWD_BIN'], process.env['PATH'], process.cwd(), process.argv[1]]);
31
+ let found = binaries.get(key);
32
+ if (found === undefined) {
33
+ try { found = resolveRelayflowdBinary(); }
34
+ catch (error) { found = error instanceof Error ? error : new Error(String(error)); }
35
+ binaries.set(key, found);
36
+ }
37
+ if (found instanceof Error) throw found;
38
+ return found;
39
+ },
40
+ run: (binary, input) => spawnSync(binary, ['validate-spec'], {
41
+ input, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024,
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ }),
44
+ };
45
+
46
+ export function unavailableValidation(
47
+ mode: DaemonValidationMode, reason: Unavailable, detail = '', binary?: string,
48
+ ): Result {
49
+ const message = `${binary === undefined ? 'relayflowd' : `relayflowd at ${binary}`}: ${detail || reason.replaceAll('_', ' ')}. `
50
+ + 'Daemon acceptance is unproven; an already attached daemon from another build is not checked.';
51
+ return {
52
+ validation: { mode: 'local', reason, ...(binary === undefined ? {} : { binary }) },
53
+ diagnostics: mode === 'required'
54
+ ? [{ severity: 'refusal', kind: 'daemon_validation_unavailable', message }]
55
+ : reason === 'disabled' || reason === 'not_reached' || reason === 'relayflowd_not_found'
56
+ ? [] : [{ severity: 'warning', kind: 'daemon_unvalidated', message }],
57
+ };
58
+ }
59
+
60
+ /** Same spec value run.start submits, in canonical form; no socket or run. */
61
+ export function validateSpecWithDaemon(
62
+ flow: FlowSpec, mode: DaemonValidationMode, deps: DaemonValidationDeps = defaults,
63
+ ): Result {
64
+ if (mode === 'off') return unavailableValidation(mode, 'disabled');
65
+ let submitted: ReturnType<typeof toKernelSpec>;
66
+ let input: string;
67
+ try { submitted = toKernelSpec(flow); input = canonicalize(submitted); }
68
+ catch (error) {
69
+ // run refuses this lowering too: degrading to local would fail open.
70
+ const errors = error instanceof CompileError ? error.errors : [String(error)];
71
+ return { validation: { mode: 'local', reason: 'not_reached' },
72
+ diagnostics: [{ severity: 'refusal', kind: 'invalid_spec', message: errors.join('; '), errors }] };
73
+ }
74
+ let binary: string;
75
+ try { binary = deps.resolveBinary(); }
76
+ catch (error) {
77
+ const reason = error instanceof RelayflowdNotFoundError
78
+ ? error.attempts[0]?.startsWith('RELAYFLOWD_BIN=') ? 'relayflowd_bin_invalid' : 'relayflowd_not_found'
79
+ : 'spawn_failed';
80
+ return unavailableValidation(mode, reason, String(error));
81
+ }
82
+ try {
83
+ const result = deps.run(binary, input);
84
+ if (result.error) return unavailableValidation(mode,
85
+ (result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT' ? 'timed_out' : 'spawn_failed', result.error.message, binary);
86
+ let verdict: unknown;
87
+ try { verdict = JSON.parse(result.stdout); } catch { /* Old binaries have no envelope. */ }
88
+ if (!isVerdict(verdict)) return unavailableValidation(mode, 'no_verdict',
89
+ 'no validation verdict (the binary may predate validate-spec)', binary);
90
+ if (verdict.protocol !== PROTOCOL_VERSION) return unavailableValidation(mode, 'protocol_mismatch',
91
+ `validator protocol ${verdict.protocol}, SDK protocol ${PROTOCOL_VERSION}`, binary);
92
+ const validation: DaemonValidation = { mode: 'daemon', binary,
93
+ protocol: verdict.protocol, specVersion: verdict.spec_version };
94
+ if (verdict.ok) {
95
+ // Acceptance is the envelope AND exit 0; a validator that fails or is
96
+ // killed after printing it has not completed validation.
97
+ if (result.status !== 0) return unavailableValidation(mode, 'abnormal_exit',
98
+ `validator ${result.status === null ? `was terminated by ${result.signal ?? 'a signal'}` : `exited ${result.status}`} after an accepting verdict`, binary);
99
+ return { validation, diagnostics: [] };
100
+ }
101
+ // A refusal fails closed whatever the exit status, so it is reported as-is.
102
+ const message = verdict.error.message;
103
+ const index = /steps\[(\d+)\]/.exec(message)?.[1];
104
+ const stepId = index === undefined ? undefined : submitted.steps[Number(index)]?.id;
105
+ const generated = stepId !== undefined && !flow.steps.some(step => step.id === stepId);
106
+ return { validation, diagnostics: [{ severity: 'refusal', kind: 'invalid_spec',
107
+ message: message + (generated ? `\nStep "${stepId}" is generated by this compiler's gate lowering, not declared in the flow; `
108
+ + 'the installed relayflowd and this @relayflows/sdk disagree (SDK lowering bug). Report this rather than editing the flow.' : ''),
109
+ errors: [message], ...(stepId === undefined ? {} : { stepId }) }] };
110
+ } catch (error) { return unavailableValidation(mode, 'spawn_failed', String(error), binary); }
111
+ }
112
+
113
+ type Verdict = { protocol: number; spec_version: string } &
114
+ ({ ok: true } | { ok: false; error: { code: 'invalid_spec'; message: string } });
115
+ function isVerdict(value: unknown): value is Verdict {
116
+ if (typeof value !== 'object' || value === null) return false;
117
+ const v = value as Partial<Verdict>;
118
+ return Number.isInteger(v.protocol) && typeof v.spec_version === 'string'
119
+ && (v.ok === true || (v.ok === false && v.error?.code === 'invalid_spec' && typeof v.error.message === 'string'));
120
+ }
121
+
122
+ export function validationAnnotation(validation?: DaemonValidation): string {
123
+ return validation?.mode === 'daemon'
124
+ ? ` [validated by relayflowd at ${validation.binary}, protocol ${validation.protocol}]`
125
+ : ` [local only: ${(validation?.reason ?? 'not_reached').replaceAll('_', ' ')}, so daemon acceptance is unproven]`;
126
+ }
@@ -48,7 +48,9 @@ export const CHECK_INPUT_FAILURE_KINDS = [
48
48
  ...SHARED_SPEC_FAILURE_KINDS,
49
49
  ] as const;
50
50
 
51
+ // run never enables daemon validation; fromCheckReport needs a mapping if that changes.
51
52
  export const CHECK_FAILURE_KINDS = [
53
+ 'daemon_validation_unavailable',
52
54
  ...CHECK_INPUT_FAILURE_KINDS,
53
55
  ...PREFLIGHT_ENVIRONMENT_FAILURE_KINDS,
54
56
  ] as const;
@@ -136,6 +138,14 @@ export const RUN_FAILURE_KINDS = [
136
138
  'gate_failed',
137
139
  /** `flows answer` named a wait the run is not asking: unknown, or already answered. */
138
140
  'human_wait_unknown',
141
+ /** `f.memory.learn` was given a finding without non-empty string fields. */
142
+ 'memory_finding_invalid',
143
+ /** `f.memory.learn` could not persist the finding to script memory. */
144
+ 'memory_unwritable',
145
+ /** `f.memory.learn` ran without a durable root run to journal the finding in. */
146
+ 'memory_unjournaled',
147
+ /** `f.memory` lost its database, or found a damaged learned finding, after the run started. */
148
+ 'memory_unreachable',
139
149
  /**
140
150
  * `--local-agent` cannot be honoured for this invocation, so it is refused
141
151
  * rather than accepted and ignored. A local agent worker is admitted at run
@@ -149,6 +159,7 @@ export const RUN_FAILURE_KINDS = [
149
159
  * reach this process. The run is NOT failed (`status: running`): its
150
160
  * completed steps are journaled and `flows resume <rootRunId>` continues it.
151
161
  */
162
+ 'daemon_unresponsive',
152
163
  'root_lease_lost',
153
164
  ] as const;
154
165
 
@@ -160,6 +171,9 @@ export const RUN_FAILURE_KINDS = [
160
171
  */
161
172
  export const RUN_WARNING_KINDS = [
162
173
  'connection_file_stale',
174
+ 'reuse_summary_unavailable',
175
+ 'result_unreadable',
176
+ 'inspection_interrupted',
163
177
  ] as const;
164
178
 
165
179
  /**
@@ -172,7 +186,9 @@ export const RUN_WARNING_KINDS = [
172
186
  * daemon-free, can see none either — so it opts in, while `flows run`, `flows
173
187
  * build` and SDK submissions do not (cli/check-worker-surface.ts).
174
188
  */
175
- export const CHECK_WARNING_KINDS = ['editor_schema_missing', 'agent_worker_unresolved'] as const;
189
+ /** helper_credential_unresolved describes a local mount gap during inspection;
190
+ * Cloud validates workspace integrations at submit, and local execution refuses. */
191
+ export const CHECK_WARNING_KINDS = ['editor_schema_missing', 'daemon_unvalidated', 'agent_worker_unresolved', 'helper_credential_unresolved'] as const;
176
192
  export type CheckWarningKind = (typeof CHECK_WARNING_KINDS)[number];
177
193
 
178
194
  export type PreflightFailureKind = (typeof PREFLIGHT_FAILURE_KINDS)[number];
@@ -0,0 +1,114 @@
1
+ // The Cloud executor for `evaluateFlow`: one fresh hosted run per case,
2
+ // waited on, with its verdict read from the hosted run report and its spend
3
+ // from the run's steps.
4
+
5
+ import { getCloudRunSteps, type CloudStep } from './cloud-read.js';
6
+ import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from './cloud-run.js';
7
+ import { cloudRunState } from './cloud-run-record.js';
8
+ import { cloudRequest, isCloudRecord, type CloudConnectionOptions } from './cloud-http.js';
9
+ import { isAuthoredFlowPath } from './direct-input.js';
10
+ import type { FlowEvalExecutor, FlowEvalRun } from './flow-eval-report.js';
11
+ import { runReportOutcome } from './flow-eval-outcome.js';
12
+ import { flowEvalSources, isPathTarget, type FlowEvalTarget } from './flow-eval-version.js';
13
+
14
+ const MODULE = /\.(?:[mc]?[jt]s)$/u;
15
+ const EXTENSION_STORE = /(?:^|\/)\.flows\/plugins\//u;
16
+ /** Manifests and locks the version pins but Cloud does not need as imports. */
17
+ const PROJECT_METADATA = /(?:^|\/)(?:package(?:-lock)?|flows(?:\.lock)?)\.json$/u;
18
+
19
+ /**
20
+ * Cloud receives the entry file's source alone (cloud-run.ts): a relative
21
+ * import is not resolved by the hosted runner, even with `syncCode`. A flow
22
+ * whose version spans local modules would therefore fail only on Cloud, or run
23
+ * something other than what was judged. Refuse it before submitting anything.
24
+ */
25
+ async function refuseLocalImports(flow: FlowEvalTarget): Promise<void> {
26
+ if (!isPathTarget(flow) || !MODULE.test(flow.path)) return;
27
+ const { files } = await flowEvalSources(flow);
28
+ const entry = files.find(file => !file.includes('/') && flow.path.endsWith(file));
29
+ // Locked extension payloads are not imports: Cloud receives them separately
30
+ // (`extensions` in the submission), so they never make a flow non-self-contained.
31
+ const modules = files.filter(file => (MODULE.test(file) || /\.json$/u.test(file)) && file !== entry
32
+ && !EXTENSION_STORE.test(file) && !PROJECT_METADATA.test(file));
33
+ if (modules.length > 0) {
34
+ throw new Error(`Cloud evaluation runs the entry file alone, and this flow imports local modules (${modules.join(', ')}). `
35
+ + 'Evaluate it locally, or inline those modules.');
36
+ }
37
+ }
38
+
39
+ export interface CloudFlowEvalExecutorOptions extends CloudConnectionOptions {
40
+ workspaceId?: string;
41
+ syncCode?: RunInCloudOptions['syncCode'];
42
+ pollIntervalMs?: number;
43
+ }
44
+
45
+ /**
46
+ * Execute each case as a fresh hosted run and wait for it. Needs a token with
47
+ * `workflow:invoke:write` and `runs:read`; inside a Cloud step that means an
48
+ * injected workspace token, since a run-scoped sandbox token reads only its own run.
49
+ */
50
+ export function cloudFlowEvalExecutor(options: CloudFlowEvalExecutorOptions = {}): FlowEvalExecutor {
51
+ return async ({ flow, input, signal }) => {
52
+ await refuseLocalImports(flow);
53
+ const connection: CloudConnectionOptions = {
54
+ ...(options.apiUrl === undefined ? {} : { apiUrl: options.apiUrl }),
55
+ ...(options.token === undefined ? {} : { token: options.token }),
56
+ ...(options.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: options.requestTimeoutMs }),
57
+ ...(signal === undefined ? {} : { signal }),
58
+ };
59
+ const receipt = await runInCloud(flow, {
60
+ ...connection,
61
+ ...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
62
+ ...(options.syncCode === undefined ? {} : { syncCode: options.syncCode }),
63
+ // Cloud requires an explicit input for authored source; `{}` is the
64
+ // documented "no fields", matching the local executor's default.
65
+ ...(input !== undefined ? { input } : 'path' in flow && isAuthoredFlowPath(flow.path) ? { input: {} } : {}),
66
+ });
67
+ await waitForCloudFlowRun(receipt.runId, {
68
+ ...connection, ...(options.pollIntervalMs === undefined ? {} : { pollIntervalMs: options.pollIntervalMs }),
69
+ });
70
+ // Re-read the terminal record for its stored report: the run state alone
71
+ // carries the kernel's reason, which says `success` for a declined body
72
+ // and has no completion detail.
73
+ const record = await cloudRequest(`/api/v1/workflows/runs/${receipt.runId}`, connection);
74
+ let steps: CloudStep[] | undefined;
75
+ try {
76
+ steps = await getCloudRunSteps(receipt.runId, connection);
77
+ } catch {
78
+ // As locally: spend is evidence, not the verdict.
79
+ }
80
+ return { runId: receipt.runId, ...cloudRunOutcome(record, receipt.runId), ...cloudSpend(steps) };
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Map a terminal hosted run record onto the eval vocabulary: the validated run
86
+ * state decides parked/failed/canceled, and the stored report (the same
87
+ * RunReport a local run prints) supplies `declined` and the completion detail.
88
+ * Exported for tests.
89
+ */
90
+ export function cloudRunOutcome(record: unknown, runId: string): Pick<FlowEvalRun, 'completionReason' | 'completionDetail'> {
91
+ const state = cloudRunState(record, runId);
92
+ const report = isCloudRecord(record) && isCloudRecord(record.result) ? record.result : {};
93
+ const mapped = runReportOutcome(report);
94
+ // A park with nothing attached to run it is the same fact on both sides.
95
+ if (mapped.completionReason === 'worker_unavailable') return mapped;
96
+ if (state.status === 'needs_human') return { ...mapped, completionReason: 'needs_human' };
97
+ if (!('completionReason' in state)) return { ...mapped, completionReason: state.status };
98
+ if (state.completionReason === 'success') return mapped.completionReason === 'declined' ? mapped : { ...mapped, completionReason: 'success' };
99
+ return { ...mapped, completionReason: state.completionReason };
100
+ }
101
+
102
+ /** Sum hosted step spend; any step with unknown cost makes the total unknown. Exported for tests. */
103
+ export function cloudSpend(steps: readonly CloudStep[] | undefined): Pick<FlowEvalRun, 'costUsd' | 'tokensIn' | 'tokensOut' | 'steps'> {
104
+ if (steps === undefined) return { costUsd: null, tokensIn: null, tokensOut: null };
105
+ const known = (values: Array<number | null>): number | null =>
106
+ values.some(v => v === null || !Number.isFinite(v) || v < 0) ? null : values.reduce<number>((sum, v) => sum + v!, 0);
107
+ const costed = steps.filter(step => step.step_type === 'agent' || step.step_type === 'llm' || step.cost_usd !== null);
108
+ return {
109
+ costUsd: known(costed.map(step => step.cost_usd)),
110
+ tokensIn: known(costed.map(step => step.tokens_in)),
111
+ tokensOut: known(costed.map(step => step.tokens_out)),
112
+ steps: steps.map(step => ({ id: step.step_name, status: step.status, durationMs: step.duration_ms, costUsd: step.cost_usd })),
113
+ };
114
+ }
@@ -0,0 +1,10 @@
1
+ export type FlowEvalErrorCode =
2
+ | 'invalid_suite' | 'invalid_baseline' | 'version_mismatch' | 'suite_mismatch' | 'invalid_options' | 'unreadable_flow';
3
+
4
+ /** The evaluation itself is invalid. Case failures are verdicts in the report, never this. */
5
+ export class FlowEvalError extends Error {
6
+ constructor(readonly code: FlowEvalErrorCode, message: string) {
7
+ super(message);
8
+ this.name = 'FlowEvalError';
9
+ }
10
+ }
@@ -0,0 +1,114 @@
1
+ // The verdict of a flow evaluation: case results aggregated into a summary,
2
+ // and the summary judged against the suite's thresholds and a baseline.
3
+
4
+ import { FlowEvalError } from './flow-eval-error.js';
5
+ import type { FlowEvalCaseResult, FlowEvalOutcome, FlowEvalReport } from './flow-eval-report.js';
6
+ import { isRecord, type FlowEvalSuite } from './flow-eval-suite.js';
7
+
8
+ /** The part of a prior report a regression check reads, validated. */
9
+ export interface FlowEvalBaseline {
10
+ suiteSha256: string;
11
+ outcomes: ReadonlyMap<string, FlowEvalOutcome>;
12
+ }
13
+
14
+ const OUTCOMES: readonly string[] = ['pass', 'fail', 'error'];
15
+
16
+ /**
17
+ * Validate an untrusted prior report (parsed JSON) before any case runs, so a
18
+ * malformed baseline is refused before an evaluation spends anything.
19
+ */
20
+ export function parseFlowEvalBaseline(value: unknown): FlowEvalBaseline {
21
+ const fail = (message: string): never => { throw new FlowEvalError('invalid_baseline', message); };
22
+ if (!isRecord(value) || value['kind'] !== 'flows.eval.report') fail('Baseline is not a flows eval report.');
23
+ const report = value as Record<string, unknown>;
24
+ const suite = report['suite'];
25
+ if (!isRecord(suite) || typeof suite['sha256'] !== 'string') fail('Baseline report names no suite sha256.');
26
+ if (!Array.isArray(report['cases'])) fail('Baseline report has no `cases` array.');
27
+ const outcomes = new Map<string, FlowEvalOutcome>();
28
+ for (const [index, entry] of (report['cases'] as unknown[]).entries()) {
29
+ if (!isRecord(entry) || typeof entry['id'] !== 'string' || !OUTCOMES.includes(entry['outcome'] as string)) {
30
+ fail(`Baseline cases[${index}] needs a string \`id\` and an \`outcome\` of pass, fail or error.`);
31
+ }
32
+ const item = entry as Record<string, unknown>;
33
+ if (outcomes.has(item['id'] as string)) fail(`Baseline case id "${item['id']}" is not unique.`);
34
+ outcomes.set(item['id'] as string, item['outcome'] as FlowEvalOutcome);
35
+ }
36
+ return { suiteSha256: (suite as Record<string, unknown>)['sha256'] as string, outcomes };
37
+ }
38
+
39
+ export function summarizeFlowEval(results: readonly FlowEvalCaseResult[]): FlowEvalReport['summary'] {
40
+ const passed = results.filter(r => r.outcome === 'pass').length;
41
+ const failed = results.filter(r => r.outcome === 'fail').length;
42
+ const errored = results.filter(r => r.outcome === 'error').length;
43
+ const costs = results.map(r => r.costUsd);
44
+ const latencies = results.map(r => r.latencyMs).sort((a, b) => a - b);
45
+ return {
46
+ total: results.length, passed, failed, errored,
47
+ passRate: results.length === 0 ? 0 : passed / results.length,
48
+ totalCostUsd: costs.some(c => c === null) ? null : fromNanos(sumNanos(costs as number[])),
49
+ latencyMs: { p50: percentile(latencies, 0.5), p95: percentile(latencies, 0.95), max: latencies.at(-1) ?? 0 },
50
+ };
51
+ }
52
+
53
+ export function decideFlowEvalGate(
54
+ suite: FlowEvalSuite,
55
+ results: readonly FlowEvalCaseResult[],
56
+ summary: FlowEvalReport['summary'],
57
+ baseline: FlowEvalBaseline | undefined,
58
+ ): FlowEvalReport['gate'] {
59
+ const reasons: string[] = [];
60
+ const thresholds = suite.thresholds ?? {};
61
+ const minPassRate = thresholds.minPassRate ?? 1;
62
+ if (summary.passRate < minPassRate) {
63
+ reasons.push(`pass rate ${formatRate(summary.passRate)} is below ${formatRate(minPassRate)} (${summary.passed}/${summary.total})`);
64
+ }
65
+ // Fails closed regardless of minPassRate: an errored case was never judged,
66
+ // so a tolerance for quality misses must not absorb it.
67
+ if (summary.errored > 0) reasons.push(`${summary.errored} case(s) errored before a verdict`);
68
+ if (thresholds.maxTotalCostUsd !== undefined) {
69
+ // Decimal, not binary: costs and the ceiling are compared in whole
70
+ // nanodollars, so 0.1 + 0.2 meets a 0.3 ceiling exactly and a total a
71
+ // fraction of a cent over it still fails. No display rounding is involved.
72
+ const costs = results.map(r => r.costUsd);
73
+ if (costs.some(c => c === null)) reasons.push(`total cost is unknown; the suite caps it at $${thresholds.maxTotalCostUsd}`);
74
+ else {
75
+ const total = sumNanos(costs as number[]);
76
+ if (total > toNanos(thresholds.maxTotalCostUsd)) {
77
+ reasons.push(`total cost $${fromNanos(total)} exceeds $${thresholds.maxTotalCostUsd}`);
78
+ }
79
+ }
80
+ }
81
+ if (thresholds.maxP95LatencyMs !== undefined && summary.latencyMs.p95 > thresholds.maxP95LatencyMs) {
82
+ reasons.push(`p95 latency ${summary.latencyMs.p95}ms exceeds ${thresholds.maxP95LatencyMs}ms`);
83
+ }
84
+ const regressions: string[] = [];
85
+ if (baseline !== undefined) {
86
+ for (const result of results) {
87
+ if (baseline.outcomes.get(result.id) === 'pass' && result.outcome !== 'pass') regressions.push(result.id);
88
+ }
89
+ if (regressions.length > 0) reasons.push(`${regressions.length} case(s) regressed against the baseline: ${regressions.join(', ')}`);
90
+ }
91
+ return { pass: reasons.length === 0, reasons, regressions };
92
+ }
93
+
94
+ function percentile(sorted: readonly number[], p: number): number {
95
+ if (sorted.length === 0) return 0;
96
+ return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1))]!;
97
+ }
98
+
99
+ /** Dollars as whole nanodollars: exact for any cost a run reports, far below a cent. */
100
+ function toNanos(usd: number): bigint {
101
+ return BigInt(Math.round(usd * 1e9));
102
+ }
103
+
104
+ function sumNanos(costs: readonly number[]): bigint {
105
+ return costs.reduce((sum, cost) => sum + toNanos(cost), 0n);
106
+ }
107
+
108
+ function fromNanos(nanos: bigint): number {
109
+ return Number(nanos) / 1e9;
110
+ }
111
+
112
+ function formatRate(rate: number): string {
113
+ return `${Math.round(rate * 1000) / 10}%`;
114
+ }