@jr2/orchestrator 0.3.0 → 0.5.1

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 (354) hide show
  1. package/README.md +20 -2
  2. package/bin/server.ts +7 -4
  3. package/console/canvas.ts +27 -14
  4. package/console/components/app.ts +12 -1
  5. package/console/components/fleet.ts +52 -6
  6. package/console/components/machine-pane.ts +128 -2
  7. package/console/components/nav.ts +1 -1
  8. package/console/components/schema-form.ts +12 -4
  9. package/console/json-field.ts +23 -0
  10. package/console/main.ts +122 -39
  11. package/console/store.ts +321 -21
  12. package/console/style.css +111 -0
  13. package/docs/agents-block.md +5 -0
  14. package/docs/concepts/agents-and-turns.md +442 -0
  15. package/docs/concepts/architecture.md +230 -0
  16. package/docs/concepts/charts.md +143 -0
  17. package/docs/concepts/composition.md +357 -0
  18. package/docs/concepts/converge.md +254 -0
  19. package/docs/concepts/durable-runs.md +189 -0
  20. package/docs/concepts/effects.md +603 -0
  21. package/docs/concepts/gates.md +208 -0
  22. package/docs/concepts/machines-and-workflows.md +289 -0
  23. package/docs/concepts/pools-and-sources.md +230 -0
  24. package/docs/concepts/secrets-and-trust.md +222 -0
  25. package/docs/concepts/the-menu.md +378 -0
  26. package/docs/concepts/workspaces.md +678 -0
  27. package/docs/get-started/a-model-decides.md +563 -0
  28. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  29. package/docs/get-started/connect-a-model.md +248 -0
  30. package/docs/get-started/create-an-instance.md +194 -0
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  32. package/docs/get-started/install.md +126 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  34. package/docs/guides/answer-a-gate.md +304 -0
  35. package/docs/guides/bind-open-parts.md +223 -0
  36. package/docs/guides/cancel-a-run.md +146 -0
  37. package/docs/guides/clean-up.md +206 -0
  38. package/docs/guides/fix-a-failed-converge.md +361 -0
  39. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  41. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  42. package/docs/guides/look-inside-a-sandbox.md +174 -0
  43. package/docs/guides/mirror-the-kit-images.md +176 -0
  44. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  45. package/docs/guides/open-the-console.md +91 -0
  46. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  47. package/docs/guides/push-and-sign-a-branch.md +486 -0
  48. package/docs/guides/run-a-chart-by-name.md +270 -0
  49. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  50. package/docs/guides/run-one-worker-per-item.md +321 -0
  51. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  52. package/docs/guides/size-a-workspace.md +204 -0
  53. package/docs/guides/start-a-run-from-a-run.md +237 -0
  54. package/docs/guides/start-a-run.md +207 -0
  55. package/docs/guides/upgrade-the-kit-version.md +240 -0
  56. package/docs/guides/use-a-hosted-provider.md +208 -0
  57. package/docs/guides/use-a-local-model.md +313 -0
  58. package/docs/guides/watch-a-run.md +326 -0
  59. package/docs/guides/write-a-workflow.md +318 -0
  60. package/docs/index.md +44 -0
  61. package/docs/llms.txt +216 -0
  62. package/docs/reference/authoring-surface/agent.md +128 -0
  63. package/docs/reference/authoring-surface/answer.md +140 -0
  64. package/docs/reference/authoring-surface/arg.md +60 -0
  65. package/docs/reference/authoring-surface/basic.md +62 -0
  66. package/docs/reference/authoring-surface/bearer.md +53 -0
  67. package/docs/reference/authoring-surface/chartschema.md +23 -0
  68. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  69. package/docs/reference/authoring-surface/command.md +191 -0
  70. package/docs/reference/authoring-surface/customize.md +64 -0
  71. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  72. package/docs/reference/authoring-surface/defineevent.md +114 -0
  73. package/docs/reference/authoring-surface/dispatch.md +126 -0
  74. package/docs/reference/authoring-surface/fail.md +83 -0
  75. package/docs/reference/authoring-surface/file.md +122 -0
  76. package/docs/reference/authoring-surface/frompick.md +277 -0
  77. package/docs/reference/authoring-surface/http.md +159 -0
  78. package/docs/reference/authoring-surface/isfailure.md +58 -0
  79. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  80. package/docs/reference/authoring-surface/open.md +74 -0
  81. package/docs/reference/authoring-surface/openbody.md +86 -0
  82. package/docs/reference/authoring-surface/paramsof.md +125 -0
  83. package/docs/reference/authoring-surface/pool.md +186 -0
  84. package/docs/reference/authoring-surface/push.md +112 -0
  85. package/docs/reference/authoring-surface/secret.md +118 -0
  86. package/docs/reference/authoring-surface/sentby.md +83 -0
  87. package/docs/reference/authoring-surface/source.md +51 -0
  88. package/docs/reference/authoring-surface/text.md +62 -0
  89. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  90. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  91. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  92. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  93. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  94. package/docs/reference/authoring-surface/type-arg.md +19 -0
  95. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  96. package/docs/reference/authoring-surface/type-binding.md +62 -0
  97. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  98. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  99. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  100. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  101. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  102. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  103. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  104. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  105. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  106. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  107. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  108. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  109. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  110. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  111. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  112. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  113. package/docs/reference/authoring-surface/type-childat.md +12 -0
  114. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  115. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  116. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  117. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  118. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  119. package/docs/reference/authoring-surface/type-customize.md +109 -0
  120. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  121. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  122. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  124. package/docs/reference/authoring-surface/type-duration.md +15 -0
  125. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  126. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  127. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  128. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  129. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  130. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  131. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  132. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  133. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  134. package/docs/reference/authoring-surface/type-failure.md +19 -0
  135. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  136. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  137. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  138. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  139. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  140. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  141. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  142. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  143. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  144. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  145. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  146. package/docs/reference/authoring-surface/type-lends.md +38 -0
  147. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  148. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  149. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  150. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  151. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  152. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  153. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  154. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  155. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  156. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  157. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  158. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  159. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  160. package/docs/reference/authoring-surface/type-openof.md +12 -0
  161. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  162. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  163. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  164. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  165. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  166. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  167. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  168. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  169. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  170. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  171. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  172. package/docs/reference/authoring-surface/type-reached.md +12 -0
  173. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  174. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  175. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  176. package/docs/reference/authoring-surface/type-secret.md +25 -0
  177. package/docs/reference/authoring-surface/type-sender.md +19 -0
  178. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  179. package/docs/reference/authoring-surface/type-size.md +85 -0
  180. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  181. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  182. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  183. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  184. package/docs/reference/authoring-surface/type-text.md +19 -0
  185. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  186. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  187. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  188. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  189. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  190. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  191. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  192. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  193. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  194. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  195. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  196. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  197. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  198. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  199. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  200. package/docs/reference/authoring-surface/workspace.md +259 -0
  201. package/docs/reference/authoring-surface.md +197 -0
  202. package/docs/reference/cli.md +433 -0
  203. package/docs/reference/config.md +756 -0
  204. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  205. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  206. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  207. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  208. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  209. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  210. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  211. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  212. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  213. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  214. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  215. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  216. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  217. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  218. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  219. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  220. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  221. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  222. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  223. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  224. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  225. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  226. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  227. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  228. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  229. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  230. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  231. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  232. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  233. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  234. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  235. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  236. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  237. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  238. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  239. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  240. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  241. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  242. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  243. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  244. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  245. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  246. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  247. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  248. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  249. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  250. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  251. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  252. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  253. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  254. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  255. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  256. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  257. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  258. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  259. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  260. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  261. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  262. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  263. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  264. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  265. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  266. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  267. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
  268. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  269. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  270. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  271. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  272. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  273. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  274. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  275. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  276. package/docs/reference/decisions.md +83 -0
  277. package/docs/reference/glossary.md +648 -0
  278. package/docs/reference/http-api.md +1348 -0
  279. package/docs/reference/machines.md +190 -0
  280. package/docs/reference/mechanism-events.md +72 -0
  281. package/docs/why.md +206 -0
  282. package/package.json +6 -4
  283. package/src/actor.ts +976 -61
  284. package/src/agent.ts +114 -38
  285. package/src/ambient.ts +4 -1
  286. package/src/assembly-checks.ts +347 -0
  287. package/src/assembly-tar.ts +494 -0
  288. package/src/assembly.ts +721 -0
  289. package/src/cause.ts +234 -0
  290. package/src/chart.ts +2182 -0
  291. package/src/charts.ts +748 -0
  292. package/src/config.ts +520 -162
  293. package/src/content-hash.ts +105 -0
  294. package/src/custodian.lua +6 -5
  295. package/src/custodian.ts +143 -39
  296. package/src/customize.ts +388 -55
  297. package/src/dispatch-effect.ts +945 -0
  298. package/src/door-compat.ts +217 -0
  299. package/src/durability.ts +11 -4
  300. package/src/effect-checks.ts +557 -0
  301. package/src/effect.ts +1202 -0
  302. package/src/effector-wire.ts +674 -0
  303. package/src/fail.ts +209 -0
  304. package/src/fingerprint.ts +4 -3
  305. package/src/gate.ts +8 -0
  306. package/src/git-clone.ts +65 -0
  307. package/src/git-push.ts +336 -0
  308. package/src/harness-client.ts +764 -48
  309. package/src/held-secrets.ts +82 -26
  310. package/src/holds.ts +156 -0
  311. package/src/http-effect.ts +1423 -0
  312. package/src/http.ts +916 -113
  313. package/src/images.ts +47 -21
  314. package/src/index.ts +62 -43
  315. package/src/instance.ts +505 -59
  316. package/src/internal.ts +84 -0
  317. package/src/kube-client.ts +333 -0
  318. package/src/machine-doc.ts +106 -6
  319. package/src/mechanism-routes.ts +168 -0
  320. package/src/model-keys.generated.ts +53 -0
  321. package/src/model-keys.ts +18 -58
  322. package/src/mount-digest.ts +53 -0
  323. package/src/names.ts +66 -8
  324. package/src/notices.ts +141 -0
  325. package/src/open-body.ts +1580 -0
  326. package/src/open-effect.ts +217 -0
  327. package/src/open.ts +59 -6
  328. package/src/parts.ts +709 -41
  329. package/src/party.ts +534 -0
  330. package/src/pick.ts +761 -0
  331. package/src/pool.ts +248 -37
  332. package/src/registration.ts +382 -19
  333. package/src/repo-fetch.ts +60 -85
  334. package/src/repos.ts +65 -75
  335. package/src/rest.ts +133 -0
  336. package/src/run-host.ts +2383 -115
  337. package/src/run-output.ts +38 -0
  338. package/src/sandbox-effect.ts +1126 -0
  339. package/src/sandbox-effector.ts +111 -0
  340. package/src/sandbox-kube.ts +1721 -0
  341. package/src/sandbox-watch.ts +260 -0
  342. package/src/secret-placement.ts +529 -0
  343. package/src/server.ts +284 -84
  344. package/src/setup.ts +819 -117
  345. package/src/size.ts +454 -0
  346. package/src/snapshot-store.ts +115 -20
  347. package/src/stub-harness.ts +49 -16
  348. package/src/sync-client.ts +54 -0
  349. package/src/timers.ts +191 -0
  350. package/src/tokens.ts +35 -4
  351. package/src/vocabulary.ts +135 -15
  352. package/src/wire.ts +238 -10
  353. package/src/workspace.ts +1195 -338
  354. package/src/sandbox-kubectl.ts +0 -1108
package/src/setup.ts CHANGED
@@ -42,22 +42,69 @@ import {
42
42
  type UnknownActorLogic,
43
43
  } from "xstate";
44
44
  import type { z } from "zod";
45
- import { eventMap, type EventDef, type EventFrom } from "@jr2/agent-protocol";
45
+ import { eventMap, type EventDef, type EventFrom, type Sender } from "@jr2/agent-protocol";
46
46
  import type { AgentRunInput, AgentTurnInput, FaultTelemetry } from "./actor.ts";
47
47
  import { isAgent } from "./agent.ts";
48
+ import { isEffect } from "./effect.ts";
49
+ import { effectChecks } from "./effect-checks.ts";
48
50
  import { gate } from "./gate.ts";
51
+ import { attachLent, isOpenBody, openBodyDeclarationOf, type LentParts } from "./open-body.ts";
52
+ import { refuseInlineParts } from "./parts.ts";
53
+ import { partyWarnings, refuseUnknownSentBy, SENT_BY, sentByGuard } from "./party.ts";
54
+ import { refuseLostOutput } from "./run-output.ts";
55
+ import { refuseUnroutedAgentFault } from "./mechanism-routes.ts";
49
56
  import { boundEpoch, boundRunId, continuedIid } from "./registration.ts";
50
- import { attachInputSchema, attachVocabulary } from "./vocabulary.ts";
57
+ import { attachInputSchema, attachLoadWarnings, attachSeats, attachVocabulary } from "./vocabulary.ts";
51
58
 
52
59
  /**
53
- * The events jr2's own mechanism delivers into any workflow machine, injected into every jr2Setup
54
- * union. Dotted names by construction (`NAME_RE` forbids dots in workflow event names), so they
55
- * can never collide with a def.
60
+ * The marker that `compileChart` puts on the config of a Chart's Machine (ADR-0070). In a Chart the
61
+ * `agent.fault` route is optional: the Open body sends an `agent.fault` that no transition takes to
62
+ * the Machine that invoked it, so `createMachine` does not refuse a Chart state with no route. Not on the Authoring
63
+ * surface (index.ts). The load walk (`partsOf`) refuses an author's Machine with no route, with or
64
+ * without the marker (ADR-0074).
56
65
  */
57
- export type MechanismEvent = FaultTelemetry | { type: "workspace.lost" };
66
+ export const COMPILED_CHART: unique symbol = Symbol("jr2.compiledChart");
58
67
 
59
- /** The full event union a jr2Setup machine sees: the defs' derived types plus the mechanism's. */
60
- export type WorkflowEvent<TDefs extends readonly EventDef[]> = EventFrom<TDefs[number]> | MechanismEvent;
68
+ // Injected into every jr2Setup union. Dotted names by construction (`NAME_RE` forbids dots in
69
+ // workflow event names), so they can never collide with a def.
70
+ /** The events that jr2 itself sends into each Machine that `jr2Setup` makes. A Mechanism event's
71
+ * name has a dot, and a workflow event's name cannot, so the two never collide. */
72
+ export type MechanismEvent = FaultTelemetry | WorkspaceLost;
73
+
74
+ // A Workspace's Continuity broke (ADR-0021): its Sandbox is gone, or Lost. `reason` and `message`
75
+ // are for a body whose policy tells the causes apart, or only reports them; no policy needs them.
76
+ /**
77
+ * jr2 sends `workspace.lost` to the body of a `workspace()` when its Sandbox is gone: its pod was
78
+ * evicted or deleted, or its node went. jr2 does not make the Sandbox again, and the commits that
79
+ * were not pushed go with it. The body decides what to do next: route the event with
80
+ * `on: { "workspace.lost": … }`, usually once, on the body's root. `workspace()` refuses a body
81
+ * with a state that is not final and has no route for it on itself or on a state above. When the
82
+ * event comes and no transition takes it (the route's guard is false), the run ends `error`, with
83
+ * the event, the state and the reason in its fault.
84
+ */
85
+ export type WorkspaceLost = {
86
+ type: "workspace.lost";
87
+ /** Why the Sandbox is gone, for a person or a log: the pod's reason (`Evicted`, `NodeShutdown`),
88
+ * the operator's (`PodDeleted`, `NodeLost`), or `Deleted`. A body can act without it. */
89
+ reason?: string;
90
+ /** More text about the loss, when jr2 has it. */
91
+ message?: string;
92
+ };
93
+
94
+ // The defs' derived types plus the mechanism's (ADR-0015).
95
+ /** The type of `event` in a Machine that `jr2Setup` makes: each workflow event in its `events`,
96
+ * and each Mechanism event. A workflow event that an Agent or a Gate sent has `event.sender`. An
97
+ * event that the Machine sent to itself (`raise`, `sendTo`) has no `sender`. */
98
+ export type WorkflowEvent<TDefs extends readonly EventDef[]> =
99
+ | (EventFrom<TDefs[number]> & { readonly sender?: Sender })
100
+ | MechanismEvent;
101
+
102
+ /** The guard jr2Setup registers for every Machine — `sentBy(id)` (ADR-0015) — in the typed guard
103
+ * map, so `sentBy("coder")` and `and([sentBy("coder"), "ready"])` type-check with no cast.
104
+ *
105
+ * @internal
106
+ */
107
+ type SentByGuards = { [SENT_BY]: { id: string } };
61
108
 
62
109
  /** The jr2 actor every workflow can invoke by name without listing it (ADR-0015). `gate` is the
63
110
  * whole set: an Agent is not pre-registered, because it is not one logic — it is the slot the
@@ -65,25 +112,154 @@ export type WorkflowEvent<TDefs extends readonly EventDef[]> = EventFrom<TDefs[n
65
112
  const jr2Actors = { gate };
66
113
  type JR2Actors = typeof jr2Actors;
67
114
 
68
- /** Consumer actors merge OVER the pre-registered set: same name → the consumer's logic wins. */
115
+ /**
116
+ * Consumer actors merge OVER the pre-registered set: same name → the consumer's logic wins.
117
+ *
118
+ * @internal
119
+ */
69
120
  type MergedActors<TActors extends Record<string, UnknownActorLogic>> = Omit<JR2Actors, keyof TActors> & TActors;
70
121
 
71
122
  // Local equivalents of xstate's non-exported setup() mapped helpers (setup.d.ts) — same shapes,
72
123
  // so the declared signature below composes with the public `ActionFunction`/`GuardPredicate`.
124
+ /** @internal */
73
125
  type ToParameterizedObject<T extends Record<string, ParameterizedObject["params"] | undefined>> = {
74
126
  [K in keyof T & string]: { type: K; params: T[K] };
75
127
  }[keyof T & string];
128
+ /** @internal */
76
129
  type ToProvidedActor<TActors extends Record<string, UnknownActorLogic>> = {
77
130
  [K in keyof TActors & string]: { src: K; logic: TActors[K]; id: string | undefined };
78
131
  }[keyof TActors & string];
79
132
 
80
133
  /**
81
- * Author a workflow machine (ADR-0015). Like `setup()`, but `types.events` is gone: the event
82
- * union derives from `events` (defineEvent defs, taken as values) plus the mechanism events.
134
+ * Set up a Machine that takes workflow events: the jr2 form of xstate's `setup()`. Write a Machine
135
+ * with it when an Agent or a Gate sends events into the Machine, or when the Machine declares a
136
+ * door. Call `.createMachine(config)` on the result, as with `setup()`. The Machine you get is a
137
+ * plain xstate Machine.
138
+ *
139
+ * What it adds to `setup()`:
140
+ * - `events` replaces `types.events`. The event type of the Machine comes from the
141
+ * `defineEvent` events you list, plus the events jr2 itself sends (`agent.fault`,
142
+ * `workspace.lost`). An event that a party sent has `event.sender`. A Machine that takes no
143
+ * workflow event leaves out `events`.
144
+ * - The `gate` actor is ready to invoke by name: `invoke: { src: "gate" }`.
145
+ * - Each Agent slot (`actors: { coder: agent(…) }`) gets its Menu from the states that invoke it.
146
+ * - The `sentBy(id)` guard is ready to use.
147
+ * - `createMachine` also takes `input`: a zod object for the input that starts a run. jr2 checks
148
+ * each start against it. Without it, a run can start with any input. The `setup()` of xstate
149
+ * does not read this `input`, so a Machine with a door uses `jr2Setup`, also when it takes no
150
+ * workflow event. `jr2 up` and the publish door refuse a registered Machine that has a zod
151
+ * `input` if `jr2Setup` did not make the Machine: ``workflow "<name>": machine "<id>" has a zod `input` on
152
+ * createMachine, but jr2Setup did not make this Machine, so no door reads that input …``
153
+ *
154
+ * The output of a run is the root `output` of `createMachine`, beside `id` and `states`, as in
155
+ * xstate v5. xstate does not read the `output` of a final state as the output of the Machine. It
156
+ * gives that output to the root `output` as `event.output`. Write
157
+ * `output: ({ event }) => event.output as Output` at the root, or make the output from `context`
158
+ * there. `createMachine` refuses an `output` on a final state directly below the root when the root
159
+ * has no `output` (see the list below).
160
+ *
161
+ * `createMachine` refuses a Machine that it cannot run as written. Among the refusals:
162
+ * - an `on` key that no listed event has: ``machine "<id>" handles event "<key>", which no def in
163
+ * jr2Setup({ events }) declares (declared: <names>) — a typo, or a missing defineEvent``
164
+ * - a listed event that no state takes: ``machine "<id>" declares "<name>", which no state handles
165
+ * — a pick no state takes is on no Menu and no Gate: handle it, or drop it from
166
+ * jr2Setup({ events })``
167
+ * - an entry of `events` that `defineEvent` did not make, or one name listed twice:
168
+ * ``machine "<id>": `events` entry is not a defineEvent() def: <entry>``,
169
+ * ``machine "<id>": duplicate event "<name>" in `events` ``
170
+ * - an `agent()`, an Effect or a `workspace()` written on `invoke.src` instead of under `actors`:
171
+ * ``machine "<id>": state "<state>" invokes <part> inline — a Part is a key of actors, so it has
172
+ * a name that the ledger, the report and customize() share: actors: { <name>: <factory>(...) },
173
+ * then invoke: { src: "<name>" }``. `<part>` is `an Agent`, `an Effect` or `a Workspace`.
174
+ * `<factory>` is `agent`, the Effect's kind (such as `command`), or `workspace`.
175
+ * - an `output` on a final state directly below the root, when the root has no `output`:
176
+ * ``machine "<id>": final state "<state>" declares an output, and the root has no output — xstate
177
+ * v5 gives a run only its root output, so this output reaches nothing and the run ends with none:
178
+ * write it at the root, as output: ({ context }) => ..., or keep it on the final state and pass it
179
+ * on with output: ({ event }) => event.output at the root``. With more than one such final state,
180
+ * it names each one and puts the words in the plural: ``final states "<state>", "<state>"
181
+ * declare an output``, ``these outputs reach nothing``, ``write one at the root`` and ``keep
182
+ * them on the final states and pass them on``. An `output` on a
183
+ * final state inside a compound state is legal: the `onDone` of that compound state gets it as
184
+ * `event.output`.
185
+ * - a parallel state at or below a state that invokes an Agent: ``machine "<id>": state "<state>"
186
+ * is parallel, at or below Agent "<slot>"'s state "<state>" — a parallel region below an Agent's
187
+ * state is reserved for picks the Agent does not wait for, which jr2 does not build; background
188
+ * work goes above the Agent's state``
189
+ * - a state below an Agent's state that invokes something and that nothing enters: ``machine
190
+ * "<id>": state "<state>" below Agent "<slot>"'s state "<state>" is entered by nothing, so what
191
+ * it invokes (<src>) never runs — target it from a pick or a route, or delete it``
192
+ * - a state that invokes an Agent when that state and each state above it have no `agent.fault`
193
+ * route: ``machine "<id>": state "<state>" invokes an Agent, and no route for "agent.fault" is
194
+ * on it or on a state above — jr2 sends agent.fault when the Agent's Turn cannot go on, and with
195
+ * no route the run faults: route it on that state or on a state above it, as on: {
196
+ * "agent.fault": "<the state that handles it>" } (ADR-0074)``. A state that invokes an Open body
197
+ * that lends an Agent or allows a model gets the same line, with `invokes an Open body that can
198
+ * run an Agent`: the body sends it a Chart's unrouted `agent.fault`. When both kinds have no route,
199
+ * one line names both, joined by `, and`. One route on the root is a route for each state. A
200
+ * Chart's Machine does not get this refusal: in a Chart the route is optional (ADR-0070).
201
+ *
202
+ * It also refuses a `sentBy` that names no party (see `sentBy`). It refuses a pick param that
203
+ * nothing reads, where the pick enters a state that invokes an Effect (see `fromPick`). Shapes that
204
+ * are legal but probably not what you mean, such as a mode that nothing enters, are warnings that
205
+ * `jr2 up` prints.
206
+ *
207
+ * @param def - What `setup()` takes, with `events` in place of `types.events`. A Machine that
208
+ * needs none of it calls `jr2Setup()`.
209
+ * @returns What `setup()` returns. Call `.createMachine(config)` on it.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * import { z } from "zod";
214
+ * import { agent, defineEvent, fail, jr2Setup } from "@jr2/orchestrator";
215
+ *
216
+ * const proposed = defineEvent({
217
+ * name: "proposed",
218
+ * description: "Call this with your plan.",
219
+ * input: z.object({ plan: z.string() }),
220
+ * });
221
+ * const approve = defineEvent({ name: "approve", input: z.object({}) });
222
+ * const reject = defineEvent({ name: "reject", input: z.object({ reason: z.string() }) });
223
+ *
224
+ * export const machine = jr2Setup({
225
+ * types: {} as { input: { goal: string }; context: { goal: string } },
226
+ * events: [proposed, approve, reject],
227
+ * actors: {
228
+ * planner: agent({
229
+ * model: "anthropic/claude-haiku-5-5",
230
+ * instructions: "You plan small releases.",
231
+ * workspace: "none",
232
+ * }),
233
+ * fail: fail(),
234
+ * },
235
+ * }).createMachine({
236
+ * id: "plan",
237
+ * input: z.object({ goal: z.string() }),
238
+ * context: ({ input }) => ({ goal: input.goal }),
239
+ * on: { "agent.fault": ".failed" },
240
+ * initial: "planning",
241
+ * states: {
242
+ * planning: {
243
+ * invoke: { src: "planner", input: ({ context }) => ({ prompt: `Plan this: ${context.goal}` }) },
244
+ * on: { proposed: "review" },
245
+ * },
246
+ * review: {
247
+ * invoke: { src: "gate" },
248
+ * on: { approve: "approved", reject: "planning" },
249
+ * },
250
+ * approved: { type: "final" },
251
+ * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
252
+ * },
253
+ * });
254
+ * ```
255
+ *
256
+ * @see docs/guides/write-a-workflow.md
257
+ * @see docs/concepts/machines-and-workflows.md
258
+ * @see docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md
83
259
  */
84
260
  export function jr2Setup<
85
261
  TContext extends MachineContext = MachineContext,
86
- const TDefs extends readonly EventDef[] = readonly EventDef[],
262
+ const TDefs extends readonly EventDef[] = readonly [],
87
263
  TActors extends Record<string, UnknownActorLogic> = {},
88
264
  TActions extends Record<string, ParameterizedObject["params"] | undefined> = {},
89
265
  TGuards extends Record<string, ParameterizedObject["params"] | undefined> = {},
@@ -93,50 +269,62 @@ export function jr2Setup<
93
269
  TOutput extends NonReducibleUnknown = NonReducibleUnknown,
94
270
  TEmitted extends EventObject = EventObject,
95
271
  TMeta extends MetaObject = MetaObject,
96
- >(def: {
97
- /** context/input/output/tags/emitted/meta — never `events`; the union is derived. */
98
- types?: {
99
- context?: TContext;
100
- input?: TInput;
101
- output?: TOutput;
102
- tags?: TTag;
103
- emitted?: TEmitted;
104
- meta?: TMeta;
105
- };
106
- /** This Machine's vocabulary, as values — the single source for types, validation, delivery. */
107
- events: TDefs;
108
- actors?: TActors;
109
- actions?: {
110
- [K in keyof TActions]: ActionFunction<
111
- TContext,
112
- WorkflowEvent<TDefs>,
113
- WorkflowEvent<TDefs>,
114
- TActions[K],
115
- ToProvidedActor<MergedActors<TActors>>,
116
- ToParameterizedObject<TActions>,
117
- ToParameterizedObject<TGuards>,
118
- TDelay,
119
- TEmitted
120
- >;
121
- };
122
- guards?: {
123
- [K in keyof TGuards]: GuardPredicate<TContext, WorkflowEvent<TDefs>, TGuards[K], ToParameterizedObject<TGuards>>;
124
- };
125
- delays?: {
126
- [K in TDelay]: DelayConfig<
127
- TContext,
128
- WorkflowEvent<TDefs>,
129
- ToParameterizedObject<TActions>["params"],
130
- WorkflowEvent<TDefs>
131
- >;
132
- };
133
- }): SetupReturn<
272
+ >(
273
+ def: {
274
+ /** The types of the Machine, as in `setup()`: `context`, `input`, `output`, `tags`, `emitted`
275
+ * and `meta`. Not `events`: the event type comes from `events`. */
276
+ types?: {
277
+ context?: TContext;
278
+ input?: TInput;
279
+ output?: TOutput;
280
+ tags?: TTag;
281
+ emitted?: TEmitted;
282
+ meta?: TMeta;
283
+ };
284
+ // The single source for types, validation and delivery.
285
+ /** The workflow events this Machine takes: each one made by `defineEvent`. Leave it out when the
286
+ * Machine takes none. Each listed event must be the name of an `on` key. Each `on` key must be
287
+ * listed, if it is not a jr2 or xstate event (an event with a `.` in its name). */
288
+ events?: TDefs;
289
+ /** The actors the states invoke by name, as in `setup()`: Agent slots (`agent(…)`), Effects
290
+ * (`command(…)`, `http(…)`, `push(…)`), child Machines, `workspace(…)` and plain xstate logic.
291
+ * The key is the slot's name. An actor named `gate` replaces jr2's Gate. */
292
+ actors?: TActors;
293
+ /** Named actions, as in `setup()`. Their `event` has the type of this Machine's events. */
294
+ actions?: {
295
+ [K in keyof TActions]: ActionFunction<
296
+ TContext,
297
+ WorkflowEvent<TDefs>,
298
+ WorkflowEvent<TDefs>,
299
+ TActions[K],
300
+ ToProvidedActor<MergedActors<TActors>>,
301
+ ToParameterizedObject<TActions>,
302
+ ToParameterizedObject<TGuards>,
303
+ TDelay,
304
+ TEmitted
305
+ >;
306
+ };
307
+ /** Named guards, as in `setup()`. `sentBy` is there already; do not name a guard `jr2.sentBy`. */
308
+ guards?: {
309
+ [K in keyof TGuards]: GuardPredicate<TContext, WorkflowEvent<TDefs>, TGuards[K], ToParameterizedObject<TGuards>>;
310
+ };
311
+ /** Named delays, as in `setup()`. */
312
+ delays?: {
313
+ [K in TDelay]: DelayConfig<
314
+ TContext,
315
+ WorkflowEvent<TDefs>,
316
+ ToParameterizedObject<TActions>["params"],
317
+ WorkflowEvent<TDefs>
318
+ >;
319
+ };
320
+ } = {},
321
+ ): SetupReturn<
134
322
  TContext,
135
323
  WorkflowEvent<TDefs>,
136
324
  MergedActors<TActors>,
137
325
  {},
138
326
  TActions,
139
- TGuards,
327
+ TGuards & SentByGuards,
140
328
  TDelay,
141
329
  TTag,
142
330
  TInput,
@@ -144,11 +332,14 @@ export function jr2Setup<
144
332
  TEmitted,
145
333
  TMeta
146
334
  > {
335
+ // Like `setup()`, but `types.events` is gone (ADR-0015): the event union derives from `events`
336
+ // (defineEvent defs, taken as values) plus the mechanism events.
147
337
  const inner = setup({
148
338
  types: def.types,
149
339
  actors: { ...jr2Actors, ...(def.actors ?? {}) },
150
340
  actions: def.actions,
151
- guards: def.guards,
341
+ // `sentBy(id)` is a named guard with params, so the load walk can READ it (ADR-0015).
342
+ guards: { ...(def.guards ?? {}), [SENT_BY]: sentByGuard },
152
343
  delays: def.delays,
153
344
  } as never);
154
345
 
@@ -157,19 +348,28 @@ export function jr2Setup<
157
348
  // machine receives at creation — and is pulled OFF before xstate sees it: a zod schema is
158
349
  // not machine structure, and it must never reach the fingerprint/serialization paths that
159
350
  // read `machine.config`. It is attached beside the vocabulary below.
160
- const { input: inputSchema, ...machineConfig } = config as { input?: z.ZodObject } & Record<string, unknown>;
351
+ // A compiled Chart (ADR-0070) is marked, and the marker is pulled off as `input` is.
352
+ const {
353
+ input: inputSchema,
354
+ [COMPILED_CHART]: compiledChart,
355
+ ...machineConfig
356
+ } = config as { input?: z.ZodObject; [COMPILED_CHART]?: true } & Record<string, unknown>;
161
357
 
162
358
  // Resolve the defs first (duplicates and reserved semantics fail HERE, naming the machine —
163
359
  // the same loud failure `eventMap` gave the manifest, moved to machine-build time)…
164
- const defs = eventMap((machineConfig as { id?: string }).id ?? "(machine)", def.events);
360
+ const defs = eventMap((machineConfig as { id?: string }).id ?? "(machine)", def.events ?? []);
361
+
362
+ // Every Open body slot (ADR-0070), found by its brand: each name it lends resolves against THIS
363
+ // setup's own parts, or the Machine is refused naming it. The resolved set is stamped on the slot,
364
+ // for the door that admits a Chart before a run and for the walk at `jr2 up`.
365
+ stampOpenBodies((machineConfig as { id?: string }).id ?? "(machine)", def);
165
366
 
166
367
  // …then rewrite the config (ADR-0015): every Agent-slot/`gate` invoke's input is wrapped to
167
368
  // append its DERIVED menu and finalize the mechanism fields. Static — the walk sees the same
168
369
  // config the Console will — and the derived names still ride serializable input, so the
169
370
  // ADR-0007 restore path and invoke-time `resolveAccepts` validation are unchanged.
170
- const machine = (inner.createMachine as unknown as (c: never) => AnyStateMachine)(
171
- deriveMenus(machineConfig, defs, def.actors ?? {}) as never,
172
- );
371
+ const derived = deriveMenus(machineConfig, defs, def.actors ?? {});
372
+ const machine = (inner.createMachine as unknown as (c: never) => AnyStateMachine)(derived.config as never);
173
373
 
174
374
  // Close the nested-`on` typo hole (ADR-0015): with the manifest dead, a typo'd key would
175
375
  // silently become vocabulary — so every non-dotted key the machine handles anywhere must map
@@ -177,6 +377,7 @@ export function jr2Setup<
177
377
  // targetless/actionless transitions — exactly where a typo'd key would hide). Dotted names
178
378
  // (`agent.*`, `workspace.*`, `xstate.*`, delayed transitions) are mechanically jr2's/xstate's;
179
379
  // `*` is the wildcard descriptor.
380
+ const handled = new Set<string>();
180
381
  const walk = (node: AnyStateMachine["root"]): void => {
181
382
  for (const descriptor of node.transitions.keys()) {
182
383
  if (descriptor === "*" || descriptor.includes(".")) continue;
@@ -186,12 +387,51 @@ export function jr2Setup<
186
387
  `declares (declared: ${[...defs.keys()].join(", ") || "none"}) — a typo, or a missing defineEvent`,
187
388
  );
188
389
  }
390
+ handled.add(descriptor);
189
391
  }
190
392
  for (const child of Object.values(node.states)) walk(child);
191
393
  };
192
394
  walk(machine.root);
193
395
 
396
+ // The other direction (ADR-0015): a def no state handles is in no derived set, so it is on no
397
+ // Menu and no Gate — a pick nothing can send and nothing would take.
398
+ const dead = [...defs.keys()].filter((name) => !handled.has(name));
399
+ if (dead.length) {
400
+ throw new Error(
401
+ `machine "${machine.id}" declares ${dead.map((name) => `"${name}"`).join(", ")}, which no state handles — ` +
402
+ `a pick no state takes is on no Menu and no Gate: handle it, or drop it from jr2Setup({ events })`,
403
+ );
404
+ }
405
+
406
+ // A Part written inline has no name (ADR-0049, ADR-0067): refused, with the slot form.
407
+ refuseInlineParts(machine);
408
+ // A final state's output with no root output (ADR-0015): the run would end with none.
409
+ refuseLostOutput(machine);
410
+ // A state that invokes an Agent, or an Open body that can run one, with no `agent.fault` route
411
+ // (ADR-0074): the run would wait for ever. Not in a compiled Chart: there the route is optional,
412
+ // and the Open body sends the event that no transition takes to the Machine that invoked it (ADR-0070).
413
+ if (compiledChart !== true) refuseUnroutedAgentFault(machine);
414
+ refuseParallelBelowAgents(machine, def.actors ?? {});
415
+ refuseUnknownSentBy(machine);
416
+ // Before the Effect checks: they ask who may send a writing (`mayTake`), which reads its audience.
194
417
  attachVocabulary(machine, defs);
418
+ // The Effect checks (ADR-0067): a pick param nothing reads, a `fill` that overwrites one, a
419
+ // `fromPick` of another pick — refused; a mapper the probe cannot run — warned.
420
+ const effects = effectChecks(machine, def.actors ?? {}, defs);
421
+ if (effects.errors.length) throw new Error(`machine "${machine.id}": ${effects.errors.join("; ")}`);
422
+
423
+ const idOf = (path: readonly string[]): string => stateAt(machine, path).id;
424
+ attachSeats(
425
+ machine,
426
+ derived.seats.map(({ path, ...seat }) => ({ state: idOf(path), ...seat })),
427
+ );
428
+ attachLoadWarnings(machine, [
429
+ ...unenteredModes(machine, def.actors ?? {}),
430
+ ...restWarnings(machine, def.actors ?? {}),
431
+ ...effects.warnings,
432
+ ...derived.warnings.map((warning) => warning(idOf)),
433
+ ...partyWarnings(machine, derived.derivedGateIds),
434
+ ]);
195
435
  if (inputSchema) attachInputSchema(machine, inputSchema);
196
436
  return machine;
197
437
  };
@@ -204,7 +444,7 @@ export function jr2Setup<
204
444
  MergedActors<TActors>,
205
445
  {},
206
446
  TActions,
207
- TGuards,
447
+ TGuards & SentByGuards,
208
448
  TDelay,
209
449
  TTag,
210
450
  TInput,
@@ -214,12 +454,17 @@ export function jr2Setup<
214
454
  >;
215
455
  }
216
456
 
217
- // --- Menu derivation (ADR-0015) ------------------------------------------------------------------
218
- // A state that invokes an AGENT SLOT gets, as its Agent's tool menu, the workflow events its
219
- // transitions handle — own + bubbled ancestors, per statechart semantics — filtered to audience
220
- // ∈ {agent, any}; a `gate` gets the same set filtered to {external, any}. The invoking actor
221
- // kind is the primary router; `audience` on the def exists to RESTRICT (tag the security-
222
- // sensitive events). Explicit `tools:`/`accepts:` on the invoke input remain as escape hatches.
457
+ // --- Menu derivation (ADR-0015, ADR-0029) --------------------------------------------------------
458
+ // A state that invokes an AGENT SLOT derives the workflow events the Turn may pick: its own
459
+ // transitions, its ancestors' (bubbled, per statechart semantics), and those written on the states
460
+ // below it — its MODES (`modePicks`) — filtered to the names that Agent may send (the audience
461
+ // rule, on `deriveMenus`). That is its derived set, before its guards narrow it to the Allowed
462
+ // picks; the Allowed picks are read off the live snapshot, so they follow the active mode. The
463
+ // Agent's MENU — its tools block — is the union of those sets over every state that invokes it,
464
+ // fixed for the conversation. A `gate` derives its own and its ancestors' transitions, and those
465
+ // written on its plain children (the same descent, stopping at a child that invokes a party),
466
+ // filtered to the names its caller may send. An explicit `accepts:` on a gate's input remains an
467
+ // escape hatch.
223
468
  //
224
469
  // An agent invoke is identified by its LOGIC, not by a reserved src name: the walk looks the
225
470
  // invoke's `src` up in the setup's own `actors` map and asks `isAgent` (ADR-0049). That is also
@@ -240,18 +485,81 @@ type LooseState = {
240
485
  };
241
486
  type InputArgs = { context: unknown; event: unknown; self: AnyActorRef };
242
487
 
243
- /** Rewrite a machine config, wrapping every Agent-slot/`gate` invoke input (immutably). */
244
- function deriveMenus(config: unknown, defs: Map<string, EventDef>, actors: Record<string, UnknownActorLogic>): unknown {
245
- const pick = (names: Set<string>, kind: "agent" | "external"): string[] =>
246
- [...names].filter((name) => {
247
- const d = defs.get(name);
248
- return !!d && (d.audience === kind || d.audience === "any");
249
- });
250
-
251
- const walk = (node: LooseState, inherited: Set<string>, path: readonly string[]): LooseState => {
252
- const names = new Set(inherited);
253
- for (const key of Object.keys(node.on ?? {})) {
254
- if (!key.includes(".") && key !== "*") names.add(key);
488
+ /**
489
+ * One party's seat on one state: who may send what there (ADR-0015). `path` is the state's key
490
+ * path below the Machine's root; `names` is the derived set — for an Agent, what its Turn may pick
491
+ * before the guards narrow it; for a Gate, its accepted set unless its input names `accepts:`.
492
+ */
493
+ export type Seat = { path: readonly string[]; party: "agent" | "gate"; agent?: string; names: string[] };
494
+
495
+ /** Each name in reach, with the OWNER mark of every writing of it in reach (see below). */
496
+ type Reach = Map<string, Set<string>>;
497
+ /** Each name written on the path from the root: where, and the owner mark of each writing. */
498
+ type Writings = Map<string, Array<{ path: readonly string[]; owner: string }>>;
499
+ /** A load warning, rendered once the built Machine can turn a key path into a state id. */
500
+ type Warning = (idOf: (path: readonly string[]) => string) => string;
501
+
502
+ /** The owner mark of a writing no party answers for — at or above it, no state invokes an Agent
503
+ * slot or a `gate` — so both kinds may send it. */
504
+ const NOBODY = "";
505
+ /** The owner mark of the state at `path`: the party it invokes answers for the writings below it.
506
+ * `#` keeps the root's mark apart from {@link NOBODY}. */
507
+ const ownerMark = (path: readonly string[]): string => `#${path.join(".")}`;
508
+
509
+ /** Rewrite a machine config, wrapping every Agent-slot/`gate` invoke input (immutably).
510
+ *
511
+ * The same walk derives each Agent name's MENU (ADR-0029): the union of the derived sets of every
512
+ * state that invokes an Agent of that name in this Machine — the scope of its continued
513
+ * conversation's id (ADR-0057) — sorted, so it is byte-stable for every Turn of the conversation
514
+ * and for every fresh one. The wrapped inputs read it at invoke time, when the walk is long done.
515
+ *
516
+ * WHO may send a name (the audience rule, ADR-0015). A writing — an `on` key on a state —
517
+ * belongs to the party that its state, or the nearest state above it, invokes: an Agent slot, a
518
+ * `gate`, or both when one state invokes both. With no such state above, it belongs to both. An
519
+ * event whose def leaves `audience` unset follows that rule; an explicit `audience` overrides it
520
+ * BY KIND over the whole reach (`agent`: every Agent whose reach holds a writing of it; `external`:
521
+ * every Gate's; `any`: both). The REACH never changes — own + ancestors, plus the states below
522
+ * with no party-invoking state between (an Agent's modes, a Gate's plain children) — and the rule
523
+ * only filters inside it, so it cannot widen who may send a name. */
524
+ function deriveMenus(
525
+ config: unknown,
526
+ defs: Map<string, EventDef>,
527
+ actors: Record<string, UnknownActorLogic>,
528
+ ): { config: unknown; seats: Seat[]; warnings: Warning[]; derivedGateIds: Set<string> } {
529
+ const pick = (reach: Reach, kind: "agent" | "external", self: string): string[] =>
530
+ [...reach]
531
+ .filter(([name, owners]) => {
532
+ const d = defs.get(name);
533
+ if (!d) return false;
534
+ if (d.audience !== undefined) return d.audience === kind || d.audience === "any";
535
+ return owners.has(self) || owners.has(NOBODY);
536
+ })
537
+ .map(([name]) => name);
538
+ const menus = new Map<string, Set<string>>();
539
+ const menuOf = (agentName: string): string[] => [...(menus.get(agentName) ?? [])].sort();
540
+ const seats: Seat[] = [];
541
+ // The load warnings this walk can see (ADR-0015), each rendered once the states have ids. Keyed,
542
+ // so a writing seen from two paths is one warning.
543
+ const warnings = new Map<string, Warning>();
544
+ // Which parties each owner mark's state invokes — what the Gate-above warning reads.
545
+ const parties = new Map<string, { agents: string[]; gate: boolean }>();
546
+ // The Gate invoke ids this walk names: a party warning says how to name one instead.
547
+ const derivedGateIds = new Set<string>();
548
+
549
+ const walk = (
550
+ node: LooseState,
551
+ inherited: Reach,
552
+ writtenAbove: Writings,
553
+ path: readonly string[],
554
+ ownerAbove: string,
555
+ ): LooseState => {
556
+ const owner = invokesParty(node, actors) ? ownerMark(path) : ownerAbove;
557
+ if (owner !== ownerAbove) parties.set(owner, partiesOf(node, actors));
558
+ const names: Reach = new Map([...inherited].map(([name, owners]) => [name, new Set(owners)]));
559
+ const written: Writings = new Map([...writtenAbove].map(([name, at]) => [name, [...at]]));
560
+ for (const name of ownPicks(node)) {
561
+ names.set(name, (names.get(name) ?? new Set<string>()).add(owner));
562
+ written.set(name, [...(written.get(name) ?? []), { path, owner }]);
255
563
  }
256
564
 
257
565
  let out = node;
@@ -264,17 +572,49 @@ function deriveMenus(config: unknown, defs: Map<string, EventDef>, actors: Recor
264
572
  const wrapOne = (inv: LooseInvoke): LooseInvoke => {
265
573
  // The slot key IS the Agent name (ADR-0049) — read off the declaration, never authored.
266
574
  if (typeof inv?.src === "string" && isAgent(actors[inv.src])) {
267
- const derived = pick(names, "agent");
268
- if (derived.length === 0) refuseBuriedPicks(node, inv.src, path, pick);
269
- return { ...inv, input: wrapAgentInput(inv.input, derived, inv.src) };
575
+ // A mode's writings are this state's by construction: the descent stops at any child
576
+ // that invokes a party, so none answers between them and this one.
577
+ const reach: Reach = new Map([...names].map(([name, owners]) => [name, new Set(owners)]));
578
+ for (const name of modePicks(node, actors))
579
+ reach.set(name, (reach.get(name) ?? new Set<string>()).add(owner));
580
+ const derived = pick(reach, "agent", owner);
581
+ // A Gate above that takes a writing from this Agent: the Gate's caller owns it, so the
582
+ // Agent below — which reaches it — cannot send it. Legal, and the one narrowing of the
583
+ // audience rule an author may not expect.
584
+ const agent = inv.src;
585
+ for (const [name, owners] of reach) {
586
+ if (derived.includes(name) || defs.get(name)?.audience !== undefined) continue;
587
+ for (const at of written.get(name) ?? []) {
588
+ if (!owners.has(at.owner) || at.owner === owner || !parties.get(at.owner)?.gate) continue;
589
+ warnings.set(
590
+ `gate:${agent}:${path.join(".")}:${name}:${at.path.join(".")}`,
591
+ (idOf) =>
592
+ `Agent "${agent}" on "${idOf(path)}" cannot send "${name}": it is written on "${idOf(at.path)}", ` +
593
+ `whose Gate's caller owns it — give the def \`audience: "any"\` for both to send it`,
594
+ );
595
+ }
596
+ }
597
+ const menu = menus.get(inv.src) ?? new Set<string>();
598
+ for (const name of derived) menu.add(name);
599
+ menus.set(inv.src, menu);
600
+ seats.push({ path, party: "agent", agent: inv.src, names: [...derived].sort() });
601
+ return { ...inv, input: wrapAgentInput(inv.input, derived, inv.src, menuOf) };
270
602
  }
271
603
  if (inv?.src === "gate") {
272
- const wrapped: LooseInvoke = { ...inv, input: wrapGateInput(inv.input, pick(names, "external")) };
604
+ // A Gate's plain children — below it, with no party-invoking state between — are its own
605
+ // by position, as an Agent's modes are its (ADR-0015).
606
+ const reach: Reach = new Map([...names].map(([name, owners]) => [name, new Set(owners)]));
607
+ for (const name of modePicks(node, actors))
608
+ reach.set(name, (reach.get(name) ?? new Set<string>()).add(owner));
609
+ const accepts = pick(reach, "external", owner);
610
+ seats.push({ path, party: "gate", names: [...accepts].sort() });
611
+ const wrapped: LooseInvoke = { ...inv, input: wrapGateInput(inv.input, accepts) };
273
612
  // A machine-root gate (empty path) is left to xstate's default id: a `""` id would be
274
613
  // worse than a noisy one, and the derived gate id still works.
275
614
  if (inv.id == null && path.length) {
276
615
  const key = path.join(".");
277
616
  wrapped.id = unnamedGates > 1 ? `${key}.${ordinal++}` : key;
617
+ derivedGateIds.add(wrapped.id as string);
278
618
  }
279
619
  return wrapped;
280
620
  }
@@ -284,53 +624,409 @@ function deriveMenus(config: unknown, defs: Map<string, EventDef>, actors: Recor
284
624
  }
285
625
  if (node.states) {
286
626
  const states: Record<string, LooseState> = {};
287
- for (const [key, child] of Object.entries(node.states)) states[key] = walk(child, names, [...path, key]);
627
+ for (const [key, child] of Object.entries(node.states))
628
+ states[key] = walk(child, names, written, [...path, key], owner);
288
629
  out = { ...(out === node ? node : out), states };
289
630
  }
290
631
  return out;
291
632
  };
292
633
 
293
- return walk(config as LooseState, new Set(), []);
634
+ const out = walk(config as LooseState, new Map(), new Map(), [], NOBODY);
635
+ return { config: out, seats, warnings: [...warnings.values()], derivedGateIds };
294
636
  }
295
637
 
296
638
  /**
297
- * Refuse the one shape that LOOKS like a Menu and is not one (ADR-0015, ADR-0057): an Agent
298
- * invoked on a state whose picks are written in its SUBSTATES.
639
+ * A mode that nothing enters (ADR-0015): a state below an Agent's state with no invoke that the
640
+ * Machine can never enter. Its picks are on the Menu and never allowed. A warning, not a refusal: the
641
+ * Machine is legal, and a mode left unwired is often work in progress. A state below an Agent's state
642
+ * that INVOKES something and that nothing enters is refused (ADR-0067): what it invokes never runs,
643
+ * which is never work in progress. Both look below a state that invokes an Effect or a child Machine;
644
+ * neither looks below one that invokes another Agent or a Gate.
299
645
  *
300
- * A Menu derives from the invoking state's own + ANCESTOR transitions, per statechart semantics,
301
- * so a pick written below that state is one the Turn is never offered — the model is handed a
302
- * Menu it cannot end its turn with, and the actor reads an empty Menu as a Turn that ended as
303
- * intended, so nothing nudges and nothing faults: the run parks. Until ADR-0057 the `tools:`
304
- * override papered over it; with the override gone the kit says it here, at build, where `jr2 up`
305
- * walks every registered Machine.
646
+ * "Can enter" is reachability from the root, to a fixed point. A state is entered when its parent is
647
+ * entered and it is the parent's initial state or a region of a parallel parent, or when a transition
648
+ * whose SOURCE can be entered targets it or a state inside it (entering a state enters its
649
+ * ancestors). So a compound mode's own `initial` does not enter it, and neither does a transition
650
+ * written on the mode into itself: neither runs unless the mode is already active.
306
651
  *
307
- * An EMPTY Menu is not itself wrong — a state moved by a Gate or a timer asks its Agent for text
308
- * and nothing else — so only the contradiction is refused: no Menu here, and picks below.
652
+ * A transition into a history state enters what that history state restores. The walk follows its
653
+ * DEFAULT target — shallow or deep, what xstate enters when nothing is recorded yet — or, with none
654
+ * written, its parent's initial state, which entering the parent already covers. A recorded history
655
+ * restores only states that were entered another way. A default target the walk cannot resolve is a
656
+ * warning of its own: the modes behind it may read as unentered when they are not, and a state that
657
+ * invokes below that history state's parent is warned about, never refused — the walk refuses only
658
+ * what it knows nothing enters. A history state is no mode.
309
659
  */
310
- function refuseBuriedPicks(
311
- node: LooseState,
312
- agentName: string,
313
- path: readonly string[],
314
- pick: (names: Set<string>, kind: "agent" | "external") => string[],
660
+ function unenteredModes(machine: AnyStateMachine, actors: Record<string, UnknownActorLogic>): string[] {
661
+ type Node = AnyStateMachine["root"];
662
+ const all: Node[] = [];
663
+ const collect = (node: Node): void => {
664
+ all.push(node);
665
+ for (const child of Object.values(node.states)) collect(child);
666
+ };
667
+ collect(machine.root);
668
+
669
+ const out: string[] = [];
670
+ const entered = new Set<Node>([machine.root]);
671
+ const enter = (node: Node): boolean => {
672
+ let added = false;
673
+ for (let n: Node | undefined = node; n !== undefined && !entered.has(n); n = n.parent) {
674
+ entered.add(n);
675
+ added = true;
676
+ }
677
+ if (node.type === "history") for (const target of historyDefault(node)) added = enter(target) || added;
678
+ return added;
679
+ };
680
+ /** A history state's default target, resolved the way xstate resolves it: a path from its parent,
681
+ * or a `#id`. Unresolvable: warned once, and nothing is followed. */
682
+ const unresolved = new Set<Node>();
683
+ const historyDefault = (node: Node): Node[] => {
684
+ const written = (node.config as { target?: unknown }).target;
685
+ if (written === undefined) return [];
686
+ const targets = (Array.isArray(written) ? written : [written]) as unknown[];
687
+ const resolved: Node[] = [];
688
+ for (const target of targets) {
689
+ const found = typeof target === "string" ? resolvePath(node.parent!, target) : undefined;
690
+ if (found !== undefined) {
691
+ resolved.push(found);
692
+ continue;
693
+ }
694
+ if (unresolved.has(node)) continue;
695
+ unresolved.add(node);
696
+ out.push(
697
+ `history state "${node.id}" has a default target the load walk cannot follow (${JSON.stringify(target)}) — ` +
698
+ `a mode entered only through it is reported as unentered, and a state that invokes is warned, not refused`,
699
+ );
700
+ }
701
+ return resolved;
702
+ };
703
+ const resolvePath = (parent: Node, target: string): Node | undefined => {
704
+ if (target.startsWith("#")) {
705
+ try {
706
+ return machine.getStateNodeById(target.slice(1)) as Node;
707
+ } catch {
708
+ return undefined;
709
+ }
710
+ }
711
+ let at: Node | undefined = parent;
712
+ for (const key of target.split(".")) at = at?.states[key];
713
+ return at;
714
+ };
715
+ for (let changed = true; changed; ) {
716
+ changed = false;
717
+ for (const node of all) {
718
+ const parent = node.parent;
719
+ if (parent === undefined || entered.has(node) || !entered.has(parent)) continue;
720
+ if (parent.type === "parallel" || (parent.type === "compound" && parent.initial.target.includes(node)))
721
+ changed = enter(node) || changed;
722
+ }
723
+ for (const node of all) {
724
+ if (!entered.has(node)) continue;
725
+ const transitions = [...node.transitions.values()].flat().concat(node.always ?? []);
726
+ for (const t of transitions) for (const target of t.target ?? []) changed = enter(target) || changed;
727
+ }
728
+ }
729
+ const invokedAgent = (node: Node): string | undefined =>
730
+ node.invoke.find((inv) => typeof inv.src === "string" && isAgent(actors[inv.src]))?.src as string | undefined;
731
+ const isBelow = (node: Node, ancestor: Node): boolean => {
732
+ for (let n = node.parent; n !== undefined; n = n.parent) if (n === ancestor) return true;
733
+ return false;
734
+ };
735
+
736
+ const refused: string[] = [];
737
+ for (const node of all) {
738
+ const agent = invokedAgent(node);
739
+ // An Agent's state the Machine never enters is a larger defect than its modes, and not this check.
740
+ if (agent === undefined || !entered.has(node)) continue;
741
+ // Every state below the Agent's state belongs to its Turn, down to a child that invokes another
742
+ // party (an Agent or a Gate): that child is checked, and below it is that party's. A state below
743
+ // an unentered one is not reported again.
744
+ const descend = (parent: Node): void => {
745
+ for (const child of Object.values(parent.states)) {
746
+ if (child.type === "history") continue;
747
+ if (child.invoke.length > 0) {
748
+ // A child that invokes something and that nothing enters (ADR-0067): what it invokes never
749
+ // runs — a refusal, where a mode nothing enters is only a warning.
750
+ if (!entered.has(child)) {
751
+ const what = child.invoke.map((inv) => (typeof inv.src === "string" ? inv.src : inv.id)).join(", ");
752
+ // Where a history default the walk cannot follow may enter it, the walk does not know: a
753
+ // warning, never a refusal of a Machine that may be legal.
754
+ const unsure = [...unresolved].find((h) => isBelow(child, h.parent!));
755
+ if (unsure !== undefined) {
756
+ out.push(
757
+ `state "${child.id}" below Agent "${agent}"'s state "${node.id}" may be entered by nothing, so what ` +
758
+ `it invokes (${what}) may never run — the walk cannot follow history state "${unsure.id}"'s ` +
759
+ `default target, which may enter it`,
760
+ );
761
+ } else {
762
+ refused.push(
763
+ `state "${child.id}" below Agent "${agent}"'s state "${node.id}" is entered by nothing, so what it ` +
764
+ `invokes (${what}) never runs — target it from a pick or a route, or delete it`,
765
+ );
766
+ }
767
+ continue;
768
+ }
769
+ const party = child.invoke.some(
770
+ (inv) => inv.src === "gate" || (typeof inv.src === "string" && isAgent(actors[inv.src])),
771
+ );
772
+ if (!party) descend(child);
773
+ continue;
774
+ }
775
+ if (!entered.has(child)) {
776
+ const picks = [...child.transitions.keys()].filter((key) => !key.includes(".") && key !== "*");
777
+ out.push(
778
+ `mode "${child.id}" of Agent "${agent}"'s state "${node.id}" — nothing enters it: it is not ` +
779
+ `initial and no state the Machine can enter has a transition into it, so its picks` +
780
+ `${picks.length ? ` (${picks.join(", ")})` : ""} are on the Menu and never allowed`,
781
+ );
782
+ continue;
783
+ }
784
+ descend(child);
785
+ }
786
+ };
787
+ descend(node);
788
+ }
789
+ if (refused.length) throw new Error(`machine "${machine.id}": ${refused.join("; ")}`);
790
+ return out;
791
+ }
792
+
793
+ /**
794
+ * Does this logic settle on its own — a promise, the one plain xstate actor that ends without being
795
+ * stopped? Asked of its own transition: a promise logic's initial snapshot, sent xstate's resolve
796
+ * event, is `done`. That event name is an xstate internal (5.32.2); a bump that renamed it would
797
+ * make every promise read long-lived, and setup.test.ts pins it against the real constructors.
798
+ */
799
+ function settlesOnItsOwn(logic: unknown): boolean {
800
+ const l = logic as {
801
+ getInitialSnapshot?: (scope: unknown, input: unknown) => unknown;
802
+ transition?: (snapshot: unknown, event: unknown, scope: unknown) => { status?: string } | undefined;
803
+ };
804
+ if (typeof l?.getInitialSnapshot !== "function" || typeof l.transition !== "function") return false;
805
+ try {
806
+ const scope = { self: {}, defer: () => {}, emit: () => {} };
807
+ const snapshot = l.getInitialSnapshot(scope, undefined);
808
+ return l.transition(snapshot, { type: "xstate.promise.resolve", data: undefined }, scope)?.status === "done";
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
813
+
814
+ /**
815
+ * The load walk's Rest warnings (ADR-0024): shapes below an Agent's state that keep it from Rest, so
816
+ * every pick of the Agent is refused for longer than the author likely meant. Warnings, never
817
+ * refusals: each can be intended.
818
+ *
819
+ * - **A Gate whose answer has no target.** The Gate stays open after its answer, and the Agent's
820
+ * state is not at Rest again until the Gate's state exits.
821
+ * - **A long-lived invoke** — an actor that runs until its state exits: not a promise or an Effect (each settles),
822
+ * a Gate, an Agent or a child Machine. While it runs, the Agent's state is never at Rest.
823
+ *
824
+ * The descent below an Agent's state stops at a child that invokes another Agent: below it is that
825
+ * Agent's state, read for that Agent.
826
+ */
827
+ function restWarnings(machine: AnyStateMachine, actors: Record<string, UnknownActorLogic>): string[] {
828
+ type Node = AnyStateMachine["root"];
829
+ const implementations = machine.implementations.actors as Record<string, unknown>;
830
+ const logicOf = (src: unknown): unknown => (typeof src === "string" ? (actors[src] ?? implementations[src]) : src);
831
+ const agentOf = (node: Node): string | undefined =>
832
+ node.invoke.find((inv) => typeof inv.src === "string" && isAgent(actors[inv.src]))?.src as string | undefined;
833
+ const out: string[] = [];
834
+ const visit = (node: Node): void => {
835
+ const agent = agentOf(node);
836
+ if (agent !== undefined) {
837
+ const descend = (parent: Node): void => {
838
+ for (const child of Object.values(parent.states)) {
839
+ if (agentOf(child) !== undefined) continue;
840
+ for (const inv of child.invoke) {
841
+ const logic = logicOf(inv.src);
842
+ const name = typeof inv.src === "string" ? inv.src : inv.id;
843
+ if (inv.src === "gate") {
844
+ for (const [event, candidates] of child.transitions) {
845
+ if (event.includes(".") || event === "*") continue;
846
+ if (candidates.some((t) => (t.target ?? []).length > 0)) continue;
847
+ out.push(
848
+ `Gate "${inv.id}" at "${child.id}", below Agent "${agent}"'s state "${node.id}": its answer "${event}" ` +
849
+ `has no target, so the Gate stays open after it — the Agent's state is not at rest again until ` +
850
+ `"${child.id}" exits, and every pick of the Agent is refused until then; give the answer a target`,
851
+ );
852
+ }
853
+ continue;
854
+ }
855
+ // An Open body (ADR-0070) runs a Chart, as a child Machine runs its states.
856
+ if (
857
+ isAgent(logic) ||
858
+ isEffect(logic) ||
859
+ isOpenBody(logic) ||
860
+ (logic as { root?: unknown } | undefined)?.root !== undefined
861
+ )
862
+ continue;
863
+ if (settlesOnItsOwn(logic)) continue;
864
+ out.push(
865
+ `"${child.id}", below Agent "${agent}"'s state "${node.id}", invokes "${name}", which runs until its ` +
866
+ `state exits (not a promise, an Effect, a Gate, an Agent or a child Machine): while it runs the Agent's state ` +
867
+ `is never at rest, so it blocks every pick of the Agent — invoke it above the Agent's state`,
868
+ );
869
+ }
870
+ descend(child);
871
+ }
872
+ };
873
+ descend(node);
874
+ }
875
+ for (const child of Object.values(node.states)) visit(child);
876
+ };
877
+ visit(machine.root);
878
+ return out;
879
+ }
880
+
881
+ /**
882
+ * Resolve and stamp every Open body slot's lent set (ADR-0070). Each name an Open body lends must be
883
+ * a part of this setup, of the kind it is lent as: an `agents` entry an Agent slot (an Open one too:
884
+ * its composer binds it as today), an `effects` entry an Effect, a `machines` entry a Machine, and a
885
+ * guard, action or delay one this setup implements. An unknown name refuses the createMachine naming
886
+ * it, so an Open body whose names did not resolve never reaches the walk.
887
+ */
888
+ function stampOpenBodies(
889
+ machineId: string,
890
+ def: {
891
+ actors?: Record<string, unknown>;
892
+ guards?: Record<string, unknown>;
893
+ actions?: Record<string, unknown>;
894
+ delays?: Record<string, unknown>;
895
+ },
315
896
  ): void {
316
- const below = new Set<string>();
317
- const descend = (n: LooseState): void => {
318
- for (const key of Object.keys(n.on ?? {})) if (!key.includes(".") && key !== "*") below.add(key);
319
- for (const child of Object.values(n.states ?? {})) descend(child);
897
+ const actors = def.actors ?? {};
898
+ for (const [slot, logic] of Object.entries(actors)) {
899
+ if (!isOpenBody(logic)) continue;
900
+ const decl = openBodyDeclarationOf(logic);
901
+ const faults: string[] = [];
902
+ const lent: LentParts = { actors: {}, guards: {}, actions: {}, delays: {} };
903
+ const actorKinds = [
904
+ ["agents", "an Agent slot (agent(...))", isAgent],
905
+ ["effects", "an Effect slot", isEffect],
906
+ ["machines", "a Machine", (l: unknown) => (l as { root?: unknown } | undefined)?.root !== undefined],
907
+ ] as const;
908
+ for (const [kind, what, test] of actorKinds) {
909
+ for (const name of decl.lends[kind] ?? []) {
910
+ const found = actors[name];
911
+ if (name === slot) faults.push(`lends.${kind}: "${name}" is the Open body itself`);
912
+ else if (found === undefined) {
913
+ faults.push(
914
+ `lends.${kind}: "${name}" is no actor slot of this Machine; its actors: ${Object.keys(actors).join(", ")}`,
915
+ );
916
+ } else if (!test(found)) faults.push(`lends.${kind}: "${name}" is not ${what}`);
917
+ else lent.actors[name] = found as UnknownActorLogic;
918
+ }
919
+ }
920
+ const implKinds = [
921
+ ["guards", def.guards ?? {}, lent.guards],
922
+ ["actions", def.actions ?? {}, lent.actions],
923
+ ["delays", def.delays ?? {}, lent.delays],
924
+ ] as const;
925
+ for (const [kind, impl, into] of implKinds) {
926
+ for (const name of decl.lends[kind] ?? []) {
927
+ if (impl[name] === undefined) {
928
+ faults.push(
929
+ `lends.${kind}: "${name}" is no ${kind.slice(0, -1)} of this Machine; its ${kind}: ${Object.keys(impl).join(", ") || "none"}`,
930
+ );
931
+ } else (into as Record<string, unknown>)[name] = impl[name];
932
+ }
933
+ }
934
+ if (faults.length) throw new Error(`machine "${machineId}": Open body "${slot}": ${faults.join("; ")}`);
935
+ attachLent(logic, lent);
936
+ }
937
+ }
938
+
939
+ /**
940
+ * A parallel region below a state that invokes an Agent is refused (ADR-0015). The shape is
941
+ * reserved for picks the Agent does not wait for, which jr2 does not build; a Turn takes one pick
942
+ * at a time. The Agent's state itself being parallel counts: its regions are below it. A
943
+ * Background work goes above the Agent's state.
944
+ */
945
+ function refuseParallelBelowAgents(machine: AnyStateMachine, actors: Record<string, UnknownActorLogic>): void {
946
+ type Node = AnyStateMachine["root"];
947
+ const firstParallel = (node: Node): Node | undefined => {
948
+ if (node.type === "parallel") return node;
949
+ for (const child of Object.values(node.states)) {
950
+ const found = firstParallel(child);
951
+ if (found !== undefined) return found;
952
+ }
953
+ return undefined;
320
954
  };
321
- for (const child of Object.values(node.states ?? {})) descend(child);
955
+ const visit = (node: Node): void => {
956
+ const agent = node.invoke.find((inv) => typeof inv.src === "string" && isAgent(actors[inv.src]))?.src;
957
+ const parallel = agent === undefined ? undefined : firstParallel(node);
958
+ if (parallel !== undefined) {
959
+ throw new Error(
960
+ `machine "${machine.id}": state "${parallel.id}" is parallel, at or below Agent "${String(agent)}"'s state ` +
961
+ `"${node.id}" — a parallel region below an Agent's state is reserved for picks the Agent does not wait ` +
962
+ `for, which jr2 does not build; background work goes above the Agent's state`,
963
+ );
964
+ }
965
+ for (const child of Object.values(node.states)) visit(child);
966
+ };
967
+ visit(machine.root);
968
+ }
322
969
 
323
- const buried = pick(below, "agent");
324
- if (buried.length === 0) return;
325
- const state = path.length ? path.join(".") : "(the machine root)";
326
- throw new Error(
327
- `agent "${agentName}" is invoked on state "${state}", which derives an EMPTY Menu while the ` +
328
- `states BELOW it handle ${buried.join(", ")} — a Menu is the invoking state's own plus its ` +
329
- `ancestors' transitions (ADR-0015), so a pick written below is one the Turn can never be ` +
330
- `offered, and a Turn with no Menu ends with no pick at all. Move it onto "${state}" (guard ` +
331
- `it if it must not always be offered — ADR-0029), or, if this Turn is deliberately ` +
332
- `menu-less, the pick belongs to another invoke.`,
333
- );
970
+ /** The built state node at a key path below the root — what turns a walk's path into a state id. */
971
+ function stateAt(machine: AnyStateMachine, path: readonly string[]): AnyStateMachine["root"] {
972
+ let node = machine.root;
973
+ for (const key of path) node = node.states[key]!;
974
+ return node;
975
+ }
976
+
977
+ /** A state's own workflow-event keys: dotted names are jr2's or xstate's, `*` is the wildcard. */
978
+ function ownPicks(node: LooseState): string[] {
979
+ return Object.keys(node.on ?? {}).filter((key) => !key.includes(".") && key !== "*");
980
+ }
981
+
982
+ /** The parties a state invokes: its Agent slots by name, and whether it invokes a `gate`. */
983
+ function partiesOf(node: LooseState, actors: Record<string, UnknownActorLogic>): { agents: string[]; gate: boolean } {
984
+ const invokes = node.invoke ? (Array.isArray(node.invoke) ? node.invoke : [node.invoke]) : [];
985
+ return {
986
+ agents: invokes.flatMap((inv) => (typeof inv?.src === "string" && isAgent(actors[inv.src]) ? [inv.src] : [])),
987
+ gate: invokes.some((inv) => inv?.src === "gate"),
988
+ };
989
+ }
990
+
991
+ /** Does this state invoke a party that sends this Machine's workflow events — an Agent slot or a
992
+ * `gate`? An Effect, a child Machine, a `workspace()`/`pool()` body: none of them sends them. */
993
+ function invokesParty(node: LooseState, actors: Record<string, UnknownActorLogic>): boolean {
994
+ const { agents, gate } = partiesOf(node, actors);
995
+ return gate || agents.length > 0;
996
+ }
997
+
998
+ /**
999
+ * The picks written on the states BELOW an Agent's state — its modes (ADR-0015). xstate hands an
1000
+ * event to the deepest active state that handles it, and a false guard there falls through to the
1001
+ * parent, so a child's `on` keys are picks the Turn can be allowed while that child is active — and
1002
+ * `can()` on the live snapshot already answers for the active child (ADR-0029).
1003
+ *
1004
+ * The descent STOPS at a child that invokes another party — an Agent slot (a nested Turn: its picks
1005
+ * are its own) or a `gate` (its `on` keys are the Gate's accepted set) — and leaves out everything
1006
+ * below it. A child that runs an Effect, a child Machine or a timer is no stop: its `on` keys are
1007
+ * this Turn's.
1008
+ */
1009
+ function modePicks(node: LooseState, actors: Record<string, UnknownActorLogic>): Set<string> {
1010
+ return new Set(modeWritings(node, [], actors).map((w) => w.name));
1011
+ }
1012
+
1013
+ /** Each writing {@link modePicks} reads, with the key path of the mode it is written on. */
1014
+ function modeWritings(
1015
+ node: LooseState,
1016
+ path: readonly string[],
1017
+ actors: Record<string, UnknownActorLogic>,
1018
+ ): Array<{ name: string; path: readonly string[] }> {
1019
+ const out: Array<{ name: string; path: readonly string[] }> = [];
1020
+ const descend = (n: LooseState, at: readonly string[]): void => {
1021
+ for (const [key, child] of Object.entries(n.states ?? {})) {
1022
+ if (invokesParty(child, actors)) continue;
1023
+ const childPath = [...at, key];
1024
+ for (const name of ownPicks(child)) out.push({ name, path: childPath });
1025
+ descend(child, childPath);
1026
+ }
1027
+ };
1028
+ descend(node, path);
1029
+ return out;
334
1030
  }
335
1031
 
336
1032
  const resolveInput = (orig: unknown, args: InputArgs): Record<string, unknown> =>
@@ -367,9 +1063,9 @@ function refuseUnknownKeys(consumer: object, agentName: string): void {
367
1063
  );
368
1064
  }
369
1065
 
370
- /** Wrap an Agent slot's invoke input: name the Agent (the slot key), append the derived menu, and
371
- * finalize the mechanism fields. */
372
- function wrapAgentInput(orig: unknown, derived: string[], agentName: string) {
1066
+ /** Wrap an Agent slot's invoke input: name the Agent (the slot key), append the state's derived set
1067
+ * and the Agent's Menu, and finalize the mechanism fields. */
1068
+ function wrapAgentInput(orig: unknown, derived: string[], agentName: string, menuOf: (agentName: string) => string[]) {
373
1069
  return (args: InputArgs): AgentRunInput => {
374
1070
  const consumer = resolveInput(orig, args) as Partial<AgentTurnInput & AgentRunInput>;
375
1071
  refuseUnknownKeys(consumer, agentName);
@@ -378,6 +1074,9 @@ function wrapAgentInput(orig: unknown, derived: string[], agentName: string) {
378
1074
  // jr2 mints every id (ADR-0016, ADR-0057): the Turn's input carries no id to honor, so
379
1075
  // nothing a Machine writes can address a conversation jr2 did not derive.
380
1076
  instanceId: mintIid(consumer, agentName, args.self),
1077
+ // One per invocation (ADR-0016): two Turns that continue one conversation share the iid, the
1078
+ // admission ledger's key, and a restore must not re-attach one Turn to the other's admission.
1079
+ turnId: randomUUID(),
381
1080
  endpoint: consumer.endpoint,
382
1081
  sandbox: consumer.sandbox,
383
1082
  prompt: consumer.prompt,
@@ -391,8 +1090,11 @@ function wrapAgentInput(orig: unknown, derived: string[], agentName: string) {
391
1090
  // ADR-0035's reroll gate: closed to `continue: true`, which names an EXISTING conversation
392
1091
  // while the runaway's one recovery is a fresh one — exactly what it opted out of.
393
1092
  ...(consumer.continue === true ? { continuation: true } : {}),
394
- // The Menu (ADR-0015): always the derived one, since ADR-0057 retired the override.
1093
+ // The state's derived set (ADR-0015): always the derived one, since ADR-0057 retired the
1094
+ // override. What delivery validates against, and what the Allowed picks narrow.
395
1095
  tools: derived,
1096
+ // The Menu (ADR-0029): every pick any state invoking this Agent derives — the tools block.
1097
+ menu: menuOf(agentName),
396
1098
  };
397
1099
  };
398
1100
  }