@jr2/orchestrator 0.5.1 → 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.
- package/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- 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
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/gate.ts
CHANGED
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
// The `gate` actor (ADR-0011): a pending external input on a run, as an addressable resource.
|
|
2
2
|
// The symmetric twin of the Agent actor for every NON-agent caller — humans (`jr2 send`, a UI),
|
|
3
|
-
//
|
|
4
|
-
// caller.
|
|
3
|
+
// a Hook, CI. "Human" is policy, not mechanism, so the actor is not named for one caller.
|
|
5
4
|
//
|
|
6
5
|
// A state that needs outside input invokes `gate` with `{ gate?, accepts?, meta? }`. On start
|
|
7
6
|
// the actor resolves the accepted names against the vocabulary of the MACHINE THAT INVOKED IT —
|
|
8
7
|
// `self._parent.logic`, per-Machine scoping (ADR-0011, ADR-0049), which is what lets a nested
|
|
9
8
|
// Machine keep its own `approve` — and registers into the host's table; `GET /runs/:id` then
|
|
10
|
-
// lists the gate (accepts +
|
|
11
|
-
//
|
|
12
|
-
// `POST /runs/:id/gates/:gate/events` validates against the named schema and delivers through
|
|
9
|
+
// lists the gate (accepts + schemas + meta — what a CLI, an inbox UI, or a Hook's handler reads),
|
|
10
|
+
// and `POST /runs/:id/gates/:gate/events` validates against the named schema and delivers through
|
|
13
11
|
// the closure below — the event lands on the state that invoked the gate, at any nesting depth.
|
|
14
12
|
// Leaving the state stops the actor, which destroys the gate: parking IS retention (ADR-0012),
|
|
15
13
|
// and a gone state is a gone surface.
|
|
14
|
+
//
|
|
15
|
+
// THE HOOKED GATE (ADR-0011). `gate({ meta, delivers, hooks, look, reply })` makes a Gate SLOT: the
|
|
16
|
+
// same actor, with parts that the Machine carries. The Machine puts it in `actors` and invokes it by
|
|
17
|
+
// its key. Its parts are code in the Assembly; its input stays data, so a restore registers it again
|
|
18
|
+
// as before. What the slot adds to the plain Gate:
|
|
19
|
+
//
|
|
20
|
+
// - `meta`, a zod object: the Gate checks the invoking state's meta against it when it opens.
|
|
21
|
+
// - `delivers`, the events that its handlers and its look can give. jr2Setup reads them: an event
|
|
22
|
+
// whose def leaves the audience unset reads as `external`, and the walk refuses one that the
|
|
23
|
+
// Gate's derived accepted set does not hold.
|
|
24
|
+
// - `hooks`, by Hook kind id and event key: the Hook's door (`POST /hooks/:name`) calls them. The
|
|
25
|
+
// registration carries them, with `reply`, for the door.
|
|
26
|
+
// - `look`: the Gate's own read of the fact that it waits for, once after it registers, again
|
|
27
|
+
// after each restore (a restore starts this actor again), and every `every` while it is open,
|
|
28
|
+
// until the run takes the event that `until` gives. `look: "none"` for a Gate that hears a
|
|
29
|
+
// command. `look: open` for a packaged Machine, which its composer binds with `customize()`.
|
|
16
30
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
31
|
+
import {
|
|
32
|
+
createActor,
|
|
33
|
+
fromCallback,
|
|
34
|
+
type AnyActorLogic,
|
|
35
|
+
type AnyActorRef,
|
|
36
|
+
type CallbackActorLogic,
|
|
37
|
+
type InputFrom,
|
|
38
|
+
} from "xstate";
|
|
39
|
+
import type { z } from "zod";
|
|
40
|
+
import { SENDER_KEY, type EventDef, type EventFrom } from "@jr2/agent-protocol";
|
|
41
|
+
import type { KindHooks } from "./hook-kinds/index.ts";
|
|
42
|
+
import type { CallerRecord } from "./callers.ts";
|
|
43
|
+
import type { Duration } from "./effect.ts";
|
|
44
|
+
import { isGateSlotLogic } from "./gate-brand.ts";
|
|
45
|
+
import { open } from "./open.ts";
|
|
46
|
+
import {
|
|
47
|
+
actorPath,
|
|
48
|
+
boundRun,
|
|
49
|
+
gateAddress,
|
|
50
|
+
resolveAccepts,
|
|
51
|
+
runBindingOf,
|
|
52
|
+
wouldMove,
|
|
53
|
+
type AnyActorSystem,
|
|
54
|
+
type DeliveredEvent,
|
|
55
|
+
type Registration,
|
|
56
|
+
type SourceSeat,
|
|
57
|
+
} from "./registration.ts";
|
|
19
58
|
|
|
20
59
|
export type GateInput = {
|
|
21
60
|
/**
|
|
@@ -34,51 +73,598 @@ export type GateInput = {
|
|
|
34
73
|
* escape hatch. An unlisted name is an invoke-time error either way.
|
|
35
74
|
*/
|
|
36
75
|
accepts?: readonly string[];
|
|
37
|
-
/** Serializable context for callers: what a UI renders, what a
|
|
76
|
+
/** Serializable context for callers: what a UI renders, what a Hook's handler matches on (prUrl…). */
|
|
38
77
|
meta?: Record<string, unknown>;
|
|
39
78
|
};
|
|
40
79
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
// --- The hooked Gate's types (ADR-0011) -----------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** One plain event of a list of defs: `{ type: "approve", by }`. What a handler, a look's `until`
|
|
83
|
+
* and a Gate's `reply` see. */
|
|
84
|
+
export type Delivered<D extends readonly EventDef[]> = {
|
|
85
|
+
[K in keyof D]: D[K] extends EventDef ? EventFrom<D[K]> : never;
|
|
86
|
+
}[number];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* What a Gate's handler reads beside `payload`: the Gate's meta, and the Caller that the Hook's
|
|
90
|
+
* delivery made (`via: "hook"`, the Hook's name, and `onBehalfOf` from the signed payload).
|
|
91
|
+
*
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
export type GateIn<Meta> = { readonly meta: Meta; readonly caller: CallerRecord };
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A Gate's read of the fact that it waits for (ADR-0011).
|
|
98
|
+
*
|
|
99
|
+
* - `read`: an `http` Effect marked `idempotent: true`, or a promise actor (`fromPromise`), which the
|
|
100
|
+
* Machine can invoke anyway and which a restore runs again. `gate()` refuses any other logic.
|
|
101
|
+
* - `input`: the read's input, from the Gate's meta.
|
|
102
|
+
* - `every`: how often the Gate looks again while it is open. Default: `"10m"`.
|
|
103
|
+
* - `until`: the read's output as one event of `delivers`, or `undefined` for "not yet". Pure and
|
|
104
|
+
* synchronous, as a guard is.
|
|
105
|
+
*
|
|
106
|
+
* The look runs once after the Gate registers, again after each restore, and every `every` while the
|
|
107
|
+
* Gate is open. It stops after the run takes its event, until the Gate opens again. A looked event
|
|
108
|
+
* has the Gate as its Sender and no Caller, and the feed marks it `looked`.
|
|
109
|
+
*/
|
|
110
|
+
export type GateLook<Meta, Ev, R extends AnyActorLogic = AnyActorLogic> = {
|
|
111
|
+
readonly read: R;
|
|
112
|
+
readonly input: (meta: Meta) => InputFrom<R>;
|
|
113
|
+
readonly every?: Duration;
|
|
114
|
+
readonly until: (output: LookOutput<R>, meta: Meta) => Ev | undefined;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The output of a look's read.
|
|
119
|
+
*
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
export type LookOutput<R> = R extends { getInitialSnapshot: (...a: never[]) => { output?: infer O } }
|
|
123
|
+
? Exclude<O, undefined>
|
|
124
|
+
: unknown;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* What a delivery did at one open Gate. The run took the event, or it did not: the handler threw,
|
|
128
|
+
* its event is not in `delivers`, or the run does not take it now. A Gate whose handler gave
|
|
129
|
+
* `undefined` was not a target, and its `reply` does not run.
|
|
130
|
+
*/
|
|
131
|
+
export type GateOutcome<Ev> =
|
|
132
|
+
| { readonly state: "delivered"; readonly event: Ev; readonly run: string }
|
|
133
|
+
| { readonly state: "refused"; readonly event?: Ev; readonly reason: string; readonly run: string };
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The text that the person who acted reads. Plain text: the Hook kind
|
|
137
|
+
* escapes it and puts it in its envelope. Pure and synchronous. `undefined` or `""`: this Gate says
|
|
138
|
+
* nothing. It runs only for a delivery whose answer a person reads (`slack`, `discord`).
|
|
139
|
+
*/
|
|
140
|
+
export type GateReply<Meta, Ev> = (
|
|
141
|
+
outcome: GateOutcome<Ev>,
|
|
142
|
+
at: { readonly meta: Meta; readonly caller: CallerRecord },
|
|
143
|
+
) => string | undefined;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A handler: one event of `delivers`, or `undefined` to ignore the delivery.
|
|
147
|
+
*
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
export type GateHandler<Meta, Ev, P = unknown> = (delivery: GateIn<Meta> & { readonly payload: P }) => Ev | undefined;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The handlers of a Gate slot, by Hook kind id and event key. Each key's payload type comes from the
|
|
154
|
+
* kind's schema (`HookKinds`, `@jr2/orchestrator/hooks`).
|
|
155
|
+
*
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
158
|
+
export type GateHooks<Meta, Ev> = KindHooks<GateIn<Meta>, Ev | undefined>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The parts of a Gate slot, untyped: what the walk, `customize()` and the Hook's door read.
|
|
162
|
+
*
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
export type GateSlotParts = {
|
|
166
|
+
readonly meta?: z.ZodType;
|
|
167
|
+
readonly delivers: readonly EventDef[];
|
|
168
|
+
readonly hooks: Readonly<Record<string, Readonly<Record<string, AnyHandler>>>>;
|
|
169
|
+
readonly look?: LooseLook | typeof open | "none";
|
|
170
|
+
readonly reply?: (outcome: GateOutcome<DeliveredEvent>, at: { meta: unknown; caller: CallerRecord }) => unknown;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A handler, untyped.
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export type AnyHandler = (delivery: { payload: unknown; meta: unknown; caller: CallerRecord }) => unknown;
|
|
179
|
+
/**
|
|
180
|
+
* A bound look, untyped.
|
|
181
|
+
*
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
184
|
+
export type LooseLook = {
|
|
185
|
+
readonly read: AnyActorLogic;
|
|
186
|
+
readonly input: (meta: unknown) => unknown;
|
|
187
|
+
readonly every?: Duration;
|
|
188
|
+
readonly until: (output: unknown, meta: unknown) => unknown;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The input of a Gate slot: the plain Gate's, with its meta typed by the slot's schema.
|
|
193
|
+
*
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
export type GateSlotInput<Meta> = Omit<GateInput, "meta"> &
|
|
197
|
+
({} extends Meta ? { readonly meta?: Meta } : { readonly meta: Meta });
|
|
198
|
+
|
|
199
|
+
declare const gateSlotMark: unique symbol;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A Gate with parts that the Machine carries. Put it in `actors` and invoke it by its key. The
|
|
203
|
+
* brand `gateSlot` holds its parts.
|
|
204
|
+
*/
|
|
205
|
+
export type GateSlot<Meta, Ev> = CallbackActorLogic<
|
|
206
|
+
DeliveredEvent,
|
|
207
|
+
{} extends Meta ? GateSlotInput<Meta> | undefined : GateSlotInput<Meta>
|
|
208
|
+
> & {
|
|
209
|
+
readonly gateSlot: GateSlotParts;
|
|
210
|
+
readonly [gateSlotMark]?: { readonly meta: Meta; readonly ev: Ev };
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Is this actor logic a Gate slot (`gate({ … })`)? A duck test on the brand, as `isAgent` is. */
|
|
214
|
+
export function isGateSlot(logic: unknown): logic is GateSlot<unknown, unknown> {
|
|
215
|
+
return isGateSlotLogic(logic);
|
|
216
|
+
}
|
|
217
|
+
export { isGateInvoke } from "./gate-brand.ts";
|
|
218
|
+
|
|
219
|
+
/** Is this look bound: a read, an input and an `until`? Not `open`, not `"none"`, not absent. */
|
|
220
|
+
export function isBoundLook(look: GateSlotParts["look"]): look is LooseLook {
|
|
221
|
+
return typeof look === "object" && look !== null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The plain Gate, as a value: `src: "gate"`.
|
|
226
|
+
*
|
|
227
|
+
* @internal
|
|
228
|
+
*/
|
|
229
|
+
export type PlainGate = CallbackActorLogic<DeliveredEvent, GateInput | undefined>;
|
|
230
|
+
|
|
231
|
+
// --- The look --------------------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/** How often a look reads again when its Gate names no `every`. */
|
|
234
|
+
export const LOOK_EVERY_DEFAULT: Duration = "10m";
|
|
235
|
+
|
|
236
|
+
/** The milliseconds of a Duration (`"90s"`, `"10m"`, `"2h"`), or undefined. */
|
|
237
|
+
export function lookEveryMs(d: unknown): number | undefined {
|
|
238
|
+
if (typeof d !== "string") return undefined;
|
|
239
|
+
const m = /^(\d+(\.\d+)?)(s|m|h)$/.exec(d);
|
|
240
|
+
if (!m) return undefined;
|
|
241
|
+
const n = Number.parseFloat(m[1]!);
|
|
242
|
+
if (!(n > 0)) return undefined;
|
|
243
|
+
return m[3] === "h" ? n * 3_600_000 : m[3] === "m" ? n * 60_000 : n * 1000;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The longest delay one timer holds: Node clamps a longer one to 1 ms. */
|
|
247
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Does this logic settle on its own — a promise, the one plain xstate actor that ends without being
|
|
251
|
+
* stopped? Asked of its own transition: a promise logic's initial snapshot, sent xstate's resolve
|
|
252
|
+
* event, is `done`. That event name is an xstate internal (5.32.2); a bump that renamed it would
|
|
253
|
+
* make every promise read long-lived, and setup.test.ts pins it against the real constructors.
|
|
254
|
+
*/
|
|
255
|
+
export function settlesOnItsOwn(logic: unknown): boolean {
|
|
256
|
+
const l = logic as {
|
|
257
|
+
getInitialSnapshot?: (scope: unknown, input: unknown) => unknown;
|
|
258
|
+
transition?: (snapshot: unknown, event: unknown, scope: unknown) => { status?: string } | undefined;
|
|
259
|
+
};
|
|
260
|
+
if (typeof l?.getInitialSnapshot !== "function" || typeof l.transition !== "function") return false;
|
|
261
|
+
try {
|
|
262
|
+
const scope = { self: {}, defer: () => {}, emit: () => {} };
|
|
263
|
+
const snapshot = l.getInitialSnapshot(scope, undefined);
|
|
264
|
+
return l.transition(snapshot, { type: "xstate.promise.resolve", data: undefined }, scope)?.status === "done";
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The fault of a look, or undefined when it is one that a Gate can run (ADR-0011, ADR-0067): an
|
|
272
|
+
* object with `read`, `input` and `until`, a positive `every`, and a read that is an `http` Effect
|
|
273
|
+
* marked `idempotent: true` or a promise actor. Any other Effect is refused, because the look runs
|
|
274
|
+
* its read again after each restore and every `every`.
|
|
275
|
+
*/
|
|
276
|
+
export function lookFault(look: unknown): string | undefined {
|
|
277
|
+
if (look === undefined || look === open || look === "none") return undefined;
|
|
278
|
+
if (typeof look !== "object" || look === null) {
|
|
279
|
+
return 'its look is a { read, input, until, every? } object, "none", or open';
|
|
55
280
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
281
|
+
const l = look as Partial<LooseLook>;
|
|
282
|
+
if (typeof l.input !== "function" || typeof l.until !== "function") {
|
|
283
|
+
return "its look has no input or no until function";
|
|
284
|
+
}
|
|
285
|
+
if (l.every !== undefined) {
|
|
286
|
+
const ms = lookEveryMs(l.every);
|
|
287
|
+
if (ms === undefined || ms > MAX_TIMER_MS) {
|
|
288
|
+
return `its look's every ${JSON.stringify(l.every)} is not a positive duration (like "90s", "10m", "2h")`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const effect = (l.read as { effect?: { kind?: unknown; idempotent?: unknown } } | undefined)?.effect;
|
|
292
|
+
if (typeof effect === "object" && effect !== null) {
|
|
293
|
+
if (effect.kind === "http" && effect.idempotent === true) return undefined;
|
|
294
|
+
return (
|
|
295
|
+
`its look reads with ${effect.kind === "http" ? "an http Effect that is not marked idempotent: true" : `a ${String(effect.kind)} Effect`} — ` +
|
|
296
|
+
"a look reads again after each restore and every `every`, so its read is an http Effect marked idempotent: true, " +
|
|
297
|
+
"or a promise (ADR-0067)"
|
|
62
298
|
);
|
|
63
299
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
300
|
+
if (!settlesOnItsOwn(l.read)) {
|
|
301
|
+
return "its look's read is not an http Effect marked idempotent: true, or a promise (fromPromise)";
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The fault of a Gate slot's spec, or undefined. */
|
|
307
|
+
function specFault(spec: unknown): string | undefined {
|
|
308
|
+
if (typeof spec !== "object" || spec === null) return "it takes { meta?, delivers, hooks?, look?, reply? }";
|
|
309
|
+
const s = spec as Record<string, unknown>;
|
|
310
|
+
for (const key of Object.keys(s)) {
|
|
311
|
+
if (!["meta", "delivers", "hooks", "look", "reply"].includes(key)) {
|
|
312
|
+
return `"${key}" is not a part of a Gate slot (meta, delivers, hooks, look, reply)`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (s.meta !== undefined && typeof (s.meta as { safeParse?: unknown }).safeParse !== "function") {
|
|
316
|
+
return "its meta is a zod object";
|
|
317
|
+
}
|
|
318
|
+
if (!Array.isArray(s.delivers) || s.delivers.length === 0) {
|
|
319
|
+
return "its delivers lists at least one event that defineEvent made";
|
|
320
|
+
}
|
|
321
|
+
const names = new Set<string>();
|
|
322
|
+
for (const d of s.delivers as unknown[]) {
|
|
323
|
+
const name = (d as { name?: unknown } | null)?.name;
|
|
324
|
+
if (typeof name !== "string" || typeof (d as { input?: { safeParse?: unknown } }).input?.safeParse !== "function") {
|
|
325
|
+
return "an entry of its delivers is not an event that defineEvent made";
|
|
326
|
+
}
|
|
327
|
+
if (names.has(name)) return `its delivers lists "${name}" twice`;
|
|
328
|
+
names.add(name);
|
|
329
|
+
}
|
|
330
|
+
const hooksFault = hooksTableFault(s.hooks);
|
|
331
|
+
if (hooksFault) return hooksFault;
|
|
332
|
+
if (s.reply !== undefined && typeof s.reply !== "function") return "its reply is a function";
|
|
333
|
+
return lookFault(s.look);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The fault of a handler table, or undefined. A key that holds `*` is refused: a kind's schema
|
|
337
|
+
* key `"block_actions.*"` types each key with that prefix, but the door matches a handler's key
|
|
338
|
+
* exactly, so a handler keyed with `*` would never hear a delivery (ADR-0011). */
|
|
339
|
+
export function hooksTableFault(hooks: unknown): string | undefined {
|
|
340
|
+
if (hooks === undefined) return undefined;
|
|
341
|
+
if (typeof hooks !== "object" || hooks === null) return "its hooks are { <kind>: { <event key>: handler } }";
|
|
342
|
+
for (const [kind, keys] of Object.entries(hooks)) {
|
|
343
|
+
if (typeof keys !== "object" || keys === null) return `its hooks.${kind} is { <event key>: handler }`;
|
|
344
|
+
for (const [key, handler] of Object.entries(keys)) {
|
|
345
|
+
if (typeof handler !== "function") return `its hooks.${kind}["${key}"] is not a function`;
|
|
346
|
+
if (key.includes("*")) {
|
|
347
|
+
return (
|
|
348
|
+
`its hooks.${kind}["${key}"] holds "*", and the door matches an event key exactly, so this handler ` +
|
|
349
|
+
`never hears a delivery — name the full key, such as "block_actions.approve" (ADR-0011)`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Make the parts of a slot from a spec that `specFault` passed. Frozen: the slot's parts never change. */
|
|
358
|
+
function partsOf(spec: Record<string, unknown>): GateSlotParts {
|
|
359
|
+
const hooks: Record<string, Readonly<Record<string, AnyHandler>>> = {};
|
|
360
|
+
for (const [kind, keys] of Object.entries((spec.hooks ?? {}) as Record<string, Record<string, AnyHandler>>)) {
|
|
361
|
+
hooks[kind] = Object.freeze({ ...keys });
|
|
362
|
+
}
|
|
363
|
+
return Object.freeze({
|
|
364
|
+
...(spec.meta !== undefined ? { meta: spec.meta as z.ZodType } : {}),
|
|
365
|
+
delivers: Object.freeze([...(spec.delivers as EventDef[])]),
|
|
366
|
+
hooks: Object.freeze(hooks),
|
|
367
|
+
...(spec.look !== undefined ? { look: spec.look as GateSlotParts["look"] } : {}),
|
|
368
|
+
...(spec.reply !== undefined ? { reply: spec.reply as GateSlotParts["reply"] } : {}),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Build a Gate slot from its parts: the actor logic with the brand. `customize()` builds one again. */
|
|
373
|
+
export function gateSlotOf(parts: GateSlotParts): GateSlot<unknown, unknown> {
|
|
374
|
+
return Object.assign(gateLogic(parts), { gateSlot: parts }) as unknown as GateSlot<unknown, unknown>;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- The actor -------------------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* What a Pool gives its `source` Gate (ADR-0017): the Source's seat, made when the Gate opens, from
|
|
381
|
+
* the Pool's actor (the Gate's parent), the Gate's `sendBack` to it, and the run's actor system.
|
|
382
|
+
*
|
|
383
|
+
* @internal
|
|
384
|
+
*/
|
|
385
|
+
export type SourceSeatOf = (at: {
|
|
386
|
+
pool: AnyActorRef;
|
|
387
|
+
send: (event: DeliveredEvent) => void;
|
|
388
|
+
system: AnyActorSystem;
|
|
389
|
+
}) => SourceSeat;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A Pool's `source` Gate (ADR-0017): a plain Gate that also carries the push half of the Pool's
|
|
393
|
+
* Source on its registration, so the Hook's fan-in reaches it as it reaches any open Gate
|
|
394
|
+
* (ADR-0011). It may accept no event: a Source with no `wake` hears Hooks alone.
|
|
395
|
+
*
|
|
396
|
+
* @internal
|
|
397
|
+
*/
|
|
398
|
+
export function sourceGate(seat: SourceSeatOf): PlainGate {
|
|
399
|
+
return gateLogic(undefined, seat);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function gateLogic(slot: GateSlotParts | undefined, seat?: SourceSeatOf) {
|
|
403
|
+
return fromCallback<DeliveredEvent, GateInput | undefined>(({ input: raw, system, sendBack, self }) => {
|
|
404
|
+
const input = raw ?? {};
|
|
405
|
+
const binding = runBindingOf(system);
|
|
406
|
+
// The derived id is deterministic from machine structure, so recomputing it on every (re)start
|
|
407
|
+
// is restore-stable and needs no input-mapper persistence — the mapper is only load-bearing
|
|
408
|
+
// where minting has a random component (mintIid's fresh-Turn suffix, ADR-0016).
|
|
409
|
+
const path = actorPath(self);
|
|
410
|
+
const id = input.gate ?? path.join(".");
|
|
411
|
+
if (!id) {
|
|
412
|
+
// Only a rootless invocation (createActor(plainGate) directly) has an empty path.
|
|
413
|
+
throw new Error("gate has no derivable id (no parent actor) — pass `gate` explicitly");
|
|
414
|
+
}
|
|
415
|
+
if (!input.accepts?.length && !seat) {
|
|
416
|
+
// Derivation found nothing (or a plain-setup machine passed nothing): a gate accepting no
|
|
417
|
+
// events can never be moved — refuse loudly at invoke rather than park a dead surface.
|
|
418
|
+
throw new Error(
|
|
419
|
+
`gate "${id}" accepts no events — handle at least one external/any event on the ` +
|
|
420
|
+
`invoking state (the accepted set derives from its transitions), or pass \`accepts\` explicitly`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
// A Gate slot checks the invoking state's meta when it opens (ADR-0011): its handlers, its look
|
|
424
|
+
// and its reply read the meta typed, so a meta the schema refuses never reaches them.
|
|
425
|
+
let meta = input.meta;
|
|
426
|
+
if (slot?.meta !== undefined) {
|
|
427
|
+
const parsed = slot.meta.safeParse(input.meta ?? {});
|
|
428
|
+
if (!parsed.success) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`gate "${id}": its meta does not match the Gate slot's meta schema — ` +
|
|
431
|
+
parsed.error.issues.map((i) => `${i.path.join(".") || "(meta)"}: ${i.message}`).join("; "),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
meta = parsed.data as Record<string, unknown>;
|
|
435
|
+
}
|
|
436
|
+
const defs = resolveAccepts(self, input.accepts ?? []);
|
|
437
|
+
const registration: Registration = {
|
|
438
|
+
address: gateAddress(binding.runId, id),
|
|
439
|
+
runId: binding.runId,
|
|
440
|
+
kind: "gate",
|
|
441
|
+
id,
|
|
442
|
+
path,
|
|
443
|
+
defs,
|
|
444
|
+
meta,
|
|
445
|
+
// A Gate's caller sends as the Gate's invoke id (ADR-0015): static in the config (authored, or
|
|
446
|
+
// named by the walk), the same after a restore, and checkable at load — unlike the
|
|
447
|
+
// caller-facing `id`, which may be computed at run time.
|
|
448
|
+
sender: { party: "gate", id: self.id },
|
|
449
|
+
invokeId: self.id,
|
|
450
|
+
// The Machine that invoked the Gate: the one whose selection says which writing a caller's
|
|
451
|
+
// event would run (ADR-0029's question, asked for the Gate's caller too).
|
|
452
|
+
invoker: self._parent,
|
|
453
|
+
deliver: (event) => sendBack(event),
|
|
454
|
+
// The Hook's door reads the slot's handlers and its reply here (ADR-0011).
|
|
455
|
+
...(slot ? { hooked: { hooks: slot.hooks, reply: slot.reply, delivers: slot.delivers.map((d) => d.name) } } : {}),
|
|
456
|
+
// A Pool's Source hears Hooks through its `source` Gate (ADR-0017).
|
|
457
|
+
...(seat && self._parent ? { source: seat({ pool: self._parent, send: sendBack, system }) } : {}),
|
|
458
|
+
};
|
|
459
|
+
const dispose = binding.table.register(registration);
|
|
460
|
+
const look = slot && isBoundLook(slot.look) ? startLook(slot.look, registration, self, meta) : undefined;
|
|
461
|
+
return () => {
|
|
462
|
+
look?.();
|
|
463
|
+
dispose();
|
|
464
|
+
};
|
|
82
465
|
});
|
|
83
|
-
|
|
84
|
-
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Run a Gate's look (ADR-0011): read now, and again every `every`, until the run takes the event
|
|
470
|
+
* that `until` gives. Returns the stop. The read is a child of the Gate, so it is in the run's actor
|
|
471
|
+
* system: an `http` read runs through the run's Instance Effector and its ledger, as any Effect does.
|
|
472
|
+
* A read, an `until` or a delivery that fails is a line in the Orchestrator's log, and the Gate looks
|
|
473
|
+
* again at the next `every`: it never faults the run.
|
|
474
|
+
*/
|
|
475
|
+
function startLook(
|
|
476
|
+
look: LooseLook,
|
|
477
|
+
reg: Registration,
|
|
478
|
+
self: Parameters<Parameters<typeof fromCallback>[0]>[0]["self"],
|
|
479
|
+
meta: unknown,
|
|
480
|
+
): () => void {
|
|
481
|
+
const every = Math.min(lookEveryMs(look.every ?? LOOK_EVERY_DEFAULT) ?? 600_000, MAX_TIMER_MS);
|
|
482
|
+
const binding = boundRun(self.system);
|
|
483
|
+
let stopped = false;
|
|
484
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
485
|
+
let reading: { stop: () => void } | undefined;
|
|
486
|
+
const log = (what: string): void => console.error(`run ${reg.runId}: gate ${reg.id}: look: ${what}`);
|
|
487
|
+
const again = (): void => {
|
|
488
|
+
if (stopped) return;
|
|
489
|
+
timer = setTimeout(read, every);
|
|
490
|
+
timer.unref?.();
|
|
491
|
+
};
|
|
492
|
+
const settle = (output: unknown): void => {
|
|
493
|
+
reading = undefined;
|
|
494
|
+
if (stopped) return;
|
|
495
|
+
let event: unknown;
|
|
496
|
+
try {
|
|
497
|
+
event = look.until(output, meta);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
log(`until threw: ${(err as Error).message}`);
|
|
500
|
+
return again();
|
|
501
|
+
}
|
|
502
|
+
if (event === undefined) return again();
|
|
503
|
+
const type = (event as { type?: unknown } | null)?.type;
|
|
504
|
+
if (typeof type !== "string" || !reg.hooked?.delivers.includes(type)) {
|
|
505
|
+
log(`until gave ${JSON.stringify(type)}, which is not in the Gate's delivers`);
|
|
506
|
+
return again();
|
|
507
|
+
}
|
|
508
|
+
const { type: _type, ...payload } = event as Record<string, unknown>;
|
|
509
|
+
const def = reg.defs.get(type);
|
|
510
|
+
const parsed = def?.input.safeParse(payload);
|
|
511
|
+
if (!def || !parsed?.success) {
|
|
512
|
+
log(`until gave "${type}", which the Gate does not accept now or whose payload its def refuses`);
|
|
513
|
+
return again();
|
|
514
|
+
}
|
|
515
|
+
// Asked as `jr2 send` asks (ADR-0013): would the run take it from this Gate now? A level that the
|
|
516
|
+
// run does not take now is read again at the next `every`.
|
|
517
|
+
if (!wouldMove(reg.invoker, { type, ...parsed.data, [SENDER_KEY]: reg.sender })) return again();
|
|
518
|
+
// The mark first, as an Effect marks its output before it settles: the delivery can end the run
|
|
519
|
+
// in its own send, and a run that has ended has no feed for the mark.
|
|
520
|
+
binding?.marker?.({ kind: "looked", gate: reg.id, event: type });
|
|
521
|
+
// The look reads a level, so it stops once the run takes its event, until the Gate opens again
|
|
522
|
+
// (ADR-0011).
|
|
523
|
+
stopped = true;
|
|
524
|
+
try {
|
|
525
|
+
binding?.table.deliver(reg.address, type, payload);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
stopped = false;
|
|
528
|
+
log(`the run did not take "${type}": ${(err as Error).message}`);
|
|
529
|
+
return again();
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const read = (): void => {
|
|
533
|
+
timer = undefined;
|
|
534
|
+
if (stopped) return;
|
|
535
|
+
let input: unknown;
|
|
536
|
+
try {
|
|
537
|
+
input = look.input(meta);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
log(`input threw: ${(err as Error).message}`);
|
|
540
|
+
return again();
|
|
541
|
+
}
|
|
542
|
+
const actor = createActor(look.read, { input, parent: self, id: "look" });
|
|
543
|
+
let done = false;
|
|
544
|
+
actor.subscribe({
|
|
545
|
+
next: (snapshot) => {
|
|
546
|
+
if (done) return;
|
|
547
|
+
if (snapshot.status === "done") {
|
|
548
|
+
done = true;
|
|
549
|
+
settle(snapshot.output);
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
error: (err) => {
|
|
553
|
+
if (done) return;
|
|
554
|
+
done = true;
|
|
555
|
+
reading = undefined;
|
|
556
|
+
log(`the read failed: ${(err as Error)?.message ?? String(err)}`);
|
|
557
|
+
again();
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
reading = actor;
|
|
561
|
+
actor.start();
|
|
562
|
+
};
|
|
563
|
+
// Once after the Gate registered: a change before the read is in its answer, and a change after
|
|
564
|
+
// it reaches the Gate as a delivery. Not inside the registration's own start: the Gate's
|
|
565
|
+
// registration and the run's save come first.
|
|
566
|
+
queueMicrotask(read);
|
|
567
|
+
return () => {
|
|
568
|
+
stopped = true;
|
|
569
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
570
|
+
reading?.stop();
|
|
571
|
+
reading = undefined;
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** The plain Gate's logic: what jr2Setup registers as `src: "gate"`, and what a test's
|
|
576
|
+
* `provide({ actors: { gate: … } })` replaces. */
|
|
577
|
+
export const plainGate: PlainGate = gateLogic(undefined);
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The Gate: an input that a run waits for from outside the run (ADR-0011).
|
|
581
|
+
*
|
|
582
|
+
* `gate({ … })` declares a GATE SLOT that also hears a Hook: put it in `actors` and invoke it by
|
|
583
|
+
* its key. A plain Gate needs no value: jr2Setup puts it in each Machine as `invoke: { src: "gate" }`.
|
|
584
|
+
* Its accepted set derives from the invoking state's transitions, and a Caller answers it with
|
|
585
|
+
* `jr2 send`, the Console or `POST /runs/:runId/gates/:gate/events`.
|
|
586
|
+
*
|
|
587
|
+
* - `meta`: a zod object of the meta that the invoking state sets. The Gate checks it when it
|
|
588
|
+
* opens, and the handlers, the look and `reply` read it typed.
|
|
589
|
+
* - `delivers`: the events that the handlers and the look can give. An event whose def leaves the
|
|
590
|
+
* audience unset reads as `audience: "external"`, so no Agent below the Gate has it on its Menu.
|
|
591
|
+
* `createMachine` refuses an entry that the Gate's derived accepted set does not hold.
|
|
592
|
+
* - `hooks`: a handler for each Hook kind id and event key. A handler reads `{ payload, meta,
|
|
593
|
+
* caller }` and gives one event of `delivers`, or `undefined` to ignore the delivery. It is pure
|
|
594
|
+
* and synchronous. A throw is a refusal that the hook log keeps; it never faults the run.
|
|
595
|
+
* - `look`: the Gate's read of the fact that it waits for (see {@link GateLook}), `"none"` for a
|
|
596
|
+
* Gate that hears a command, or `open` for the composer to bind with
|
|
597
|
+
* `customize(m, { gates: { <slot>: { look } } })`. With no look and no `after` on the invoking
|
|
598
|
+
* state, or with an Open look, `jr2 up` warns.
|
|
599
|
+
* - `reply`: the text that the person who acted reads, for a Hook kind whose answer a person reads.
|
|
600
|
+
*
|
|
601
|
+
* It throws on a spec that it cannot run: an unknown key, a `delivers` that is empty or that holds an
|
|
602
|
+
* entry `defineEvent` did not make, a handler that is not a function, or a look whose read is not an
|
|
603
|
+
* `http` Effect marked `idempotent: true` or a promise.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```ts
|
|
607
|
+
* import { z } from "zod";
|
|
608
|
+
* import type {} from "@jr2/orchestrator/hooks/github";
|
|
609
|
+
* import { defineEvent, gate, jr2Setup } from "@jr2/orchestrator";
|
|
610
|
+
*
|
|
611
|
+
* const approve = defineEvent({ name: "approve", input: z.object({ by: z.string() }) });
|
|
612
|
+
*
|
|
613
|
+
* const review = gate({
|
|
614
|
+
* meta: z.object({ pr: z.string(), sha: z.string() }),
|
|
615
|
+
* delivers: [approve],
|
|
616
|
+
* hooks: {
|
|
617
|
+
* github: {
|
|
618
|
+
* "pull_request_review.submitted": ({ payload, meta }) =>
|
|
619
|
+
* payload.pull_request.html_url === meta.pr &&
|
|
620
|
+
* payload.review.commit_id === meta.sha &&
|
|
621
|
+
* payload.review.state === "approved"
|
|
622
|
+
* ? { type: "approve", by: payload.review.user.login }
|
|
623
|
+
* : undefined,
|
|
624
|
+
* },
|
|
625
|
+
* },
|
|
626
|
+
* look: "none",
|
|
627
|
+
* });
|
|
628
|
+
*
|
|
629
|
+
* export const machine = jr2Setup({
|
|
630
|
+
* types: {} as { context: { pr: string; sha: string }; input: { pr: string; sha: string } },
|
|
631
|
+
* events: [approve],
|
|
632
|
+
* actors: { review },
|
|
633
|
+
* }).createMachine({
|
|
634
|
+
* id: "pr",
|
|
635
|
+
* context: ({ input }) => input,
|
|
636
|
+
* initial: "inReview",
|
|
637
|
+
* states: {
|
|
638
|
+
* inReview: {
|
|
639
|
+
* invoke: { src: "review", input: ({ context }) => ({ meta: { pr: context.pr, sha: context.sha } }) },
|
|
640
|
+
* on: { approve: "merged" },
|
|
641
|
+
* },
|
|
642
|
+
* merged: { type: "final" },
|
|
643
|
+
* },
|
|
644
|
+
* });
|
|
645
|
+
* ```
|
|
646
|
+
*
|
|
647
|
+
* @see docs/concepts/gates.md
|
|
648
|
+
* @see docs/guides/answer-a-gate-from-a-forge.md
|
|
649
|
+
* @see docs/reference/decisions/0011-workflow-defined-events.md
|
|
650
|
+
*/
|
|
651
|
+
export function gate<
|
|
652
|
+
const D extends readonly EventDef[],
|
|
653
|
+
M extends z.ZodObject = z.ZodObject<{}>,
|
|
654
|
+
R extends AnyActorLogic = never,
|
|
655
|
+
>(spec: {
|
|
656
|
+
/** A zod object for the meta that the invoking state sets. The Gate checks it when it opens. */
|
|
657
|
+
readonly meta?: M;
|
|
658
|
+
/** The events that the handlers and the look can give. */
|
|
659
|
+
readonly delivers: D;
|
|
660
|
+
/** The handlers, by Hook kind id and event key. */
|
|
661
|
+
readonly hooks?: GateHooks<z.output<NoInfer<M>>, Delivered<D>>;
|
|
662
|
+
/** The look, `"none"` for a Gate that hears a command, or `open` for its composer to bind. */
|
|
663
|
+
readonly look?: GateLook<z.output<NoInfer<M>>, Delivered<D>, R> | typeof open | "none";
|
|
664
|
+
/** The text that the person who acted reads. */
|
|
665
|
+
readonly reply?: GateReply<z.output<NoInfer<M>>, Delivered<D>>;
|
|
666
|
+
}): GateSlot<z.output<M>, Delivered<D>> {
|
|
667
|
+
const fault = specFault(spec);
|
|
668
|
+
if (fault) throw new Error(`gate(): ${fault} (ADR-0011)`);
|
|
669
|
+
return gateSlotOf(partsOf(spec as unknown as Record<string, unknown>)) as GateSlot<z.output<M>, Delivered<D>>;
|
|
670
|
+
}
|