@jr2/orchestrator 0.6.0 → 0.7.0

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 (301) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +86 -9
  14. package/docs/concepts/architecture.md +41 -28
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +7 -5
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +4 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +50 -22
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +5 -4
  23. package/docs/concepts/pools-and-sources.md +187 -11
  24. package/docs/concepts/secrets-and-trust.md +73 -33
  25. package/docs/concepts/the-menu.md +11 -0
  26. package/docs/concepts/workspaces.md +6 -5
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/guides/add-tools-to-the-sandbox.md +55 -11
  32. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  33. package/docs/guides/answer-a-gate.md +74 -37
  34. package/docs/guides/bind-open-parts.md +24 -8
  35. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  36. package/docs/guides/cancel-a-run.md +26 -16
  37. package/docs/guides/fix-a-failed-converge.md +18 -6
  38. package/docs/guides/give-a-caller-a-scope.md +203 -0
  39. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  41. package/docs/guides/let-an-agent-write-a-chart.md +9 -6
  42. package/docs/guides/look-inside-a-sandbox.md +101 -31
  43. package/docs/guides/open-the-console.md +63 -14
  44. package/docs/guides/read-a-conversation.md +225 -0
  45. package/docs/guides/run-a-chart-by-name.md +17 -16
  46. package/docs/guides/run-one-worker-per-item.md +15 -9
  47. package/docs/guides/set-an-agents-access-and-dials.md +2 -0
  48. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  49. package/docs/guides/start-a-run-from-a-run.md +18 -16
  50. package/docs/guides/start-a-run.md +24 -18
  51. package/docs/guides/steer-an-agent.md +332 -0
  52. package/docs/guides/use-a-hosted-provider.md +1 -1
  53. package/docs/guides/watch-a-run.md +127 -121
  54. package/docs/guides/write-a-hook-kind.md +238 -0
  55. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  56. package/docs/guides/write-a-workflow.md +17 -7
  57. package/docs/llms.txt +81 -15
  58. package/docs/reference/authoring-surface/agent.md +1 -1
  59. package/docs/reference/authoring-surface/answer.md +1 -1
  60. package/docs/reference/authoring-surface/arg.md +1 -1
  61. package/docs/reference/authoring-surface/basic.md +1 -1
  62. package/docs/reference/authoring-surface/bearer.md +1 -1
  63. package/docs/reference/authoring-surface/branchname.md +1 -1
  64. package/docs/reference/authoring-surface/carry.md +23 -0
  65. package/docs/reference/authoring-surface/chartschema.md +1 -1
  66. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  67. package/docs/reference/authoring-surface/command.md +1 -1
  68. package/docs/reference/authoring-surface/customize.md +4 -1
  69. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  70. package/docs/reference/authoring-surface/defineevent.md +1 -1
  71. package/docs/reference/authoring-surface/dispatch.md +5 -4
  72. package/docs/reference/authoring-surface/fail.md +1 -1
  73. package/docs/reference/authoring-surface/file.md +1 -1
  74. package/docs/reference/authoring-surface/frompick.md +1 -1
  75. package/docs/reference/authoring-surface/gate.md +118 -0
  76. package/docs/reference/authoring-surface/group-agents.md +15 -0
  77. package/docs/reference/authoring-surface/group-charts.md +16 -0
  78. package/docs/reference/authoring-surface/group-composition.md +25 -0
  79. package/docs/reference/authoring-surface/group-config.md +17 -0
  80. package/docs/reference/authoring-surface/group-effects.md +48 -0
  81. package/docs/reference/authoring-surface/group-failure.md +13 -0
  82. package/docs/reference/authoring-surface/group-gates.md +16 -0
  83. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  84. package/docs/reference/authoring-surface/group-steering.md +12 -0
  85. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  86. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  87. package/docs/reference/authoring-surface/http.md +1 -1
  88. package/docs/reference/authoring-surface/isfailure.md +1 -1
  89. package/docs/reference/authoring-surface/isready.md +1 -1
  90. package/docs/reference/authoring-surface/jr2setup.md +13 -3
  91. package/docs/reference/authoring-surface/mcp.md +107 -0
  92. package/docs/reference/authoring-surface/open.md +5 -3
  93. package/docs/reference/authoring-surface/openbody.md +1 -1
  94. package/docs/reference/authoring-surface/paramsof.md +1 -1
  95. package/docs/reference/authoring-surface/pool.md +77 -9
  96. package/docs/reference/authoring-surface/push.md +1 -1
  97. package/docs/reference/authoring-surface/readyof.md +1 -1
  98. package/docs/reference/authoring-surface/reread.md +19 -0
  99. package/docs/reference/authoring-surface/secret.md +1 -1
  100. package/docs/reference/authoring-surface/sentby.md +1 -1
  101. package/docs/reference/authoring-surface/skip.md +22 -0
  102. package/docs/reference/authoring-surface/source.md +56 -1
  103. package/docs/reference/authoring-surface/steer.md +93 -0
  104. package/docs/reference/authoring-surface/text.md +1 -1
  105. package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
  106. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  107. package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
  108. package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
  109. package/docs/reference/authoring-surface/type-binding.md +1 -1
  110. package/docs/reference/authoring-surface/type-caller.md +40 -0
  111. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  112. package/docs/reference/authoring-surface/type-carry.md +19 -0
  113. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  114. package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
  115. package/docs/reference/authoring-surface/type-customize.md +16 -1
  116. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  117. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  118. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  119. package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
  120. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  121. package/docs/reference/authoring-surface/type-framepath.md +1 -1
  122. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  123. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  124. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  125. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  126. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  127. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  128. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  129. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  130. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  131. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  132. package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
  133. package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
  134. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  135. package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
  136. package/docs/reference/authoring-surface/type-lends.md +5 -1
  137. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  138. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  139. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  140. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  141. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  142. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  143. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  144. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  145. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  146. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  147. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  148. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  149. package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
  150. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  151. package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
  152. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  153. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  154. package/docs/reference/authoring-surface/type-reposlot.md +1 -1
  155. package/docs/reference/authoring-surface/type-reread.md +18 -0
  156. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  157. package/docs/reference/authoring-surface/type-sender.md +26 -1
  158. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  159. package/docs/reference/authoring-surface/type-size.md +1 -1
  160. package/docs/reference/authoring-surface/type-skip.md +19 -0
  161. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  162. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  163. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  164. package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
  165. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  166. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  167. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  168. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  169. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  170. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  171. package/docs/reference/authoring-surface/type-workspaced.md +1 -1
  172. package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
  173. package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
  174. package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
  175. package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
  176. package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
  177. package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
  178. package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
  179. package/docs/reference/authoring-surface/workspace.md +1 -1
  180. package/docs/reference/authoring-surface/worktree.md +1 -1
  181. package/docs/reference/authoring-surface.md +88 -167
  182. package/docs/reference/cli.md +413 -74
  183. package/docs/reference/config.md +235 -2
  184. package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
  185. package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
  186. package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
  187. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  188. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  189. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  190. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  191. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
  192. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
  193. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  194. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
  195. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
  196. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  197. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
  198. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
  199. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  200. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
  201. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
  202. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  203. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
  204. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
  205. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  206. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  207. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
  208. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  209. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
  210. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  211. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
  212. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
  213. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  214. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  215. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  216. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
  217. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
  218. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
  219. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
  220. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
  221. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  222. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  223. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
  224. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
  225. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
  226. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  227. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
  228. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  229. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  230. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
  231. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
  232. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  233. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  234. package/docs/reference/decisions.md +6 -4
  235. package/docs/reference/glossary.md +96 -42
  236. package/docs/reference/hooks.md +1077 -0
  237. package/docs/reference/http-api.md +976 -149
  238. package/docs/why.md +2 -1
  239. package/package.json +10 -3
  240. package/src/actor.ts +36 -10
  241. package/src/agent.ts +50 -0
  242. package/src/assembly-checks.ts +530 -4
  243. package/src/callers.ts +245 -0
  244. package/src/chart.ts +52 -7
  245. package/src/config.ts +550 -1
  246. package/src/conversations.ts +453 -0
  247. package/src/custodian.ts +8 -2
  248. package/src/customize.ts +143 -4
  249. package/src/dispatch-effect.ts +142 -26
  250. package/src/durability.ts +2 -0
  251. package/src/duration.ts +20 -0
  252. package/src/effect-checks.ts +2 -1
  253. package/src/effect.ts +60 -7
  254. package/src/effector-wire.ts +272 -27
  255. package/src/gate-brand.ts +16 -0
  256. package/src/gate.ts +634 -48
  257. package/src/harness-client.ts +10 -4
  258. package/src/held-secrets.ts +72 -23
  259. package/src/holds.ts +2 -2
  260. package/src/hook-kinds/discord.ts +92 -0
  261. package/src/hook-kinds/door.ts +107 -0
  262. package/src/hook-kinds/github.ts +89 -0
  263. package/src/hook-kinds/gitlab.ts +72 -0
  264. package/src/hook-kinds/hook.ts +7 -0
  265. package/src/hook-kinds/index.ts +465 -0
  266. package/src/hook-kinds/slack.ts +151 -0
  267. package/src/hook-kinds/webhook.ts +113 -0
  268. package/src/hooks.ts +506 -0
  269. package/src/http-effect.ts +76 -38
  270. package/src/http.ts +889 -94
  271. package/src/index.ts +16 -5
  272. package/src/instance.ts +196 -26
  273. package/src/internal.ts +22 -3
  274. package/src/machine-doc.ts +26 -1
  275. package/src/mcp-effect.ts +424 -0
  276. package/src/mount-digest.ts +41 -7
  277. package/src/names.ts +64 -2
  278. package/src/notices.ts +138 -10
  279. package/src/open-body.ts +25 -11
  280. package/src/open-effect.ts +17 -4
  281. package/src/open.ts +4 -2
  282. package/src/parts.ts +87 -9
  283. package/src/party.ts +2 -1
  284. package/src/pick.ts +2 -2
  285. package/src/pool.ts +1142 -163
  286. package/src/registration.ts +203 -13
  287. package/src/run-host.ts +1509 -171
  288. package/src/run-list.ts +191 -0
  289. package/src/sandbox-kube.ts +53 -5
  290. package/src/secret-placement.ts +239 -33
  291. package/src/server.ts +53 -4
  292. package/src/setup.ts +72 -32
  293. package/src/snapshot-store.ts +565 -48
  294. package/src/steer.ts +111 -0
  295. package/src/stub-harness.ts +6 -2
  296. package/src/tokens.ts +263 -22
  297. package/src/wire.ts +17 -0
  298. package/src/workspace.ts +50 -6
  299. package/src/worktree.ts +3 -3
  300. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  301. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/notices.ts CHANGED
@@ -27,8 +27,51 @@
27
27
  // a second read in the same conversation cannot carry it too — and a restart forgets reservations
28
28
  // along with everything else in memory. jr2 keeps no "handled" state: whether an Agent acted on a
29
29
  // notice is not the ledger's concern.
30
+ //
31
+ // The same ledger keeps Steering messages (ADR-0002): one transport, two kinds of body. Each entry is
32
+ // an ENVELOPE — the address, the source, the audience, what is reserved and heard — and a BODY: a
33
+ // Notice, or a Steering message with its author. The address is a tagged value: the body's `scope`
34
+ // names its kind and `to` its value — `conversation` (a continued conversation's base id),
35
+ // `workspace` (a Sandbox name), or `slot` (an Agent slot of one Machine instance, ADR-0076). A
36
+ // message to a slot has one taker, like a conversation notice: the slot's open Turn at a step
37
+ // boundary, or else its next Turn's admission, whose conversation does not exist yet when it is
38
+ // sent. It RETIRES, recorded "not delivered", when its Machine instance ends (`retire`). The ledger
39
+ // also keeps the record of each message (`steering`): its author and its outcome.
40
+
41
+ import type { Notice, SteeringAuthor, SteeringMessage } from "./wire.ts";
42
+
43
+ /** One body of the ledger: a Notice (a fact with no author) or a Steering message (an author's
44
+ * text). The body's `scope` is the kind of its address. */
45
+ export type LedgerBody = Notice | SteeringMessage;
46
+
47
+ /** What became of one Steering message (ADR-0002): `held` until a Turn of its slot hears it,
48
+ * `heard` once one did, `not-delivered` when its Machine instance ended first. */
49
+ export type SteeringOutcome = "held" | "heard" | "not-delivered";
50
+
51
+ /** The run's record of one Steering message: the slot it went to, its author, its text, when it was
52
+ * sent, and what became of it. */
53
+ export type SteeringRecord = {
54
+ id: string;
55
+ agent: string;
56
+ author: SteeringAuthor;
57
+ text: string;
58
+ at: string;
59
+ outcome: SteeringOutcome;
60
+ };
61
+
62
+ /** How many Steering messages a run's record keeps: the newest, and every held one. A message is at
63
+ * most 16 KiB, so the record stays under about 1.6 MiB of a run's blob. */
64
+ export const STEERING_RECORD_MAX = 100;
30
65
 
31
- import type { Notice } from "./wire.ts";
66
+ /** The Machine path of a slot (`<machine path>/<agent>`): what ends with its Machine instance. */
67
+ export const slotMachine = (slot: string): string => slot.slice(0, Math.max(0, slot.lastIndexOf("/")));
68
+
69
+ const isSteering = (body: LedgerBody): body is SteeringMessage => body.kind === "steering";
70
+
71
+ /** Whether a record is the record of this message to this slot: a message that an unheard
72
+ * admission carried is put back as a new entry, and its record stays the same. */
73
+ const recordOf = (r: SteeringRecord, body: SteeringMessage, slot: string): boolean =>
74
+ r.agent === slot && r.at === body.at && r.text === body.text;
32
75
 
33
76
  /** Where an admission, or a step boundary, may hear notices: its continued conversation's base
34
77
  * Instance ID (ADR-0057) and its Workspace's Sandbox name. Either may be absent — a fresh Turn has no
@@ -70,7 +113,8 @@ export const AUDIENCE_SKEW_MS = 2_000;
70
113
  * own `bash` answer told them. */
71
114
  export type PendingNotice = {
72
115
  id: string;
73
- notice: Notice;
116
+ /** The body: a Notice or a Steering message. */
117
+ notice: LedgerBody;
74
118
  to: string;
75
119
  source?: string;
76
120
  audience?: string[];
@@ -93,15 +137,17 @@ export type NoticeLedgerState = {
93
137
  /** Each retry ever recorded, by source and slot: a replayed raise records it once. */
94
138
  retriesSeen?: string[];
95
139
  members?: Record<string, NoticeMember[]>;
140
+ /** The record of the run's Steering messages, oldest first (ADR-0002). */
141
+ steering?: SteeringRecord[];
96
142
  };
97
143
 
98
144
  /** What one admission takes: the notices it carries, and their ids for the ledger write. */
99
145
  export type TakenNotices = {
100
146
  ids: string[];
101
- notices: Notice[];
147
+ notices: LedgerBody[];
102
148
  /** Every notice taken, in the order of `ids`, with the source it was raised with — what `restore`
103
149
  * puts back when no model heard the admission. */
104
- raised?: Array<{ notice: Notice; source?: string }>;
150
+ raised?: Array<{ notice: LedgerBody; source?: string }>;
105
151
  };
106
152
 
107
153
  /** What a Workspace notice's raise knows of its kill: the cut Turn's retry, the conversations its
@@ -129,6 +175,7 @@ export class NoticeLedger {
129
175
  * retry `retried`. */
130
176
  readonly #asRetry = new Map<string, string>();
131
177
  readonly #newId: () => string;
178
+ readonly #steering: SteeringRecord[];
132
179
 
133
180
  constructor(state: NoticeLedgerState | undefined, newId: () => string) {
134
181
  this.#pending = (state?.pending ?? []).map((p) => ({
@@ -142,6 +189,71 @@ export class NoticeLedger {
142
189
  this.#retriesSeen = new Set(state?.retriesSeen ?? []);
143
190
  this.#members = new Map(Object.entries(state?.members ?? {}).map(([ws, ms]) => [ws, ms.map((m) => ({ ...m }))]));
144
191
  this.#newId = newId;
192
+ this.#steering = (state?.steering ?? []).map((r) => ({ ...r }));
193
+ }
194
+
195
+ /** Hold a Steering message for `slot` (ADR-0002): its open Turn hears it at a step boundary, or
196
+ * else its next Turn's admission carries it. Recorded `held`. Answers its id. */
197
+ steer(message: SteeringMessage, slot: string): string {
198
+ const id = this.#newId();
199
+ this.#pending.push({ id, notice: message, to: slot });
200
+ this.#steering.push({
201
+ id,
202
+ agent: slot,
203
+ author: message.author,
204
+ text: message.text,
205
+ at: message.at,
206
+ outcome: "held",
207
+ });
208
+ this.#trimRecord();
209
+ return id;
210
+ }
211
+
212
+ /** The Machine instance at `machine` (a slot's Machine path) ended (ADR-0002): every message held
213
+ * for a slot of it, or of a Machine below it, retires, recorded `not-delivered`. With no
214
+ * `machine`, the run ended, and every held message retires. Answers whether anything changed. */
215
+ retire(machine?: string): boolean {
216
+ let changed = false;
217
+ for (const p of [...this.#pending]) {
218
+ if (!isSteering(p.notice)) continue;
219
+ const at = slotMachine(p.to);
220
+ if (machine !== undefined && at !== machine && !at.startsWith(`${machine}.`)) continue;
221
+ this.#pending.splice(this.#pending.indexOf(p), 1);
222
+ this.#reserved.delete(p.id);
223
+ this.#outcome(p.notice, p.to, "not-delivered");
224
+ changed = true;
225
+ }
226
+ return changed;
227
+ }
228
+
229
+ /** The run's record of its Steering messages, oldest first. */
230
+ steeringRecord(): SteeringRecord[] {
231
+ return this.#steering.map((r) => ({ ...r }));
232
+ }
233
+
234
+ /** The outcome of a message, on its record. */
235
+ #outcome(body: SteeringMessage, slot: string, outcome: SteeringOutcome): void {
236
+ const record = this.#steering.find((r) => recordOf(r, body, slot));
237
+ if (record !== undefined) record.outcome = outcome;
238
+ }
239
+
240
+ /** Whether a message's record says that it retired: one put back after its Machine ended stays
241
+ * retired. */
242
+ #retired(body: SteeringMessage, slot: string): boolean {
243
+ return this.#steering.some((r) => r.outcome === "not-delivered" && recordOf(r, body, slot));
244
+ }
245
+
246
+ /** Keep the newest {@link STEERING_RECORD_MAX} records, and every held one. */
247
+ #trimRecord(): void {
248
+ let over = this.#steering.length - STEERING_RECORD_MAX;
249
+ for (let i = 0; over > 0 && i < this.#steering.length; ) {
250
+ if (this.#steering[i]!.outcome === "held") {
251
+ i++;
252
+ continue;
253
+ }
254
+ this.#steering.splice(i, 1);
255
+ over--;
256
+ }
145
257
  }
146
258
 
147
259
  /** Raise a notice for `to` (a conversation's base id, or a Sandbox name — the notice's own
@@ -240,7 +352,7 @@ export class NoticeLedger {
240
352
 
241
353
  /** What {@link take} would reserve in this scope, reserving nothing: a step boundary asks first
242
354
  * whether a guard kill is among them, and so whether it must learn who was told. */
243
- peek(scope: NoticeScope): Array<{ id: string; notice: Notice; source?: string }> {
355
+ peek(scope: NoticeScope): Array<{ id: string; notice: LedgerBody; source?: string }> {
244
356
  return this.#eligible(scope, scope.taker ?? scope.conversation).map((p) => ({
245
357
  id: p.id,
246
358
  notice: p.notice,
@@ -253,7 +365,17 @@ export class NoticeLedger {
253
365
  * for the other conversations: it is pending again for `taker`, the conversation that did not hear
254
366
  * it, which is a member of its audience again. Nothing changes when the notice is pending for that
255
367
  * conversation already. Answers whether anything changed. */
256
- restore(notice: Notice, to: string, source?: string, taker?: string): boolean {
368
+ restore(notice: LedgerBody, to: string, source?: string, taker?: string): boolean {
369
+ // A Steering message has one taker: pending again for its slot, unless its Machine instance
370
+ // ended while the admission that carried it was unheard.
371
+ if (isSteering(notice)) {
372
+ const same = (p: PendingNotice) =>
373
+ p.to === to && isSteering(p.notice) && p.notice.at === notice.at && p.notice.text === notice.text;
374
+ if (this.#retired(notice, to) || this.#pending.some(same)) return false;
375
+ this.#pending.push({ id: this.#newId(), notice, to });
376
+ this.#outcome(notice, to, "held");
377
+ return true;
378
+ }
257
379
  const pending =
258
380
  source === undefined
259
381
  ? notice.scope === "workspace" && taker !== undefined
@@ -303,6 +425,7 @@ export class NoticeLedger {
303
425
  this.#reserved.delete(key);
304
426
  if (p.notice.scope !== "workspace") {
305
427
  this.#pending.splice(at, 1);
428
+ if (isSteering(p.notice)) this.#outcome(p.notice, p.to, "heard");
306
429
  continue;
307
430
  }
308
431
  const slot = this.#asRetry.get(key);
@@ -421,6 +544,7 @@ export class NoticeLedger {
421
544
  ...(this.#members.size === 0
422
545
  ? {}
423
546
  : { members: Object.fromEntries([...this.#members].map(([ws, ms]) => [ws, ms.map((m) => ({ ...m }))])) }),
547
+ ...(this.#steering.length === 0 ? {} : { steering: this.#steering.map((r) => ({ ...r })) }),
424
548
  };
425
549
  }
426
550
 
@@ -434,10 +558,14 @@ export class NoticeLedger {
434
558
  (scope.exceptPrefix === undefined || p.source === undefined || !p.source.startsWith(scope.exceptPrefix)) &&
435
559
  (p.notice.scope === "conversation"
436
560
  ? p.to === scope.conversation
437
- : taker !== undefined &&
438
- p.to === scope.workspace &&
439
- !(p.heardBy ?? []).includes(taker) &&
440
- (p.audience === undefined || p.audience.includes(taker) || this.#retryFor(p, scope, taker) !== undefined)),
561
+ : p.notice.scope === "slot"
562
+ ? p.to === scope.slot
563
+ : taker !== undefined &&
564
+ p.to === scope.workspace &&
565
+ !(p.heardBy ?? []).includes(taker) &&
566
+ (p.audience === undefined ||
567
+ p.audience.includes(taker) ||
568
+ this.#retryFor(p, scope, taker) !== undefined)),
441
569
  );
442
570
  }
443
571
 
package/src/open-body.ts CHANGED
@@ -51,6 +51,7 @@ import { fail, FAILURE_GUARD, failureGuard } from "./fail.ts";
51
51
  import { agent } from "./harness-client.ts";
52
52
  import { SENT_BY } from "./party.ts";
53
53
  import { composesSandbox, sandboxPartsOf, wrapperBodyOf } from "./parts.ts";
54
+ import { isGateSlot } from "./gate.ts";
54
55
  import { open } from "./open.ts";
55
56
  import { actorPath, boundRun } from "./registration.ts";
56
57
  import { COMPILED_CHART, jr2Setup } from "./setup.ts";
@@ -115,7 +116,9 @@ export function jsonOver(value: unknown, cap: number): boolean {
115
116
  }
116
117
 
117
118
  /** The parts of the Chart wrapper that a Chart can use, by their keys in `actors`, `guards`,
118
- * `actions` and `delays`. `gate: true` lends the Gate, so that a Chart can wait for an answer from
119
+ * `actions` and `delays`. `gates` lends Gate slots (`gate({ … })`) that hear a Hook, by slot name:
120
+ * the door refuses a Chart that invokes one whose `delivers` names an event that is not in the
121
+ * Chart's `events`. `gate: true` lends the Gate, so that a Chart can wait for an answer from
119
122
  * outside the run. `fail: true` lends the Part that fails the run, so that a Chart can end its run
120
123
  * as a Failure (ADR-0073): the Machine decides which ends a Chart can choose. A Chart can use no
121
124
  * other part. */
@@ -123,6 +126,7 @@ export type Lends = {
123
126
  agents?: string[];
124
127
  effects?: string[];
125
128
  machines?: string[];
129
+ gates?: string[];
126
130
  guards?: string[];
127
131
  actions?: string[];
128
132
  delays?: string[];
@@ -161,7 +165,7 @@ export type OpenBodySlot = { readonly openBody: OpenBodyDeclaration };
161
165
  /** The logic `openBody()` returns: actor logic that carries the brand. */
162
166
  export type OpenBodyLogic = UnknownActorLogic & OpenBodySlot;
163
167
 
164
- const LEND_KINDS = ["agents", "effects", "machines", "guards", "actions", "delays"] as const;
168
+ const LEND_KINDS = ["agents", "effects", "machines", "gates", "guards", "actions", "delays"] as const;
165
169
  const WORKSPACE: readonly WorkspaceAccess[] = ["write", "read", "none"];
166
170
  const CEILING_KEYS = ["turns", "wallClock", "transitions", "fanOut", "size"] as const;
167
171
  const DURATION = /^(\d+(\.\d+)?)(s|m|h)$/;
@@ -170,9 +174,9 @@ const MAX_WALL_CLOCK_MS = 2_147_483_647;
170
174
  /** A name a Chart's grammar reads as its own, so a lent part by that name would be shadowed. */
171
175
  const reservedName = (kind: (typeof LEND_KINDS)[number], v: string): string | undefined => {
172
176
  if (v.includes(".")) return `"${v}" holds a dot; a lent name is a slot key, and dotted names are jr2's and xstate's`;
173
- if ((kind === "agents" || kind === "effects" || kind === "machines") && v === "gate")
177
+ if ((kind === "agents" || kind === "effects" || kind === "machines" || kind === "gates") && v === "gate")
174
178
  return `"gate" is the kit's Gate; lend a slot of another name`;
175
- if ((kind === "agents" || kind === "effects" || kind === "machines") && v === "fail")
179
+ if ((kind === "agents" || kind === "effects" || kind === "machines" || kind === "gates") && v === "fail")
176
180
  return `"fail" is the kit's Part that fails the run, lent as \`fail: true\`; lend a slot of another name`;
177
181
  if (kind === "actions" && (GENERIC_ACTIONS as readonly string[]).includes(v))
178
182
  return `"${v}" is a generic action every Chart has; lend an action of another name`;
@@ -520,7 +524,12 @@ const EMPTY_LENT: LentParts = { actors: {}, guards: {}, actions: {}, delays: {}
520
524
  /** The lent names of a declaration, by the implementation map each resolves in. */
521
525
  function lentNames(decl: OpenBodyDeclaration) {
522
526
  return {
523
- actors: [...(decl.lends.agents ?? []), ...(decl.lends.effects ?? []), ...(decl.lends.machines ?? [])],
527
+ actors: [
528
+ ...(decl.lends.agents ?? []),
529
+ ...(decl.lends.effects ?? []),
530
+ ...(decl.lends.machines ?? []),
531
+ ...(decl.lends.gates ?? []),
532
+ ],
524
533
  guards: [...(decl.lends.guards ?? [])],
525
534
  actions: [...(decl.lends.actions ?? [])],
526
535
  delays: [...(decl.lends.delays ?? [])],
@@ -977,7 +986,7 @@ function gateInput(value: unknown, self: AnyActorRef): unknown {
977
986
  return { ...(value as Record<string, unknown>), gate: [...actorPath(self), id].join(".") };
978
987
  }
979
988
 
980
- function invokeOf(inv: ChartInvoke, agents: ReadonlySet<string>): unknown {
989
+ function invokeOf(inv: ChartInvoke, agents: ReadonlySet<string>, gates: ReadonlySet<string>): unknown {
981
990
  const out: Record<string, unknown> = { src: inv.src };
982
991
  if (inv.id !== undefined) out.id = inv.id;
983
992
  if (inv.input !== undefined) {
@@ -985,7 +994,7 @@ function invokeOf(inv: ChartInvoke, agents: ReadonlySet<string>): unknown {
985
994
  // Resolved at invoke time, with `{ context, event, input }` (ADR-0070).
986
995
  out.input = agents.has(inv.src)
987
996
  ? (args: Args) => turnInput(resolveAt(template, args), placementOf(args.context?.[CHART_INPUT_KEY]))
988
- : inv.src === "gate"
997
+ : inv.src === "gate" || gates.has(inv.src)
989
998
  ? (args: Args & { self: AnyActorRef }) => gateInput(resolveAt(template, args), args.self)
990
999
  : (args: Args) => resolveAt(template, args);
991
1000
  }
@@ -994,14 +1003,16 @@ function invokeOf(inv: ChartInvoke, agents: ReadonlySet<string>): unknown {
994
1003
  return out;
995
1004
  }
996
1005
 
997
- function stateOf(st: ChartState, agents: ReadonlySet<string>): Record<string, unknown> {
1006
+ function stateOf(st: ChartState, agents: ReadonlySet<string>, gates: ReadonlySet<string>): Record<string, unknown> {
998
1007
  const out: Record<string, unknown> = {};
999
1008
  if (st.type !== undefined) out.type = st.type;
1000
1009
  if (st.initial !== undefined) out.initial = st.initial;
1001
1010
  if (st.history !== undefined) out.history = st.history;
1002
- if (st.states !== undefined) out.states = mapValues(st.states, (child) => stateOf(child, agents));
1011
+ if (st.states !== undefined) out.states = mapValues(st.states, (child) => stateOf(child, agents, gates));
1003
1012
  if (st.invoke !== undefined)
1004
- out.invoke = Array.isArray(st.invoke) ? st.invoke.map((inv) => invokeOf(inv, agents)) : invokeOf(st.invoke, agents);
1013
+ out.invoke = Array.isArray(st.invoke)
1014
+ ? st.invoke.map((inv) => invokeOf(inv, agents, gates))
1015
+ : invokeOf(st.invoke, agents, gates);
1005
1016
  if (st.on !== undefined) out.on = mapValues(st.on, transitionsOf);
1006
1017
  if (st.always !== undefined) out.always = transitionsOf(st.always);
1007
1018
  if (st.after !== undefined) out.after = mapValues(st.after, transitionsOf);
@@ -1051,6 +1062,9 @@ export function compileChart(
1051
1062
  ...Object.keys(lent.actors).filter((slot) => isAgent(lent.actors[slot])),
1052
1063
  ...Object.keys(minted),
1053
1064
  ]);
1065
+ // The lent Gate slots (ADR-0011): an authored gate id sits below the body's actor path, as the kit
1066
+ // Gate's does.
1067
+ const gateSlots = new Set(Object.keys(lent.actors).filter((slot) => isGateSlot(lent.actors[slot])));
1054
1068
  const initialContext = text.context ?? {};
1055
1069
  const config = {
1056
1070
  id: text.id ?? options.id ?? "chart",
@@ -1059,7 +1073,7 @@ export function compileChart(
1059
1073
  [CHART_INPUT_KEY]: input ?? {},
1060
1074
  }),
1061
1075
  initial: text.initial,
1062
- states: mapValues(text.states, (st) => stateOf(st, agentSlots)),
1076
+ states: mapValues(text.states, (st) => stateOf(st, agentSlots, gateSlots)),
1063
1077
  output: ({ context, event }: Args) =>
1064
1078
  ({
1065
1079
  outcome: "done",
@@ -28,8 +28,9 @@ import type { Arg, EffectLogic, EffectParts, EffectSnapshot } from "./effect.ts"
28
28
  import type { Secret, Text } from "./http-effect.ts";
29
29
 
30
30
  /** The parts of an Effect a package may leave Open: an http url, a command's argv, a secret's name,
31
- * a dispatch's target (its url and token together, ADR-0071). */
32
- export type OpenEffectPart = "url" | "argv" | "secret" | "target";
31
+ * a dispatch's target (its url and token together, ADR-0071), an `mcp` Effect's server and tool
32
+ * (ADR-0079). */
33
+ export type OpenEffectPart = "url" | "argv" | "secret" | "target" | "server" | "tool";
33
34
 
34
35
  // What a composer binds an Effect's Open parts to, each key one part (ADR-0054); a dispatch's
35
36
  // target is its url and token, bound as one (ADR-0071). One object for an
@@ -39,7 +40,8 @@ export type OpenEffectPart = "url" | "argv" | "secret" | "target";
39
40
  * The values that bind the Open parts of one Effect. A packaged Machine leaves a part of an Effect
40
41
  * Open when it cannot know it. That part is the url of an `http` Effect, the argv of a `command`,
41
42
  * the name of a secret, written `secret(open)`, or the target of a `dispatch`: the url of another
42
- * Orchestrator and the secret that holds its Instance token, bound as one. You bind it once, where you register the
43
+ * Orchestrator and the secret that holds its Instance token, bound as one, or the server or the tool
44
+ * of an `mcp` Effect. You bind it once, where you register the
43
45
  * Machine, with `customize(machine, { effects: { <slot>: binding } })`. Each key binds one Open
44
46
  * part. For an Effect with two Open parts, one object holds both keys.
45
47
  *
@@ -101,6 +103,11 @@ export type EffectBinding = {
101
103
  * `jr2.config.ts`. The Effector reads it from a file of that name. */
102
104
  readonly secret?: string;
103
105
  readonly target?: { readonly url: string; readonly token: Secret };
106
+ /** The server of an `mcp` Effect whose server is Open: a name of the `mcp` block of
107
+ * `jr2.config.ts`. The server's url and its secret are the config's. */
108
+ readonly server?: string;
109
+ /** The tool of an `mcp.call` Effect whose tool is Open: a tool name of the server. */
110
+ readonly tool?: string;
104
111
  };
105
112
 
106
113
  declare const openPart: unique symbol;
@@ -135,7 +142,11 @@ export function openPartsOf(parts: EffectParts): OpenEffectPart[] {
135
142
  if (parts.kind === "http" && parts.url === open) out.push("url");
136
143
  if (parts.kind === "command" && parts.argv === open) out.push("argv");
137
144
  if (parts.kind === "dispatch" && parts.target === open) out.push("target");
138
- if (parts.kind !== "push" && parts.openSecret) out.push("secret");
145
+ if (parts.kind === "mcp") {
146
+ if (parts.server === open) out.push("server");
147
+ if (parts.tool === open) out.push("tool");
148
+ }
149
+ if (parts.kind !== "push" && parts.kind !== "mcp" && parts.openSecret) out.push("secret");
139
150
  return out;
140
151
  }
141
152
 
@@ -151,6 +162,8 @@ const BINDING_SAMPLE: Record<OpenEffectPart, string> = {
151
162
  argv: 'argv: ["<program>", "<arg>"]',
152
163
  secret: 'secret: "<name>"',
153
164
  target: 'target: { url: "https://<host>", token: secret("<name>") }',
165
+ server: 'server: "<name>"',
166
+ tool: 'tool: "<tool>"',
154
167
  };
155
168
 
156
169
  /** One binding object for these Open parts — `{ url: "…", secret: "<name>" }` for two. */
package/src/open.ts CHANGED
@@ -14,12 +14,14 @@
14
14
  * - a Repo Slot: `repos: { app: open }`, or the whole map: `repos: open`;
15
15
  * - an Agent's model: `agent({ model: open, … })`;
16
16
  * - an Effect's url or argv: `http({ url: open, … })`, `command(open)`;
17
- * - the secret of an Effect's auth, of a header, or of a command's `env`: `secret(open)`.
17
+ * - the secret of an Effect's auth, of a header, or of a command's `env`: `secret(open)`;
18
+ * - the look of a Gate slot: `gate({ …, look: open })`.
18
19
  *
19
20
  * `jr2 up` refuses a Workflow with an Open part that nobody bound, and prints the `customize()`
20
21
  * line that binds it: ``refusing: workflow "<name>" leaves <part> open — bind it where the Machine
21
22
  * is registered: export const machine = customize(<import>, …) (<adr>)``. A run never sees an
22
- * Open part.
23
+ * Open part. An Open look is the one exception: `jr2 up` warns about it and does not refuse,
24
+ * because a person can still answer the Gate with `jr2 send`.
23
25
  *
24
26
  * @example
25
27
  * ```ts
package/src/parts.ts CHANGED
@@ -48,8 +48,9 @@
48
48
  import { basename } from "node:path";
49
49
  import { fileURLToPath } from "node:url";
50
50
  import type { AnyActorRef, AnyStateMachine, StateNode, UnknownActorLogic } from "xstate";
51
- import { isAgent, isOpenAgent, type AgentDeclaration } from "./agent.ts";
52
- import { argvText, isEffect, placementOf, type EffectParts, type Placement } from "./effect.ts";
51
+ import { isAgent, isOpenAgent, type AgentDeclaration, type AgentMcpServer, type WorkspaceAccess } from "./agent.ts";
52
+ import { argvText, isEffect, placementOf, type EffectParts, type EffectSlot, type Placement } from "./effect.ts";
53
+ import { isGateSlot } from "./gate.ts";
53
54
  import { effectBindingLine, openPartsOf, type OpenEffectPart } from "./open-effect.ts";
54
55
  import { isFailSlot } from "./fail.ts";
55
56
  import { isImageContext } from "./images.ts";
@@ -601,6 +602,20 @@ export type CarriedEffect = {
601
602
  input?: Record<string, unknown>;
602
603
  open?: Array<"target">;
603
604
  }
605
+ /** An mcp Effect (ADR-0079): what it asks of which server, by the server's name in the `mcp`
606
+ * block of `jr2.config.ts`. It names no secret of its own: `jr2 up` reads the server's url and
607
+ * secret from the config and places them in the Instance Effector. An Open server or tool is
608
+ * absent until bound. `timeoutMs` is the author's; 0 when the author set none, and the server's
609
+ * `timeout` then applies. */
610
+ | {
611
+ kind: "mcp";
612
+ op: "call" | "read" | "prompt";
613
+ server?: string;
614
+ tool?: string;
615
+ secrets: string[];
616
+ idempotent: boolean;
617
+ open?: Array<"server" | "tool">;
618
+ }
604
619
  );
605
620
 
606
621
  /** One state that can fail the run (ADR-0073): it invokes a `fail()` slot. `state` is the state's
@@ -649,9 +664,22 @@ export type CarriedParts = {
649
664
  /**
650
665
  * The Instance Harness is needed (ADR-0031): an Agent declares `workspace: "none"`, or an Open
651
666
  * body allows it. `models` are the models its Turns may run: those Agents', and those Open bodies'
652
- * allowed ones. Absent when nothing needs it.
667
+ * allowed ones. `servers` are the MCP servers those Agents name (ADR-0079), sorted, deduped.
668
+ * Absent when nothing needs it.
653
669
  */
654
- instanceHarness?: { models: string[] };
670
+ instanceHarness?: { models: string[]; servers: string[] };
671
+ /** Every Agent slot whose definition names an MCP server (ADR-0079), at each `customize()` route
672
+ * the walk reached it by, in walk order: what `jr2 up` checks against the config's `mcp` block. */
673
+ mcpAgents: CarriedMcpAgent[];
674
+ };
675
+
676
+ /** An Agent that names MCP servers (ADR-0079): its slot, the `customize()` route to its Machine,
677
+ * where its Turns run, and its `mcp` list as written. */
678
+ export type CarriedMcpAgent = {
679
+ slot: string;
680
+ path: OpenSlot["path"];
681
+ workspace: WorkspaceAccess;
682
+ mcp: readonly AgentMcpServer[];
655
683
  };
656
684
 
657
685
  /** A machine actor, told apart from a promise/callback/observable one by having a state tree.
@@ -742,6 +770,7 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
742
770
  const openEffects: CarriedParts["openEffects"] = [];
743
771
  const sizes: CarriedSize[] = [];
744
772
  const openBodies: CarriedParts["openBodies"] = [];
773
+ const mcpAgents: CarriedMcpAgent[] = [];
745
774
  // The Machine that declares each Open body: where its lent Agents are read (not carried data).
746
775
  const bodyWrappers = new Map<CarriedParts["openBodies"][number], AnyStateMachine>();
747
776
  let sandboxed = false;
@@ -832,8 +861,11 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
832
861
  const body = wrapperBodyOf(machine);
833
862
  for (const { state, slot } of failingStatesOf(machine)) failures.push({ machine: machine.id, state, slot, path });
834
863
  for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
835
- if (isAgent(logic)) collectAgent(path, name, logic.definition);
836
- else if (isOpenBody(logic)) {
864
+ if (isAgent(logic)) {
865
+ collectAgent(path, name, logic.definition);
866
+ const { mcp, workspace } = logic.definition;
867
+ if (mcp?.length) mcpAgents.push({ slot: name, path, workspace: workspace ?? "write", mcp });
868
+ } else if (isOpenBody(logic)) {
837
869
  const body = { slot: name, declaration: openBodyDeclarationOf(logic), path };
838
870
  openBodies.push(body);
839
871
  bodyWrappers.set(body, machine);
@@ -842,6 +874,10 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
842
874
  effects.push(effect);
843
875
  if (effect.kind !== "push" && effect.open !== undefined)
844
876
  openEffects.push({ slot: name, path, parts: [...effect.open] });
877
+ } else if (lookReadOf(logic)) {
878
+ // A Gate slot's look reads with an http Effect (ADR-0011): the Instance Effector runs it, so
879
+ // `jr2 up` converges the Effector and places its secrets as for any Effect slot.
880
+ effects.push(carriedEffectOf(`${name}.look`, path, route, lookReadOf(logic)!.effect));
845
881
  } else {
846
882
  const child = asMachine(logic);
847
883
  if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name]);
@@ -858,6 +894,7 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
858
894
  const noneBodies = openBodies.filter((b) => bodyTurnsRunIn(bodyWrappers.get(b)!, b.declaration, "instance"));
859
895
  const harnessModels = new Set<string>(menuOnly.map((a) => a.definition.model).filter(bound));
860
896
  for (const body of noneBodies) for (const m of body.declaration.allows.models) harnessModels.add(m);
897
+ const harnessServers = new Set(menuOnly.flatMap((a) => (a.definition.mcp ?? []).map((e) => e.server)));
861
898
  return {
862
899
  agents,
863
900
  failures,
@@ -871,7 +908,10 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
871
908
  composesSandbox: sandboxed,
872
909
  openBodies,
873
910
  models: [...models].sort(),
874
- ...(menuOnly.length || noneBodies.length ? { instanceHarness: { models: [...harnessModels].sort() } } : {}),
911
+ ...(menuOnly.length || noneBodies.length
912
+ ? { instanceHarness: { models: [...harnessModels].sort(), servers: [...harnessServers].sort() } }
913
+ : {}),
914
+ mcpAgents,
875
915
  };
876
916
  }
877
917
 
@@ -924,6 +964,11 @@ export function placedEffectsOf(root: AnyStateMachine): CarriedEffect[] {
924
964
  effects.push(carriedEffectOf(name, path, route, logic.effect));
925
965
  continue;
926
966
  }
967
+ const look = lookReadOf(logic);
968
+ if (look) {
969
+ effects.push(carriedEffectOf(`${name}.look`, path, route, look.effect));
970
+ continue;
971
+ }
927
972
  const child = asMachine(logic);
928
973
  if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
929
974
  }
@@ -976,6 +1021,22 @@ export function carriedEffectOf(
976
1021
  ...(parts.file ? { file: true as const } : {}),
977
1022
  };
978
1023
  }
1024
+ if (parts.kind === "mcp") {
1025
+ return {
1026
+ name,
1027
+ path,
1028
+ route,
1029
+ kind: "mcp",
1030
+ placement,
1031
+ op: parts.op,
1032
+ ...(parts.server === open ? {} : { server: parts.server }),
1033
+ ...(parts.tool === undefined || parts.tool === open ? {} : { tool: parts.tool }),
1034
+ secrets: [],
1035
+ idempotent: parts.idempotent,
1036
+ timeoutMs: parts.timeoutMs ?? 0,
1037
+ ...(openParts.length ? { open: openParts as Array<"server" | "tool"> } : {}),
1038
+ };
1039
+ }
979
1040
  if (parts.kind === "push") {
980
1041
  const { sign, squash, force, pin, timeoutMs, slot } = parts;
981
1042
  return {
@@ -1040,6 +1101,10 @@ export type WorkspacePod = {
1040
1101
  * down to the next `workspace()`, which is a pod of its own, and every model an Open body below
1041
1102
  * it allows a Chart's own Agents when it allows "write" or "read" (ADR-0070). Sorted, deduped. */
1042
1103
  models: string[];
1104
+ /** The MCP servers that the Agents whose Turns run in this pod name (ADR-0079), with the Agent
1105
+ * that names each and its allowlist: what places each server's secret in this pod, and what the
1106
+ * allowlist rule reads. Absent when none names one. */
1107
+ mcp?: Array<{ slot: string; path: OpenSlot["path"]; entry: AgentMcpServer }>;
1043
1108
  /** The Effects placed in this pod's Sandbox Effector (ADR-0068): every `push`, file-body `http` and
1044
1109
  * credentialed `command` slot below the `workspace()`, down to the next `workspace()` — read off
1045
1110
  * their plain data, statically. Empty → the pod gets no Sandbox Effector and no Secret. */
@@ -1076,6 +1141,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1076
1141
  on: Set<AnyStateMachine>,
1077
1142
  effects: Map<string, CarriedEffect>,
1078
1143
  cuts: Set<string>,
1144
+ mcp: NonNullable<WorkspacePod["mcp"]>,
1079
1145
  path: OpenSlot["path"],
1080
1146
  route: PlacementRoute,
1081
1147
  ): void => {
@@ -1083,12 +1149,14 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1083
1149
  on.add(machine);
1084
1150
  const body = wrapperBodyOf(machine);
1085
1151
  const visit = (child: AnyStateMachine, at: OpenSlot["path"], to: PlacementRoute) => {
1086
- if (!composesSandbox(child)) modelsIn(child, models, on, effects, cuts, at, to);
1152
+ if (!composesSandbox(child)) modelsIn(child, models, on, effects, cuts, mcp, at, to);
1087
1153
  };
1088
1154
  for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
1089
1155
  if (isAgent(logic)) {
1090
1156
  const { model, workspace } = logic.definition;
1091
1157
  if (workspace !== "none" && typeof model === "string") models.add(model);
1158
+ // A "none" Agent's servers are the Instance Harness's (ADR-0079), never this pod's.
1159
+ if (workspace !== "none") for (const entry of logic.definition.mcp ?? []) mcp.push({ slot: name, path, entry });
1092
1160
  continue;
1093
1161
  }
1094
1162
  // A Turn under an Open body may run in this pod with any model the body allows (ADR-0070):
@@ -1123,7 +1191,8 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1123
1191
  const models = new Set<string>();
1124
1192
  const effects = new Map<string, CarriedEffect>();
1125
1193
  const cuts = new Set<string>();
1126
- modelsIn(machine, models, new Set(), effects, cuts, path, route);
1194
+ const mcp: NonNullable<WorkspacePod["mcp"]> = [];
1195
+ modelsIn(machine, models, new Set(), effects, cuts, mcp, path, route);
1127
1196
  const parts = sandboxPartsOf(machine);
1128
1197
  const bindings: Record<string, string | undefined> = {};
1129
1198
  if (parts.repos !== open) {
@@ -1139,6 +1208,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1139
1208
  models: [...models].sort(),
1140
1209
  effects: [...effects.values()],
1141
1210
  ...(cuts.size ? { cuts: [...cuts] } : {}),
1211
+ ...(mcp.length ? { mcp } : {}),
1142
1212
  ...(parts.effector ? { effector: { ...parts.effector } } : {}),
1143
1213
  ...(parts.image !== undefined ? { image: parts.image } : {}),
1144
1214
  bindings,
@@ -1157,6 +1227,14 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1157
1227
  return pods;
1158
1228
  }
1159
1229
 
1230
+ /** The read of a Gate slot's bound look, when it is an Effect (ADR-0011); undefined otherwise. */
1231
+ function lookReadOf(logic: unknown): EffectSlot | undefined {
1232
+ if (!isGateSlot(logic)) return undefined;
1233
+ const look = logic.gateSlot.look;
1234
+ if (typeof look !== "object" || look === null) return undefined;
1235
+ return isEffect(look.read) ? look.read : undefined;
1236
+ }
1237
+
1160
1238
  /** Every state of `machine`'s own tree that invokes a `fail()` slot (ADR-0073), in document order,
1161
1239
  * with the slot key: a state that can fail the run. A child Machine's states are its own walk's. */
1162
1240
  export function failingStatesOf(machine: AnyStateMachine): Array<{ state: string; slot: string }> {
package/src/party.ts CHANGED
@@ -19,6 +19,7 @@
19
19
  import type { AnyActorRef, AnyEventObject, AnyMachineSnapshot, AnyStateMachine } from "xstate";
20
20
  import { SENDER_KEY, type Sender } from "@jr2/agent-protocol";
21
21
  import { isAgent } from "./agent.ts";
22
+ import { isGateInvoke } from "./gate-brand.ts";
22
23
  import { vocabularyOf } from "./vocabulary.ts";
23
24
 
24
25
  /** The guard type `sentBy` names: dotted, so no author guard can take the name. */
@@ -138,7 +139,7 @@ export function partiesAt(node: Node, machine: AnyStateMachine): Sender[] {
138
139
  const out: Sender[] = [];
139
140
  for (const inv of node.invoke) {
140
141
  if (typeof inv.src !== "string") continue;
141
- if (inv.src === "gate") out.push({ party: "gate", id: inv.id });
142
+ if (isGateInvoke(inv, actors)) out.push({ party: "gate", id: inv.id });
142
143
  else if (isAgent(actors[inv.src])) out.push({ party: "agent", id: inv.src });
143
144
  }
144
145
  return out;
package/src/pick.ts CHANGED
@@ -424,8 +424,8 @@ export function fromPick<N extends string, I extends z.ZodObject, C, E extends E
424
424
  }
425
425
 
426
426
  /**
427
- * One reply since the pick, as the author reads it: an invoked actor's output, typed by its slot
428
- * (`src`); or an event that answered a state below the Agent's — a Gate's answer, a nested Agent's
427
+ * One reply since the pick, as the author reads it. It is an invoked actor's output, typed by its
428
+ * slot (`src`); or an event that answered a state below the Agent's — a Gate's answer, a nested Agent's
429
429
  * pick — typed by the Machine's events. A reply is from outside the Machine: the events it sends
430
430
  * itself are not links.
431
431
  */