@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/customize.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// `.provide()`, nothing to learn (ADR-0015's rule, restated by ADR-0049 — a `.with()` method and
|
|
10
10
|
// a callable-machine hybrid were both rejected there).
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// Five moves, and each is the smallest one that works:
|
|
13
13
|
//
|
|
14
14
|
// - An AGENT override is xstate's own `provide`: `machine.provide({ actors: { coder:
|
|
15
15
|
// agent({ ...stock, ...override }) } })`. The slot already IS the Agent (ADR-0049), so
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// package left `open`, or whose auth, header or env names `secret(open)`, carries a binder,
|
|
19
19
|
// which builds the Effect again with the composer's parts, so the bound slot is checked as a
|
|
20
20
|
// written one is. Once — a bound part is fixed from then on.
|
|
21
|
+
// - A GATE binding is the same substitution (ADR-0011, ADR-0054): a Gate slot is built again from
|
|
22
|
+
// its parts, with the look bound and the handlers patched by kind and event key.
|
|
21
23
|
// - A CHILD override is the same call one level down, recursing. Each level is exactly the
|
|
22
24
|
// one-level reach ADR-0015 found `provide` has — held by the composer who owns the child
|
|
23
25
|
// object, never host-side injection into somebody else's Machine.
|
|
@@ -45,6 +47,8 @@
|
|
|
45
47
|
// its slot MAP Open (`repos: open`, ADR-0051) declared exactly this: every slot is the composer's
|
|
46
48
|
// to name, so the map they write is the declaration, not an addition to one.
|
|
47
49
|
|
|
50
|
+
import type { CallbackActorLogic } from "xstate";
|
|
51
|
+
import type { AgentRunReceiveEvent } from "./actor.ts";
|
|
48
52
|
import { StateMachine, type AnyStateMachine, type InputFrom, type ProvidedActor, type UnknownActorLogic } from "xstate";
|
|
49
53
|
import type { AgentLogic } from "./actor.ts";
|
|
50
54
|
import { isAgent, type AgentDefinition } from "./agent.ts";
|
|
@@ -78,6 +82,18 @@ import { isEffect, type Arg } from "./effect.ts";
|
|
|
78
82
|
import { attachLent, copyOpenBody, lentIn, openBodiesOf, openBodyDeclarationOf } from "./open-body.ts";
|
|
79
83
|
import { effectChecks } from "./effect-checks.ts";
|
|
80
84
|
import type { NamedSecret, Text } from "./http-effect.ts";
|
|
85
|
+
import type { EventsOf, HookKinds, NoWildcardKeys } from "./hook-kinds/index.ts";
|
|
86
|
+
import {
|
|
87
|
+
gateSlotOf,
|
|
88
|
+
hooksTableFault,
|
|
89
|
+
isGateSlot,
|
|
90
|
+
lookFault,
|
|
91
|
+
type AnyHandler,
|
|
92
|
+
type GateIn,
|
|
93
|
+
type GateLook,
|
|
94
|
+
type GateSlot,
|
|
95
|
+
type GateSlotParts,
|
|
96
|
+
} from "./gate.ts";
|
|
81
97
|
import {
|
|
82
98
|
binderOf,
|
|
83
99
|
effectBindingShape,
|
|
@@ -128,8 +144,10 @@ type LogicAt<M extends AnyStateMachine, K extends string> = Extract<SlotsOf<M>,
|
|
|
128
144
|
*
|
|
129
145
|
* @internal
|
|
130
146
|
*/
|
|
131
|
-
type AgentSlots<M extends AnyStateMachine> = Extract<
|
|
132
|
-
|
|
147
|
+
type AgentSlots<M extends AnyStateMachine> = Extract<
|
|
148
|
+
SlotsOf<M>,
|
|
149
|
+
{ logic: CallbackActorLogic<AgentRunReceiveEvent, any> }
|
|
150
|
+
>["src"];
|
|
133
151
|
/** The Effect slots with an Open part — told by the phantom `OpenEffect` their constructor's
|
|
134
152
|
* overload states for an `open` url or argv, or a `secret(open)` (open-effect.ts).
|
|
135
153
|
*
|
|
@@ -137,6 +155,45 @@ type AgentSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: AgentL
|
|
|
137
155
|
*/
|
|
138
156
|
type OpenEffectSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: OpenEffect<OpenEffectPart> }>["src"];
|
|
139
157
|
|
|
158
|
+
/** The Gate slots (`gate({ … })`, ADR-0011) — told by their brand.
|
|
159
|
+
*
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
162
|
+
type GateSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: GateSlot<any, any> }>["src"];
|
|
163
|
+
/**
|
|
164
|
+
* The meta and the event type of the Gate slot under one key.
|
|
165
|
+
*
|
|
166
|
+
* @internal
|
|
167
|
+
*/
|
|
168
|
+
type GateTypesAt<M extends AnyStateMachine, K extends string> =
|
|
169
|
+
LogicAt<M, K> extends GateSlot<infer Meta, infer Ev> ? { meta: Meta; ev: Ev } : never;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A patch of a Gate slot's handlers (ADR-0011, ADR-0054): by Hook kind id and event key, a handler
|
|
173
|
+
* that adds or replaces the one there, or `null` to remove it. A key that the kind's payloads do not
|
|
174
|
+
* name does not compile.
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export type GateHooksPatch<Meta, Ev> = {
|
|
179
|
+
readonly [Id in keyof HookKinds as HookKinds[Id] extends { readonly id: Id } ? Id : never]?: {
|
|
180
|
+
readonly [Key in keyof EventsOf<HookKinds[Id]>]?:
|
|
181
|
+
| ((d: GateIn<Meta> & { readonly payload: EventsOf<HookKinds[Id]>[Key] }) => Ev | undefined)
|
|
182
|
+
| null;
|
|
183
|
+
} & NoWildcardKeys<HookKinds[Id]>;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What `customize()` binds on one Gate slot: its look, and a patch of its handlers.
|
|
188
|
+
*
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
export type GateBinding<Meta, Ev> = {
|
|
192
|
+
/** The look, bound once: the package left it `open`, or the composer replaces it. Or `"none"`. */
|
|
193
|
+
readonly look?: GateLook<Meta, Ev> | "none";
|
|
194
|
+
readonly hooks?: GateHooksPatch<Meta, Ev>;
|
|
195
|
+
};
|
|
196
|
+
|
|
140
197
|
/** What one Open part binds to: a url, an argv, a secret's name, a dispatch's target — another
|
|
141
198
|
* Orchestrator's origin and the secret that holds its Instance token, together (ADR-0071). */
|
|
142
199
|
type PartBinding<P> = P extends "url"
|
|
@@ -147,7 +204,11 @@ type PartBinding<P> = P extends "url"
|
|
|
147
204
|
? { secret: string }
|
|
148
205
|
: P extends "target"
|
|
149
206
|
? { target: { url: string; token: NamedSecret } }
|
|
150
|
-
:
|
|
207
|
+
: P extends "server"
|
|
208
|
+
? { server: string }
|
|
209
|
+
: P extends "tool"
|
|
210
|
+
? { tool: string }
|
|
211
|
+
: never;
|
|
151
212
|
|
|
152
213
|
/** A union of object types as their intersection: one binding object with every key. */
|
|
153
214
|
type Intersected<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
@@ -271,6 +332,19 @@ export type Customize<M extends AnyStateMachine> = {
|
|
|
271
332
|
effects?: [OpenEffectSlots<Reached<M>>] extends [never]
|
|
272
333
|
? never
|
|
273
334
|
: { [K in OpenEffectSlots<Reached<M>>]?: BindingOf<LogicAt<Reached<M>, K>> };
|
|
335
|
+
/** Bind the look of a Gate slot (`gate({ … })`) that a package left `open`, and add, replace or
|
|
336
|
+
* remove its handlers by Hook kind id and event key (`null` removes one), by slot name:
|
|
337
|
+
* `{ review: { look, hooks: { gitlab: { "note.MergeRequest": null } } } }`. At run time,
|
|
338
|
+
* `customize()` refuses a slot that holds no Gate slot: `customize(): machine "<id>" carries no
|
|
339
|
+
* Gate slot "<slot>" — its Gate slots are: <slots>`. */
|
|
340
|
+
gates?: [GateSlots<Reached<M>>] extends [never]
|
|
341
|
+
? never
|
|
342
|
+
: {
|
|
343
|
+
[K in GateSlots<Reached<M>>]?: GateBinding<
|
|
344
|
+
GateTypesAt<Reached<M>, K>["meta"],
|
|
345
|
+
GateTypesAt<Reached<M>, K>["ev"]
|
|
346
|
+
>;
|
|
347
|
+
};
|
|
274
348
|
/** Retune a Machine that this Machine composes, by slot name. The value has the same shape as
|
|
275
349
|
* these parts, one level down. At run time, `customize()` refuses a slot that holds no Machine:
|
|
276
350
|
* `customize(): machine "<id>" composes no Machine under "<slot>"`. */
|
|
@@ -311,13 +385,18 @@ export type Customize<M extends AnyStateMachine> = {
|
|
|
311
385
|
* the Machine does not declare does not compile. Some Machines, such as `task`, leave their
|
|
312
386
|
* whole slot map Open. Then you name every slot, and at least one. The keys are your names for
|
|
313
387
|
* the Repos, and the Machine says what their order means. */
|
|
314
|
-
|
|
388
|
+
// A Workspace with only optional fields can start with no input, but the wrapper gives a mapper `{}`,
|
|
389
|
+
// so its `input` is never `undefined`.
|
|
390
|
+
repos?: [RepoSlotsOf<M>] extends [never]
|
|
391
|
+
? never
|
|
392
|
+
: { [K in RepoSlotsOf<M>]?: RepoSlot<Exclude<InputFrom<WorkspaceOf<M>>, undefined>> };
|
|
315
393
|
};
|
|
316
394
|
|
|
317
395
|
/** The same shape with the types erased — what the implementation walks. */
|
|
318
396
|
type LooseParts = {
|
|
319
397
|
agents?: Record<string, Partial<AgentDefinition> | undefined>;
|
|
320
398
|
effects?: Record<string, EffectBinding | undefined>;
|
|
399
|
+
gates?: Record<string, { look?: unknown; hooks?: Record<string, Record<string, AnyHandler | null>> } | undefined>;
|
|
321
400
|
actors?: Record<string, LooseParts | undefined>;
|
|
322
401
|
image?: string;
|
|
323
402
|
user?: string | UserContainer;
|
|
@@ -342,6 +421,9 @@ type LooseParts = {
|
|
|
342
421
|
* - `customize(): machine "<id>" carries no Agent slot "<slot>" — its Agents are: <slots>`
|
|
343
422
|
* - `customize(): machine "<id>" composes no Machine under "<slot>" — the Machines it composes are: <slots>`
|
|
344
423
|
* - `customize(): machine "<id>" carries no Effect slot "<slot>" — its Effects are: <slots>`
|
|
424
|
+
* - `customize(): machine "<id>" carries no Gate slot "<slot>" — its Gate slots are: <slots>`
|
|
425
|
+
* - `` customize(): Gate slot "<slot>"'s look is bound once, to a look or "none" — not `open` again ``, and the
|
|
426
|
+
* refusals of a look that `gate()` makes
|
|
345
427
|
* - `customize(): machine "<id>" declares no Repo Slot "<slot>" — its slots are: <slots>`
|
|
346
428
|
* - `customize(): machine "<id>" declares its Repo Slots open as a map — name at least one`
|
|
347
429
|
* - `` customize(): `repos` say what a SANDBOX is made of, how big it is and which Repos it
|
|
@@ -376,11 +458,11 @@ type LooseParts = {
|
|
|
376
458
|
* @see docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md
|
|
377
459
|
*/
|
|
378
460
|
export function customize<M extends AnyStateMachine>(machine: M, parts: Customize<M>): M {
|
|
379
|
-
const { agents, effects, actors, image, user, repos, resources } = parts as LooseParts;
|
|
461
|
+
const { agents, effects, gates, actors, image, user, repos, resources } = parts as LooseParts;
|
|
380
462
|
let out: AnyStateMachine = machine;
|
|
381
463
|
// Order matters in one direction only: the Sandbox seats and slots REBUILD the wrapper, so they
|
|
382
464
|
// go last and carry the retuned implementations with them.
|
|
383
|
-
if (agents || effects || actors) out = retune(out, { agents, effects, actors });
|
|
465
|
+
if (agents || effects || gates || actors) out = retune(out, { agents, effects, gates, actors });
|
|
384
466
|
const seats: Seats = {
|
|
385
467
|
...(image !== undefined ? { image } : {}),
|
|
386
468
|
...(user !== undefined ? { user } : {}),
|
|
@@ -450,6 +532,9 @@ function retune(machine: AnyStateMachine, parts: LooseParts): AnyStateMachine {
|
|
|
450
532
|
}
|
|
451
533
|
const bound = Object.entries(parts.effects ?? {}).filter(([, b]) => b !== undefined);
|
|
452
534
|
for (const [name, binding] of bound) actors[name] = bindEffect(machine, name, binding!);
|
|
535
|
+
for (const [name, binding] of Object.entries(parts.gates ?? {})) {
|
|
536
|
+
if (binding) actors[name] = bindGate(machine, name, binding);
|
|
537
|
+
}
|
|
453
538
|
for (const [name, childParts] of Object.entries(parts.actors ?? {})) {
|
|
454
539
|
if (!childParts) continue;
|
|
455
540
|
const child = asMachine((machine.implementations.actors as Record<string, unknown>)[name]);
|
|
@@ -539,6 +624,12 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
|
|
|
539
624
|
);
|
|
540
625
|
}
|
|
541
626
|
}
|
|
627
|
+
if ((part === "server" || part === "tool") && (typeof value !== "string" || value === "")) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`customize(): the mcp Effect "${name}"'s ${part} is bound to a name, a string: ` +
|
|
630
|
+
`${effectBindingShape(part)} (ADR-0054, ADR-0079)`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
542
633
|
if (part === "secret" && typeof value !== "string") {
|
|
543
634
|
throw new Error(
|
|
544
635
|
`customize(): the ${logic.effect.kind} Effect "${name}"'s secret is bound to a secret's name, a string — ` +
|
|
@@ -549,6 +640,62 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
|
|
|
549
640
|
return binder(binding);
|
|
550
641
|
}
|
|
551
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Bind a Gate slot's look and patch its handlers (ADR-0011, ADR-0054). The slot is built again from
|
|
645
|
+
* its parts, with the same `meta`, `delivers` and `reply`, so a run of the import and a run of the
|
|
646
|
+
* bound Machine each have their own. A look is bound to a value once: `open` again is refused, and
|
|
647
|
+
* the bound look meets the checks that `gate()` runs.
|
|
648
|
+
*/
|
|
649
|
+
function bindGate(
|
|
650
|
+
machine: AnyStateMachine,
|
|
651
|
+
name: string,
|
|
652
|
+
binding: NonNullable<NonNullable<LooseParts["gates"]>[string]>,
|
|
653
|
+
): unknown {
|
|
654
|
+
const logic = (machine.implementations.actors as Record<string, unknown>)[name];
|
|
655
|
+
if (!isGateSlot(logic)) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`customize(): machine "${machine.id}" carries no Gate slot "${name}" — its Gate slots are: ` +
|
|
658
|
+
`${listed(slotsWhere(machine, isGateSlot))} (ADR-0011). Only declared parts can be bound.`,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const parts: GateSlotParts = logic.gateSlot;
|
|
662
|
+
let look = parts.look;
|
|
663
|
+
if (binding.look !== undefined) {
|
|
664
|
+
if (binding.look === open) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`customize(): Gate slot "${name}"'s look is bound once, to a look or "none" — not \`open\` again (ADR-0054)`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const fault = lookFault(binding.look);
|
|
670
|
+
if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
|
|
671
|
+
look = binding.look as GateSlotParts["look"];
|
|
672
|
+
}
|
|
673
|
+
const hooks: Record<string, Record<string, AnyHandler>> = {};
|
|
674
|
+
for (const [kind, keys] of Object.entries(parts.hooks)) hooks[kind] = { ...keys };
|
|
675
|
+
for (const [kind, keys] of Object.entries(binding.hooks ?? {})) {
|
|
676
|
+
if (typeof keys !== "object" || keys === null) {
|
|
677
|
+
throw new Error(`customize(): Gate slot "${name}": hooks.${kind} is { <event key>: handler | null } (ADR-0011)`);
|
|
678
|
+
}
|
|
679
|
+
const into = (hooks[kind] ??= {});
|
|
680
|
+
for (const [key, handler] of Object.entries(keys)) {
|
|
681
|
+
if (handler === null) delete into[key];
|
|
682
|
+
else if (handler !== undefined) into[key] = handler;
|
|
683
|
+
}
|
|
684
|
+
if (Object.keys(into).length === 0) delete hooks[kind];
|
|
685
|
+
}
|
|
686
|
+
const fault = hooksTableFault(hooks);
|
|
687
|
+
if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
|
|
688
|
+
const frozen: Record<string, Readonly<Record<string, AnyHandler>>> = {};
|
|
689
|
+
for (const [kind, keys] of Object.entries(hooks)) frozen[kind] = Object.freeze(keys);
|
|
690
|
+
return gateSlotOf(
|
|
691
|
+
Object.freeze({
|
|
692
|
+
...parts,
|
|
693
|
+
hooks: Object.freeze(frozen),
|
|
694
|
+
...(look !== undefined ? { look } : {}),
|
|
695
|
+
}),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
552
699
|
const isSecret = (value: unknown): boolean =>
|
|
553
700
|
typeof value === "object" && value !== null && typeof (value as { secret?: unknown }).secret === "string";
|
|
554
701
|
|
package/src/dispatch-effect.ts
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
// through the binding, foreign in the `jr2-dispatched-by` header), and the door answers a
|
|
20
20
|
// repeat with the run the key already started. So a dispatch is idempotent by construction: a
|
|
21
21
|
// restore that cannot tell whether its start arrived sends it again, and gets the same run.
|
|
22
|
+
// The key is asked before the slot's schema reads the body: in-process through the run host's
|
|
23
|
+
// lookup, foreign as a key-only ask (no body) when the slot's schema refuses the body. With no
|
|
24
|
+
// run under the key, the schema's refusal is the invoke's error, and nothing starts.
|
|
22
25
|
// - THE RUN ON THE ROW. Once the start's answer names the run, the intent row is written again
|
|
23
26
|
// with it, saved at the write: a restore with it settles `started` at once, or resumes the
|
|
24
27
|
// wait on that run, and never starts another.
|
|
@@ -78,9 +81,10 @@ export type DispatchDefinition<S extends z.ZodObject | undefined = undefined> =
|
|
|
78
81
|
wait?: boolean;
|
|
79
82
|
/** How long the Effect waits: by default 30s for the start, and 10m with `wait`. */
|
|
80
83
|
timeout?: Duration;
|
|
81
|
-
/** A different Orchestrator: `url` is its origin, and `token` is the secret that holds
|
|
82
|
-
*
|
|
83
|
-
* <slot>: { target: { url, token
|
|
84
|
+
/** A different Orchestrator: `url` is its origin, and `token` is the secret that holds the token
|
|
85
|
+
* of a Caller of it with `start` for the Workflow, and `read` too when `wait` is true. Or `open`,
|
|
86
|
+
* which the composer binds with `customize(<import>, { effects: { <slot>: { target: { url, token
|
|
87
|
+
* } } } })`. Without it, the run starts in this Orchestrator. */
|
|
84
88
|
target?: { url: string; token: NamedSecret } | typeof open;
|
|
85
89
|
};
|
|
86
90
|
|
|
@@ -208,7 +212,9 @@ function targetOf(workflow: string, target: unknown): { url: string; token: stri
|
|
|
208
212
|
);
|
|
209
213
|
}
|
|
210
214
|
if (typeof token !== "object" || token === null || typeof (token as { secret?: unknown }).secret !== "string") {
|
|
211
|
-
throw new Error(
|
|
215
|
+
throw new Error(
|
|
216
|
+
`${where}: the token is a secret("<name>") holding a Caller token of the target with start for the Workflow (ADR-0071)`,
|
|
217
|
+
);
|
|
212
218
|
}
|
|
213
219
|
const name = (token as { secret: string }).secret;
|
|
214
220
|
if (name === OPEN_SECRET) {
|
|
@@ -255,23 +261,75 @@ export function openDispatchOf<S extends z.ZodObject | undefined>(
|
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
/** The wire request for one foreign start (ADR-0071): the target's origin, the Workflow, the body,
|
|
258
|
-
* the token by name,
|
|
264
|
+
* the token by name, who dispatched it, and the party it acts for. With no body (`undefined`), the
|
|
265
|
+
* start is a key-only ask: the door answers the run under the key, and starts nothing. */
|
|
259
266
|
export function dispatchStartOf(
|
|
260
267
|
parts: DispatchParts,
|
|
261
|
-
input: Record<string, unknown
|
|
268
|
+
input: Record<string, unknown> | undefined,
|
|
262
269
|
dispatchedBy: DispatchedBy & { instance: string },
|
|
270
|
+
onBehalfOf?: string,
|
|
263
271
|
): DispatchStartRequest {
|
|
264
272
|
if (parts.target === undefined) throw new Error("dispatchStartOf: an in-process dispatch has no wire request");
|
|
265
273
|
return {
|
|
266
274
|
kind: "dispatch",
|
|
267
275
|
url: [{ text: parts.target.url }],
|
|
268
276
|
workflow: parts.workflow,
|
|
269
|
-
input,
|
|
277
|
+
...(input !== undefined ? { input } : { keyOnly: true as const }),
|
|
270
278
|
auth: { bearer: { secret: parts.target.token } },
|
|
271
279
|
dispatchedBy,
|
|
272
280
|
wait: parts.wait,
|
|
273
281
|
timeoutMs: parts.timeoutMs,
|
|
282
|
+
...(onBehalfOf !== undefined ? { onBehalfOf } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* What a foreign start sends as `onBehalfOf` for the first Caller of the calling run's chain
|
|
288
|
+
* (ADR-0071, ADR-0078): the party that Caller says it acts for, else the name that jr2 shows, else
|
|
289
|
+
* `<via>/<name>`. Undefined when jr2 started the chain, or the text is not printable ASCII of at
|
|
290
|
+
* most 256 characters, which a header cannot carry: the start then goes with no claim.
|
|
291
|
+
*/
|
|
292
|
+
export function onBehalfOfFor(
|
|
293
|
+
caller: { via: string; name: string; shown?: string; onBehalfOf?: string } | undefined,
|
|
294
|
+
): string | undefined {
|
|
295
|
+
if (!caller) return undefined;
|
|
296
|
+
const text = caller.onBehalfOf ?? caller.shown ?? `${caller.via}/${caller.name}`;
|
|
297
|
+
return /^[\x20-\x7e]{1,256}$/.test(text) ? text : undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- A Pool's worker ------------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/** What a Pool of runs gives its `dispatch` worker (ADR-0017): the run's body, and the Hook delivery
|
|
303
|
+
* that carried the item, when one did. The key is the worker's own spawn id: the item's id. */
|
|
304
|
+
export type PoolWorkerInput = { body: unknown; hook?: DispatchedBy["hook"] };
|
|
305
|
+
|
|
306
|
+
/** A dispatch's snapshot as a Pool's worker holds it: the key and the Hook delivery ride the
|
|
307
|
+
* persisted snapshot, so a restore sends the same key again. */
|
|
308
|
+
type KeyedSnapshot<O, I> = EffectSnapshot<O, I> & { key?: string; hook?: DispatchedBy["hook"] };
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The `dispatch` slot of a Pool of runs, as the Pool spawns it (ADR-0017): the same Effect, whose
|
|
312
|
+
* idempotency key is the item's id — the spawn id — and not its ledger row id. So every Pool, every
|
|
313
|
+
* hand-over and every restore that gives the same item to the callee Workflow gets the same run
|
|
314
|
+
* (ADR-0071). The input is {@link PoolWorkerInput}.
|
|
315
|
+
*
|
|
316
|
+
* @internal
|
|
317
|
+
*/
|
|
318
|
+
export function poolWorker(logic: EffectLogic<any, DispatchOutput>): EffectLogic<PoolWorkerInput, DispatchOutput> {
|
|
319
|
+
const keyed = Object.create(
|
|
320
|
+
Object.getPrototypeOf(logic) as object,
|
|
321
|
+
Object.getOwnPropertyDescriptors(logic),
|
|
322
|
+
) as EffectLogic<PoolWorkerInput, DispatchOutput>;
|
|
323
|
+
const initial = logic.getInitialSnapshot.bind(logic);
|
|
324
|
+
keyed.getInitialSnapshot = (scope, input) => {
|
|
325
|
+
const given = (input ?? { body: undefined }) as PoolWorkerInput;
|
|
326
|
+
return {
|
|
327
|
+
...initial(scope, given.body),
|
|
328
|
+
key: scope.self.id,
|
|
329
|
+
...(given.hook !== undefined ? { hook: given.hook } : {}),
|
|
330
|
+
} as KeyedSnapshot<DispatchOutput, PoolWorkerInput> as never;
|
|
274
331
|
};
|
|
332
|
+
return keyed;
|
|
275
333
|
}
|
|
276
334
|
|
|
277
335
|
// --- The actor logic -----------------------------------------------------------------------------
|
|
@@ -418,6 +476,9 @@ async function drive<I, O>(
|
|
|
418
476
|
mark({ stage: "output", text: `${named}: run ${runId}${existing ? " (the key found it: started before)" : ""}` });
|
|
419
477
|
};
|
|
420
478
|
const deadline = Date.parse(at) + parts.timeoutMs;
|
|
479
|
+
// A Pool's worker carries the item's id as its key, and the delivery that carried the item
|
|
480
|
+
// (ADR-0017). Any other dispatch's key is its ledger row id.
|
|
481
|
+
const { key, hook } = snapshot as KeyedSnapshot<O, I>;
|
|
421
482
|
|
|
422
483
|
if (prior?.run !== undefined && !parts.wait) {
|
|
423
484
|
// Started before the restart, and the answer is the run: nothing to send.
|
|
@@ -440,22 +501,58 @@ async function drive<I, O>(
|
|
|
440
501
|
await waitIn(prior.run);
|
|
441
502
|
return;
|
|
442
503
|
}
|
|
443
|
-
//
|
|
444
|
-
|
|
504
|
+
// The key first, as the door asks it (ADR-0071): a run of the callee that carries it is
|
|
505
|
+
// answered before the input is read, so a re-send across a change of the slot's schema answers
|
|
506
|
+
// `existing`, never an error. The slot's bound holds the lookup as it holds the start.
|
|
507
|
+
const startBound = prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline;
|
|
508
|
+
const asked = host.lookup
|
|
509
|
+
? await beforeDeadline(
|
|
510
|
+
host.lookup(parts.workflow, key ?? id).then((run) => ({ run })),
|
|
511
|
+
startBound,
|
|
512
|
+
signal,
|
|
513
|
+
)
|
|
514
|
+
: { run: undefined };
|
|
515
|
+
if (signal.aborted) return;
|
|
516
|
+
if (asked === undefined) {
|
|
517
|
+
settle(
|
|
518
|
+
unknown(
|
|
519
|
+
`the run host did not answer the start of ${parts.workflow} within the slot's timeout; a run may have started`,
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const found = asked.run;
|
|
525
|
+
if (found) {
|
|
526
|
+
if (!prior) binding.recordEffect?.(id, intent());
|
|
527
|
+
pin(found.runId, true);
|
|
528
|
+
if (!parts.wait) {
|
|
529
|
+
settle({ outcome: "started", run: found.runId });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
await waitIn(found.runId);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Fresh, or in flight with no run on the row: the key makes a second start the same run. With
|
|
536
|
+
// the key asked above and no run under it, the body is a first send's, and the slot's schema
|
|
537
|
+
// refuses it as the invoke's error (ADR-0071). A host with no lookup asks the key at its start.
|
|
538
|
+
const input = inputOf(prior !== undefined && !host.lookup);
|
|
445
539
|
if (!prior) binding.recordEffect?.(id, intent());
|
|
446
540
|
mark({
|
|
447
541
|
stage: "started",
|
|
448
542
|
text: prior ? `${named} (sent again under its key: a run it started is answered)` : named,
|
|
449
543
|
});
|
|
450
544
|
// The depth is the callee's: one below this run, which the host records as sent (ADR-0071).
|
|
451
|
-
const by: DispatchedBy = {
|
|
545
|
+
const by: DispatchedBy = {
|
|
546
|
+
run: binding.runId,
|
|
547
|
+
slot,
|
|
548
|
+
effect: id,
|
|
549
|
+
depth: (binding.depth ?? 0) + 1,
|
|
550
|
+
...(key !== undefined ? { key } : {}),
|
|
551
|
+
...(hook !== undefined ? { hook } : {}),
|
|
552
|
+
};
|
|
452
553
|
// The slot's bound holds the start too (ADR-0071): a run host that does not answer in it is
|
|
453
554
|
// `unknown` — the start may have been taken, and a restore's re-send under the key would find it.
|
|
454
|
-
const answer = await beforeDeadline(
|
|
455
|
-
host.start(parts.workflow, input, by),
|
|
456
|
-
prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline,
|
|
457
|
-
signal,
|
|
458
|
-
);
|
|
555
|
+
const answer = await beforeDeadline(host.start(parts.workflow, input, by), startBound, signal);
|
|
459
556
|
if (signal.aborted) return;
|
|
460
557
|
if (answer === undefined) {
|
|
461
558
|
settle(
|
|
@@ -489,8 +586,13 @@ async function drive<I, O>(
|
|
|
489
586
|
const port = portFactory(target);
|
|
490
587
|
// The wait's stop goes through this port when its state exits (ADR-0071).
|
|
491
588
|
run.port = port;
|
|
589
|
+
// The party the start acts for: the first Caller of this run's chain, read once (ADR-0071).
|
|
590
|
+
const onBehalfOf = onBehalfOfFor(await binding?.firstCaller?.());
|
|
591
|
+
if (signal.aborted) return;
|
|
492
592
|
// Fresh: nothing was sent. The body is checked before the intent row, so a defect in the input is
|
|
493
|
-
// the Machine's error and sends nothing.
|
|
593
|
+
// the Machine's error and sends nothing. A body the slot's schema refuses is sent as a key-only
|
|
594
|
+
// ask (ADR-0071): the door answers a run under the key, and with none the schema's refusal is the
|
|
595
|
+
// invoke's error.
|
|
494
596
|
const req = prior ? undefined : requestFor(false);
|
|
495
597
|
const big = req && tooLarge(req);
|
|
496
598
|
if (big) {
|
|
@@ -567,18 +669,27 @@ async function drive<I, O>(
|
|
|
567
669
|
expired();
|
|
568
670
|
}
|
|
569
671
|
|
|
570
|
-
/**
|
|
571
|
-
|
|
672
|
+
/** Why the slot's schema refuses the invoke's input, or undefined when it takes it (or the slot
|
|
673
|
+
* has no schema). */
|
|
674
|
+
function refusedBody(): string | undefined {
|
|
675
|
+
if (def.input === undefined) return undefined;
|
|
676
|
+
const parsed = def.input.safeParse(snapshot.input ?? {});
|
|
677
|
+
if (parsed.success) return undefined;
|
|
678
|
+
return (
|
|
679
|
+
`dispatch "${slot}": its input does not match the slot's input schema — ` +
|
|
680
|
+
parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** The run's body: the invoke's input, parsed by the slot's schema when it has one. `again`: an
|
|
685
|
+
* in-process re-send to a run host that asks the key only at its start; a body the slot's schema
|
|
686
|
+
* refuses then goes as it is, and the run host answers the run under the key (ADR-0071). */
|
|
687
|
+
function inputOf(again = false): Record<string, unknown> {
|
|
572
688
|
let input: unknown = snapshot.input ?? {};
|
|
573
689
|
if (def.input !== undefined) {
|
|
574
690
|
const parsed = def.input.safeParse(input);
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
`dispatch "${slot}": its input does not match the slot's input schema — ` +
|
|
578
|
-
parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
input = parsed.data;
|
|
691
|
+
if (parsed.success) input = parsed.data;
|
|
692
|
+
else if (!again) throw new Error(refusedBody());
|
|
582
693
|
}
|
|
583
694
|
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
584
695
|
throw new Error(`dispatch "${slot}": its input is the run's body, a JSON object (ADR-0071)`);
|
|
@@ -600,7 +711,9 @@ async function drive<I, O>(
|
|
|
600
711
|
instance: binding?.orchestratorName ?? "an Orchestrator with no name",
|
|
601
712
|
};
|
|
602
713
|
const timeoutMs = again ? Math.max(1, Math.floor(deadline - Date.now())) : parts.timeoutMs;
|
|
603
|
-
|
|
714
|
+
// A body the slot's schema refuses is never sent: the start asks the key alone (ADR-0071).
|
|
715
|
+
const body = refusedBody() === undefined ? inputOf() : undefined;
|
|
716
|
+
return dispatchStartOf({ ...parts, timeoutMs }, body, by, onBehalfOf);
|
|
604
717
|
}
|
|
605
718
|
|
|
606
719
|
/** The in-process wait (ADR-0071): the run host tells when the run leaves `live`; the slot's
|
|
@@ -677,6 +790,9 @@ async function drive<I, O>(
|
|
|
677
790
|
if (state.state === "settled" && state.output) {
|
|
678
791
|
const parsed = DispatchOutputShape.safeParse(state.output);
|
|
679
792
|
if (!parsed.success) throw new EffectorMalformed("the Effector's output for a dispatch is not a dispatch's");
|
|
793
|
+
// A key-only ask, and no run has the key: nothing was started, and the slot's schema refusal
|
|
794
|
+
// is the invoke's error (ADR-0071).
|
|
795
|
+
if (parsed.data.outcome === "absent") throw new Error(refusedBody() ?? "the door found no run under the key");
|
|
680
796
|
const output = parsed.data as DispatchOutput;
|
|
681
797
|
if (output.outcome === "started" || output.outcome === "settled") pin(output.run, false);
|
|
682
798
|
settle(output);
|
package/src/durability.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type StoredSnapshot = {
|
|
|
18
18
|
status: string;
|
|
19
19
|
snapshot: unknown;
|
|
20
20
|
reason?: string;
|
|
21
|
+
/** When the run started (ADR-0009): the row's `started_at`. */
|
|
22
|
+
startedAt?: string;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
type ChildEntry = { snapshot?: { input?: unknown; [k: string]: unknown }; [k: string]: unknown };
|
package/src/duration.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A duration as a person writes it in config or a query: a number and one unit, such as `"90m"`,
|
|
2
|
+
// `"36h"`, `"30d"` or `"2w"`. `runs.retain` (ADR-0065) and the `since` of `GET /runs` (ADR-0009)
|
|
3
|
+
// read it.
|
|
4
|
+
|
|
5
|
+
const DURATION = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/;
|
|
6
|
+
const UNIT_MS: Record<string, number> = {
|
|
7
|
+
s: 1000,
|
|
8
|
+
m: 60_000,
|
|
9
|
+
h: 3_600_000,
|
|
10
|
+
d: 86_400_000,
|
|
11
|
+
w: 7 * 86_400_000,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The duration in milliseconds, or undefined when `text` is not a positive duration. */
|
|
15
|
+
export function periodMs(text: string): number | undefined {
|
|
16
|
+
const m = DURATION.exec(text.trim());
|
|
17
|
+
if (!m) return undefined;
|
|
18
|
+
const ms = Number(m[1]) * UNIT_MS[m[2]!]!;
|
|
19
|
+
return ms > 0 && Number.isFinite(ms) ? ms : undefined;
|
|
20
|
+
}
|
package/src/effect-checks.ts
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
// Agent reads the Effect's output, not the answer. Everywhere else a settled child with no
|
|
28
28
|
// `onDone` is Rest, and the one-line form is correct — silent on purpose.
|
|
29
29
|
//
|
|
30
|
+
// - AN onDone WITH NO LAST ARM (ADR-0075, `refuseUnroutedOutcome`). Each Effect outcome is open,
|
|
31
|
+
// so an `onDone` that an output can miss holds the run in the state and shows no fault. A type
|
|
32
|
+
// cannot see the missing arm, so the walk refuses it. No `onDone` at all is Rest, as above: the
|
|
33
|
+
// author routes nothing, so no outcome can miss a route.
|
|
34
|
+
//
|
|
30
35
|
// The probe reads, it never runs work: a mapper is called with a Proxy event whose reads are
|
|
31
36
|
// recorded, and a context and self that survive any read. A guard and an assign are pure by
|
|
32
37
|
// contract, and only they are probed; any other action may do work, and is never run.
|
|
@@ -34,12 +39,44 @@
|
|
|
34
39
|
import type { AnyStateMachine, UnknownActorLogic } from "xstate";
|
|
35
40
|
import type { EventDef } from "@jr2/agent-protocol";
|
|
36
41
|
import { isAgent } from "./agent.ts";
|
|
37
|
-
import {
|
|
42
|
+
import { isGateInvoke } from "./gate-brand.ts";
|
|
43
|
+
import { inputKeysOf, inputShapeOf, isEffect } from "./effect.ts";
|
|
38
44
|
import { mayTake, partiesAt } from "./party.ts";
|
|
39
45
|
import { isAnswer, isPickMapper } from "./pick.ts";
|
|
40
46
|
|
|
41
47
|
type Node = AnyStateMachine["root"];
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Refuse a Machine with a state that invokes an Effect when the last arm of that `onDone` has a
|
|
51
|
+
* guard (ADR-0075). The slots are the Machine's own `actors`. Thrown at build by
|
|
52
|
+
* `jr2Setup.createMachine`, so also at a Chart's door, and at load by `partsOf`, so a Machine built
|
|
53
|
+
* with xstate's own `createMachine` is refused where `jr2 check`, `jr2 up` and the Orchestrator read
|
|
54
|
+
* it. Throws one line that names each state and the arm to write.
|
|
55
|
+
*/
|
|
56
|
+
export function refuseUnroutedOutcome(machine: AnyStateMachine): void {
|
|
57
|
+
const actors = (machine.implementations.actors ?? {}) as Record<string, unknown>;
|
|
58
|
+
const unrouted: string[] = [];
|
|
59
|
+
const visit = (node: Node): void => {
|
|
60
|
+
for (const inv of node.invoke) {
|
|
61
|
+
if (typeof inv.src !== "string" || !isEffect(actors[inv.src])) continue;
|
|
62
|
+
const arms = node.transitions.get(`xstate.done.actor.${inv.id}`);
|
|
63
|
+
if (arms !== undefined && (arms.length === 0 || arms.at(-1)!.guard !== undefined)) {
|
|
64
|
+
unrouted.push(`"${node.id}" (Effect "${inv.src}")`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const child of Object.values(node.states)) visit(child);
|
|
68
|
+
};
|
|
69
|
+
visit(machine.root);
|
|
70
|
+
if (!unrouted.length) return;
|
|
71
|
+
const many = unrouted.length > 1;
|
|
72
|
+
throw new Error(
|
|
73
|
+
`machine "${machine.id}": ${many ? "states" : "state"} ${unrouted.join(", ")} ${many ? "have" : "has"} an ` +
|
|
74
|
+
`onDone with no unguarded last arm — an Effect's outcomes are open, and an output that no arm matches holds ` +
|
|
75
|
+
`the run in that state with no fault: add a last arm with no guard, as onDone: [..., { target: "<the state ` +
|
|
76
|
+
`that handles the rest>" }] (ADR-0075)`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
43
80
|
/** What the walk found: refusals, and warnings. */
|
|
44
81
|
export type EffectFindings = { errors: string[]; warnings: string[] };
|
|
45
82
|
|
|
@@ -47,6 +84,7 @@ export function effectChecks(
|
|
|
47
84
|
machine: AnyStateMachine,
|
|
48
85
|
actors: Record<string, UnknownActorLogic>,
|
|
49
86
|
defs: Map<string, EventDef>,
|
|
87
|
+
chart = false,
|
|
50
88
|
): EffectFindings {
|
|
51
89
|
const errors: string[] = [];
|
|
52
90
|
const warnings: string[] = [];
|
|
@@ -220,6 +258,15 @@ export function effectChecks(
|
|
|
220
258
|
};
|
|
221
259
|
});
|
|
222
260
|
const effects = invokes.filter((inv) => inv.effect !== undefined);
|
|
261
|
+
// xstate's `input` is optional for an actor: a kit Effect that takes a key refuses an invoke
|
|
262
|
+
// with no input when `jr2Setup` or `customize()` builds the Machine.
|
|
263
|
+
for (const inv of effects) {
|
|
264
|
+
const shape = inputShapeOf(inv.effect!);
|
|
265
|
+
const needs = [...(shape?.required ?? []), ...(shape?.oneOf ?? [])];
|
|
266
|
+
if (inv.mapper === undefined && needs.length > 0) {
|
|
267
|
+
errors.push(`state "${n.id}": Effect "${inv.slot}" has no \`input\`, and it takes ${needs.join(", ")}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
223
270
|
if (!effects.length) continue;
|
|
224
271
|
const at = `state "${n.id}" (Effect${effects.length > 1 ? "s" : ""} ${effects.map((e) => `"${e.slot}"`).join(", ")})`;
|
|
225
272
|
for (const [event, { pick, ...reads }] of entering.get(n) ?? []) {
|
|
@@ -276,7 +323,8 @@ export function effectChecks(
|
|
|
276
323
|
});
|
|
277
324
|
} catch (err) {
|
|
278
325
|
probed = false;
|
|
279
|
-
|
|
326
|
+
if (!chart)
|
|
327
|
+
warnings.push(`${own}: could not probe the input mapper (${messageOf(err)}); its params are unchecked`);
|
|
280
328
|
}
|
|
281
329
|
}
|
|
282
330
|
}
|
|
@@ -289,7 +337,7 @@ export function effectChecks(
|
|
|
289
337
|
const picked = effects.filter((e) => isPickMapper(e.mapper));
|
|
290
338
|
if (probed && lost.length && probe.unrun.length) {
|
|
291
339
|
// Any other action may do work, so the probe never runs one; it may read the param.
|
|
292
|
-
warnings.push(unrunWarning(where, lost, probe.unrun));
|
|
340
|
+
if (!chart) warnings.push(unrunWarning(where, lost, probe.unrun));
|
|
293
341
|
} else if (probed && lost.length && picked.length) {
|
|
294
342
|
const inputs =
|
|
295
343
|
picked.length === 1
|
|
@@ -342,7 +390,7 @@ export function effectChecks(
|
|
|
342
390
|
const below: Node[] = [];
|
|
343
391
|
const descend = (p: Node): void => {
|
|
344
392
|
for (const child of Object.values(p.states)) {
|
|
345
|
-
if (invokesAgent(child) !== undefined || child.invoke.some((inv) => inv
|
|
393
|
+
if (invokesAgent(child) !== undefined || child.invoke.some((inv) => isGateInvoke(inv, actors))) continue;
|
|
346
394
|
below.push(child);
|
|
347
395
|
descend(child);
|
|
348
396
|
}
|