@jr2/orchestrator 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/console/components/app.ts +2 -0
- package/console/components/fleet.ts +14 -1
- package/console/components/machine-pane.ts +41 -2
- package/console/main.ts +13 -1
- package/console/store.ts +102 -6
- package/console/style.css +21 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +442 -0
- package/docs/concepts/architecture.md +230 -0
- package/docs/concepts/charts.md +143 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +254 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +603 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +289 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +222 -0
- package/docs/concepts/the-menu.md +378 -0
- package/docs/concepts/workspaces.md +678 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
- package/docs/get-started/install.md +126 -0
- package/docs/guides/add-tools-to-the-sandbox.md +295 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +223 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +226 -0
- package/docs/guides/look-inside-a-sandbox.md +174 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +486 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +544 -0
- package/docs/guides/run-one-worker-per-item.md +321 -0
- package/docs/guides/set-an-agents-access-and-dials.md +375 -0
- package/docs/guides/size-a-workspace.md +204 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +208 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +326 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +216 -0
- package/docs/reference/authoring-surface/agent.md +128 -0
- package/docs/reference/authoring-surface/answer.md +140 -0
- package/docs/reference/authoring-surface/arg.md +60 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +191 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +83 -0
- package/docs/reference/authoring-surface/file.md +122 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +159 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +74 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +125 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +112 -0
- package/docs/reference/authoring-surface/secret.md +118 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +12 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-binding.md +62 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +19 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
- package/docs/reference/authoring-surface/type-customize.md +109 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +41 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +15 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +32 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +94 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
- package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
- package/docs/reference/authoring-surface/workspace.md +259 -0
- package/docs/reference/authoring-surface.md +197 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +756 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions.md +83 -0
- package/docs/reference/glossary.md +648 -0
- package/docs/reference/http-api.md +1348 -0
- package/docs/reference/machines.md +190 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +206 -0
- package/package.json +5 -3
- package/src/actor.ts +541 -79
- package/src/agent.ts +114 -38
- package/src/ambient.ts +2 -1
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +169 -37
- package/src/charts.ts +34 -6
- package/src/config.ts +430 -178
- package/src/custodian.ts +74 -22
- package/src/customize.ts +188 -46
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect.ts +341 -44
- package/src/effector-wire.ts +160 -11
- package/src/fail.ts +209 -0
- package/src/harness-client.ts +541 -47
- package/src/http-effect.ts +495 -53
- package/src/http.ts +427 -16
- package/src/index.ts +60 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +84 -0
- package/src/machine-doc.ts +21 -3
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/notices.ts +66 -13
- package/src/open-body.ts +159 -23
- package/src/open-effect.ts +81 -8
- package/src/open.ts +59 -6
- package/src/parts.ts +184 -13
- package/src/party.ts +68 -2
- package/src/pick.ts +463 -11
- package/src/pool.ts +247 -36
- package/src/registration.ts +83 -4
- package/src/repos.ts +2 -1
- package/src/run-host.ts +565 -31
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +273 -38
- package/src/sandbox-kube.ts +3 -1
- package/src/secret-placement.ts +52 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +239 -56
- package/src/size.ts +121 -9
- package/src/snapshot-store.ts +54 -10
- package/src/stub-harness.ts +44 -16
- package/src/timers.ts +14 -1
- package/src/vocabulary.ts +69 -14
- package/src/wire.ts +90 -16
- package/src/workspace.ts +697 -175
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Observation is open; run state and control need the Instance token
|
|
2
|
+
|
|
3
|
+
[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) minted two tokens and scoped them
|
|
4
|
+
for **delivery** — a Sandbox token may deliver to its own agent registrations, an Instance token may deliver to gates.
|
|
5
|
+
It said nothing about who may **read** a run or **cancel** one, and the code took the obvious shortcut: `authenticated`,
|
|
6
|
+
meaning any token we minted. That is a hole. It also left the visualizer with no way in at all, which is the same
|
|
7
|
+
question from the other side.
|
|
8
|
+
|
|
9
|
+
Building `jr2 visualize` (the Machine renderer and its live run highlighting) forced both halves at once: the page is a
|
|
10
|
+
**browser**, and a browser holds no token. So this ADR splits the HTTP surface into **three bands**, and adds an
|
|
11
|
+
**observation projection** — the page gets a view instead of a credential.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
**Three bands, and the middleware is what says which.**
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
open /workflows, /workflows/:name/machine, /viz/* structure
|
|
19
|
+
/workflows/:name/runs, …/runs/:runId/events OBSERVATION (new)
|
|
20
|
+
authenticated /agents/:iid/* the Agent's surface [Sandbox token]
|
|
21
|
+
instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + control [Instance token]
|
|
22
|
+
POST /workflows/:name/runs starting a run (ADR-0058)
|
|
23
|
+
/runs/:id/gates/:gate/events gates (ADR-0011/0013)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Run state and run control are the Instance token's alone.** `authenticated` accepts _any_ token we minted, so a
|
|
27
|
+
Sandbox token — the one credential that sits in a pod an Agent shares a network namespace with — could read **every**
|
|
28
|
+
run's context (other features' branches, ticket bodies, review verdicts, Harness endpoints) and **CANCEL any run**.
|
|
29
|
+
Neither is on the Agent's surface, no more than a gate is: ADR-0013 scopes that token to delivering into registrations
|
|
30
|
+
recorded against its _own_ Sandbox. This closes the same class of hole ADR-0013 closed for gates, on the routes it did
|
|
31
|
+
not name. The band is named for the credential it requires, not for a role — "operator" in this repo is the Kubernetes
|
|
32
|
+
operator (ADR-0001).
|
|
33
|
+
|
|
34
|
+
- **The observer gets a PROJECTION, not a credential.** `RunObservation` is
|
|
35
|
+
`{ runId, workflow, status, value, children }` — identity, and where in the Machine the run is. What is absent is the
|
|
36
|
+
point: `context` is the workflow's working data, and `instanceId`/`fault` name live infrastructure and leak error
|
|
37
|
+
text. A state `value` (and the `children` tree of state values below it) is a tree of state **keys** — it is
|
|
38
|
+
structure, and structure is already open at `/workflows/:name/machine`. So an observer learns nothing here it could
|
|
39
|
+
not read from the Machine doc, except **which states are lit**. `observe()` is the one place that line is drawn, and
|
|
40
|
+
`RunChild` has no context field at any depth — the redaction surface cannot grow by forgetting.
|
|
41
|
+
|
|
42
|
+
- **Observation is scoped to one workflow**, because that is what an observer already knows (it is in the page's path).
|
|
43
|
+
There is no listing of everything an Orchestrator happens to be running, and the feed serves **live runs only** —
|
|
44
|
+
reading a settled run's terminal status through the store stays on `/runs/:id`, which is guarded.
|
|
45
|
+
|
|
46
|
+
- **An `emit`'s payload does not cross.** The observation feed carries an emit's `type` alone: an author's emit payload
|
|
47
|
+
is author data, the same class of thing as context.
|
|
48
|
+
|
|
49
|
+
## Considered options
|
|
50
|
+
|
|
51
|
+
- **Give the page a token.** It would have to be the **Instance** token — gates, cancel, every run's context — handed to
|
|
52
|
+
whatever can load a URL. Putting it in `jr2.config.ts` (checked in) is worse still, and the token is minted per boot,
|
|
53
|
+
so the page would not have it anyway. Rejected: the visualizer needs to _see_ a run, and seeing is not a subset of the
|
|
54
|
+
credential that steers it. That mismatch is the whole reason a projection exists.
|
|
55
|
+
- **Keep `/runs*` on `authenticated` and filter in the handler.** Rejected: the guard would then depend on every future
|
|
56
|
+
handler remembering to redact. The band is a middleware precisely so the default is refusal.
|
|
57
|
+
- **A fourth "observer" token.** Rejected: it buys nothing today (the observation surface carries no secret) and costs a
|
|
58
|
+
minting, distribution, and revocation story. If observation ever needs to be _private_ — a deployed jr2 Application on
|
|
59
|
+
a shared network — that is the ADR to write, and it is the same open question ADR-0013 left about authn for non-kube
|
|
60
|
+
callers.
|
|
61
|
+
|
|
62
|
+
## Consequences
|
|
63
|
+
|
|
64
|
+
- **`auth.test.ts` changed its mind.** Its prose already said "Observation and control are the operator's, not the
|
|
65
|
+
Agent's" directly above assertions that a Sandbox token gets **200** on a run read and **200** on CANCEL. Those are
|
|
66
|
+
now **403**. The test pinned the hole.
|
|
67
|
+
- **The API is a cluster Service** (ADR-0019), so "open" means open to whoever can reach it — in-cluster peers, a
|
|
68
|
+
port-forward, or an ingress — deliberately, since what is open is structure plus lit states. Anything that would
|
|
69
|
+
embarrass a user if seen belongs behind `instanceOnly`, and that is where it is.
|
|
70
|
+
- **The CLI is unaffected.** It holds the Instance token from the in-cluster Secret (ADR-0019) and reads the guarded
|
|
71
|
+
routes, as before.
|
|
72
|
+
- **Open, inherited from ADR-0013:** what a deployed jr2 Application uses for human callers. Whatever it is, the band
|
|
73
|
+
split survives it — only the credential in the `instanceOnly` band changes.
|
|
74
|
+
|
|
75
|
+
- **One scoped exception, and it does not live on this surface at all (ADR-0023).** The Harness prints each Agent's
|
|
76
|
+
conversation — prompts, assistant text, thinking, tool calls, and the answer jr2 gave each Menu pick — to container
|
|
77
|
+
stdout, which leaves the pod for the cluster's log plane and lands in front of a reader this document never gets to
|
|
78
|
+
check. That is a real exception to the rule above, and it is stated rather than reasoned away. Two things bound it. It
|
|
79
|
+
is **not a new band**: the HTTP surface is untouched, nothing here is served, and a reader gets it from
|
|
80
|
+
`kubectl logs`, holding cluster log access instead of a jr2 credential. And **a Working tool's results are excluded**
|
|
81
|
+
— the file contents, command output, API responses it returns never print, so the mechanical leak (an Agent `cat`s a
|
|
82
|
+
config while debugging and its secrets ride a log shipper into 30-day retention) is off the table, leaving what an
|
|
83
|
+
Agent reasons aloud and what jr2 answered its picks. What an Effector returns in that answer is scrubbed before it
|
|
84
|
+
signs it as ADR-0068 claims: no secret the Effector holds, in the forms it sent or in common encodings. The scrub does
|
|
85
|
+
not try to catch a transformed secret (a hash, a truncation, an encoding it does not list), a partial echo, or a side
|
|
86
|
+
channel, and a credentialed command's child reads the secret its command names, so it can print that secret in such a
|
|
87
|
+
form; then it prints there as the model read it. The answer also carries an Effect's output as the model read it: a
|
|
88
|
+
`command` Effect's output prints there up to the bound, and a Held value an upstream echoed into that output prints
|
|
89
|
+
with it (ADR-0023, ADR-0059). What remains, deliberately, is that on a shared cluster the population who can read an
|
|
90
|
+
Agent's reasoning, and what its picks answered, is whoever can read pod logs. If that becomes unacceptable, the fix is
|
|
91
|
+
the read-through ADR-0023 declined to build — `instanceOnly`, on this surface, where the band already works.
|
|
92
|
+
|
|
93
|
+
Evidence: `packages/orchestrator/src/http.ts` (the three middlewares), `run-host.ts` (`observe` / `RunObservation` /
|
|
94
|
+
`RunChild`), `test/auth.test.ts` (the boundary), `test/http-viz.test.ts` (a planted secret never reaches the feed, at
|
|
95
|
+
any depth of the child tree).
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Workflows are authored through `jr2Setup`; vocabulary and agent menus derive from the machine
|
|
2
|
+
|
|
3
|
+
The jr-parity exercise produced a working model but a bad consumer API: mechanism plumbing (manifests, tool lists,
|
|
4
|
+
event-union bookkeeping) dominated the workflow. The redesign grill (2026-07-13/14, full record in
|
|
5
|
+
[docs/design/workflow-api/](https://github.com/snapwich/jr2/blob/71ebe36/docs/design/workflow-api/proposal.md)) settled
|
|
6
|
+
the split: **the consumer writes pure workflow — states, transitions, prompts, policy — and jr2 absorbs everything else
|
|
7
|
+
through convention.** This ADR covers the Authoring surface; [ADR-0016](0016-agent-turn-mechanics-are-internal.md)
|
|
8
|
+
covers the agent turn; [ADR-0017](0017-pool-over-a-source-port.md) covers the run loop.
|
|
9
|
+
|
|
10
|
+
## The root of `@jr2/orchestrator` is the Authoring surface
|
|
11
|
+
|
|
12
|
+
A workflow authors against one package: `jr2Setup`, `defineEvent`, `agent`, `workspace`, the Effects, `customize` and
|
|
13
|
+
`defineConfig` all import from `@jr2/orchestrator`. Its root exports the Authoring surface (GLOSSARY.md) and nothing
|
|
14
|
+
else.
|
|
15
|
+
|
|
16
|
+
- **The rule.** A name is on the surface when an author writes it in a workflow module or in `jr2.config.ts`. No other
|
|
17
|
+
name is. A name that an author only gets by inference stays off the surface: the event union that `jr2Setup` derives,
|
|
18
|
+
a Gate's input, an Agent's Frame. The `gate` actor is off the surface too. `jr2Setup` registers it, and a state
|
|
19
|
+
invokes it as `src: "gate"`, so no author writes the value. A type is on the surface when an author must write its
|
|
20
|
+
name, for example `Workspaced`, `HostInjectedInput`, `AgentDefinition`, and the types of the parts that `customize()`
|
|
21
|
+
takes. `JR2Config` is on the surface too: the `jr2.config.ts` Reference documents its fields, and an author names it
|
|
22
|
+
to type a config that is built outside `defineConfig()`.
|
|
23
|
+
- **Named exports only.** `src/index.ts` names each export (`export { a } from …`, `export type { T } from …`), in
|
|
24
|
+
groups by topic, and has no `export *`. Thus an Assistant that reads the file sees the full surface, and each change
|
|
25
|
+
to the surface is a line in a diff.
|
|
26
|
+
- **A test pins it.** `test/authoring-surface.test.ts` makes sure that `index.ts` has no `export *`, that the root's
|
|
27
|
+
runtime values are equal to a sorted list in the test, and that the type names in the `export` lists of `index.ts` are
|
|
28
|
+
equal to a second sorted list. A name that is added or removed fails the test until the list changes. The same test
|
|
29
|
+
makes sure that `@jr2/machines` and each Instance-shaped folder import the root only.
|
|
30
|
+
- **The kit's own packages import `@jr2/orchestrator/internal`.** It holds the surface and all that the kit shares: the
|
|
31
|
+
run host, the HTTP API, the Harness wire, the Custodian, the Effector wire, the cluster names. It is not for authors,
|
|
32
|
+
and it has no stability promise: any name in it can change in any release. A Machine that the kit ships
|
|
33
|
+
(`@jr2/machines`) and each Instance-shaped folder in the repo import the root only. The `exports` map has `.`,
|
|
34
|
+
`./internal` and `./tsconfig.instance.json`, and nothing else. Thus an Instance that imports a deep path, such as
|
|
35
|
+
`@jr2/orchestrator/src/run-host.ts`, does not resolve it, in `tsc` or in Node.
|
|
36
|
+
|
|
37
|
+
## `jr2Setup`: an xstate `setup()` analog, not a DSL
|
|
38
|
+
|
|
39
|
+
`jr2Setup({ types, events: [defs], actors, actions, guards })` is statically imported and returns xstate's **public
|
|
40
|
+
`SetupReturn`** — `.createMachine()` yields a plain `StateMachine`, Stately-inspectable and `.provide()`-testable, with
|
|
41
|
+
no jr2 runtime needed to construct it (proven by compiled experiment against xstate 5.32.2; consumer surface has zero
|
|
42
|
+
casts). It:
|
|
43
|
+
|
|
44
|
+
- injects the mechanism events (`agent.fault`, `workspace.lost`) into the event union and derives the workflow event
|
|
45
|
+
types from the zod defs — hand-written `EventFrom` unions retire;
|
|
46
|
+
- pre-registers the jr2 actors (`agentRun`, `gate`) with typed inputs;
|
|
47
|
+
- takes the defs **as values**, which lets it validate at `createMachine` time that every event key appearing anywhere
|
|
48
|
+
in the machine maps to a def — closing xstate's nested-`on` typo hole (unknown keys in nested states typecheck
|
|
49
|
+
silently upstream) with a load-time failure;
|
|
50
|
+
- attaches the vocabulary to the machine (`vocabularyOf(machine)`, a WeakMap keyed on `machine.config` — the object
|
|
51
|
+
xstate's `.provide()` passes through, so a clone keeps it, and the returned machine stays bit-identical). **The
|
|
52
|
+
`export const events` manifest retires**, and the workflow module contract shrinks to `export const machine` (revising
|
|
53
|
+
[ADR-0011](0011-workflow-defined-events.md)'s named-exports contract): the defs ride the Machine, and the actors that
|
|
54
|
+
need them resolve names off the Machine that INVOKED them (ADR-0011, ADR-0049) — the host copies nothing at
|
|
55
|
+
registration, and the only other reader is the Machine doc the Console renders. ADR-0011's anti-global-registry
|
|
56
|
+
argument is preserved — attribution flows through the machine, per-Machine by construction.
|
|
57
|
+
|
|
58
|
+
`events` is optional, and so is the whole argument: a Machine with no Vocabulary leaves `events` out, or calls
|
|
59
|
+
`jr2Setup()`, and its event type is the Mechanism events alone. Such a Machine still uses `jr2Setup` when it declares a
|
|
60
|
+
door ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)), because xstate's `setup()` does not read the
|
|
61
|
+
`input` of `createMachine`. An empty Vocabulary is the default, not `readonly EventDef[]`, so the type of `event` does
|
|
62
|
+
not widen to every event name. A required `events` was considered and rejected: `events: []` tells the reader nothing,
|
|
63
|
+
and the compiler gave a missing `events` as a large structural diff of `SetupReturn`, not as the name of the missing
|
|
64
|
+
key.
|
|
65
|
+
|
|
66
|
+
## Agent menus and gate accepts derive from the machine
|
|
67
|
+
|
|
68
|
+
A state that invokes an Agent slot derives the workflow events its Turn may pick: its own transitions, its ancestors'
|
|
69
|
+
(bubbled, per statechart semantics), and the transitions written on the states below it — its **modes**. xstate hands an
|
|
70
|
+
event to the deepest active state that handles it, and a false guard there falls through to the parent, so a pick
|
|
71
|
+
written on a child is one the Turn can take while that child is active. The descent stops at a child that invokes
|
|
72
|
+
another Agent slot (a nested Turn: its picks are its own) or a `gate` (its transitions are the Gate's accepted set), and
|
|
73
|
+
leaves out everything below that child; a child that runs anything else stays in. A state that invokes `gate` gets its
|
|
74
|
+
accepted set from its own and its ancestors' transitions and those written on its plain children, by the same descent:
|
|
75
|
+
position gives a Gate the writings below it that no other party answers for, as it gives an Agent its modes (the Pool's
|
|
76
|
+
wake Gate, on its root, takes the writing on `parked`). The consumer names neither; MCP appears nowhere in workflow
|
|
77
|
+
code. Mechanics: the **vocabulary** derivation is static, in `jr2Setup.createMachine` — a config walk wraps each
|
|
78
|
+
invoke's `input` to append the derived names, so names still ride serializable input and the ADR-0007 restore path and
|
|
79
|
+
invoke-time validation are unchanged. The same walk unions the derived sets of every state that invokes an Agent of one
|
|
80
|
+
name into that Agent's **Menu** — its tools, fixed for a conversation so the provider's prompt cache survives a change
|
|
81
|
+
of state. The walk reads transition _keys_ and so cannot see guards, which is why what a Turn may pick is not static on
|
|
82
|
+
top of it: `agentSurface` asks the invoking machine's guards, and the Turn is told its **Allowed picks** — the derived
|
|
83
|
+
set, less every event that the live snapshot, in its active mode, would not take
|
|
84
|
+
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md) owns the rationale).
|
|
85
|
+
Authoring is untouched by the split: a workflow names no tools in either leg. The same walk feeds `jr2 visualize` ("this
|
|
86
|
+
state's agent can call X, Y"). Dotted names (`agent.*`, `workspace.lost`, `xstate.*`, `after`) are mechanically
|
|
87
|
+
excluded.
|
|
88
|
+
|
|
89
|
+
**A transition belongs to the party that its state, or the nearest state above it, invokes.** The party is the Agent
|
|
90
|
+
slot or the `gate` that state invokes, or both when one state invokes both; root transitions, with no such state above,
|
|
91
|
+
belong to both. That is who may send an event whose def leaves `audience` unset
|
|
92
|
+
(`defineEvent({ audience?: "agent" | "external" | "any" })`, no default):
|
|
93
|
+
|
|
94
|
+
| Transition written on | Unset audience: who may send |
|
|
95
|
+
| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
96
|
+
| A state that invokes an Agent, or a mode below it | That Agent |
|
|
97
|
+
| A state that invokes a Gate | The Gate's caller |
|
|
98
|
+
| A state that invokes both | Both |
|
|
99
|
+
| A state that invokes anything else (a child Machine, a wrapper body, any other actor), or nothing | The party of the nearest invoking state above; none above means both |
|
|
100
|
+
|
|
101
|
+
So child or sibling says only "same Turn or not"
|
|
102
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)): an in-Turn Gate's caller sends the Gate's
|
|
103
|
+
events and not the Agent's picks, and a nested Agent does not get the outer Agent's picks. **Write an audience only to
|
|
104
|
+
give a pick to the other party, or to hold a root pick back.** An explicit `audience` overrides by kind over the whole
|
|
105
|
+
reach: `agent` reaches every Agent whose reach holds a writing of the event, `external` every Gate's, `any` both — so
|
|
106
|
+
`submit: "any"` lets an in-Turn Gate's caller send the Agent's pick, and `report_blocked: "agent"` keeps a root pick
|
|
107
|
+
from the Gates. A def means one thing: its description must be true at every writing its party can reach, and two
|
|
108
|
+
meanings are two defs. The reach itself is unchanged by the rule — own and ancestors, plus the states below with no
|
|
109
|
+
party-invoking state between (an Agent's modes, a Gate's plain children) — and the rule only filters inside it, so no
|
|
110
|
+
case widens who can send a pick; a test scans every shipped Machine and fixture for it, both the derived names and the
|
|
111
|
+
writings each party may take at run time. The walk computes every party's seat — per state, who may send what — and the
|
|
112
|
+
Machine's doc carries it; `jr2 up` prints it for every registered Machine and every Machine it composes, and the Console
|
|
113
|
+
shows the same lines, folded beside the workflow's diagram (structure, so tokenless too). An explicit `accepts:` on a
|
|
114
|
+
Gate's input remains an escape hatch.
|
|
115
|
+
|
|
116
|
+
**`sentBy(id)`: a writing that declines other senders.** Every event a party delivers carries its sender (ADR-0011):
|
|
117
|
+
`sender: { party, id }`, the Agent's slot or the Gate's invoke id. `jr2Setup` registers one guard, `jr2.sentBy`, and the
|
|
118
|
+
author writes it on a writing as `guard: sentBy("coder")` — a named guard with params, alone or inside `and([...])`. It
|
|
119
|
+
is true for an event whose `sender.id` is `id` and for an event with no sender (the Machine's own: `raise`, `sendTo`
|
|
120
|
+
itself, a child's `sendParent`), and false otherwise, so xstate's own selection moves outward to the next writing of the
|
|
121
|
+
name. It can only make a writing decline; it never gives a writing to a party — position does. `id` is an Agent slot or
|
|
122
|
+
a Gate's invoke id of this Machine, and a `sentBy` that names neither is refused at load, like an unknown event key.
|
|
123
|
+
Under `or()` or `not()`, or in an author's own guard that reads `event.sender`, it works at run time but the walk cannot
|
|
124
|
+
read it. A `.provide()` test sends as a party by writing the field: `{ type, sender: { party, id } }`.
|
|
125
|
+
|
|
126
|
+
**A delivery never runs another party's writing.** Audience says who may send a name; xstate selects which writing runs,
|
|
127
|
+
deepest active first, whoever sent it. So at delivery the real event — its payload and its sender — goes through
|
|
128
|
+
xstate's own selection (`getTransitionData`) before the Machine sees it, and if a writing selected is one the sender may
|
|
129
|
+
not take by the position rule above, the event is refused: an Agent's pick is answered `moved: false` and not delivered
|
|
130
|
+
(ADR-0029), a Gate's caller gets 409 with what the Gate takes now (ADR-0013). The lists ask the same question the same
|
|
131
|
+
way, payload-blind: an Agent's Allowed picks and a Gate's `acceptsNow` are the names whose selection, for an event that
|
|
132
|
+
carries the sender and with every guard that reads the payload counted as passing, moves something and holds only
|
|
133
|
+
writings the sender may take. A guard that reads the payload is unknown there and passes the party question; it is never
|
|
134
|
+
the fail-open of a party check skipped, because delivery asks again with the real payload. The payload-blind selection
|
|
135
|
+
is a copy of xstate 5.32.2's (`transitionNode`, `StateNode.next`, `evaluateGuard`, the members of `and`/`or`/`not`),
|
|
136
|
+
pinned against `getTransitionData` in the default test run — the canary for an xstate bump. Without a `sentBy`, nothing
|
|
137
|
+
misroutes: the shallower party is refused while the deeper writing is active, and the walk warns (below). When the
|
|
138
|
+
deeper party runs BELOW an Agent's state — a Gate, a nested Agent — the Agent's state is not at Rest, so the Agent's
|
|
139
|
+
picks are refused before the selection is asked
|
|
140
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)); the selection decides the Gate's caller's
|
|
141
|
+
sends there, and every case with the Agent's state at Rest.
|
|
142
|
+
|
|
143
|
+
**Load checks on modes and audience.** `createMachine` refuses a registered event that no state handles: it is in no
|
|
144
|
+
derived set, so it is on no Menu and no Gate. It refuses a parallel region below a state that invokes an Agent — the
|
|
145
|
+
Agent's state itself being parallel included, since its regions are below it: the shape is reserved for picks the Agent
|
|
146
|
+
does not wait for, which jr2 does not build, and background work goes above the Agent's state. It warns — the Machine
|
|
147
|
+
stays legal; the doc carries the warning, `jr2 up` prints it and the Console shows it with the seats — about a mode that
|
|
148
|
+
nothing enters (not initial, and no state the Machine can enter has a transition into it or a state inside it — so
|
|
149
|
+
neither the mode's own `initial` nor its transition into itself counts: its picks are on the Menu and never allowed; a
|
|
150
|
+
transition into a history state enters that history state's default target, shallow or deep, or its parent's initial
|
|
151
|
+
state when it names none, and a default target the walk cannot resolve is its own warning); about a Gate above an
|
|
152
|
+
Agent's state that takes an unset-audience writing the Agent reaches (the one narrowing of the audience rule an author
|
|
153
|
+
may not expect); about two parties' writings of one name that can be active together; about one party's writings of one
|
|
154
|
+
name that mean two things; and about two shapes below an Agent's state that keep it from Rest, so every pick of the
|
|
155
|
+
Agent waits (ADR-0024): a Gate whose answer has no target (the Gate stays open after it), and a long-lived invoke — one
|
|
156
|
+
that runs until its state exits, not a promise, an Effect, a Gate, an Agent or a child Machine. A child with no `onDone`
|
|
157
|
+
that strands the Agent in it — a one-way mode — is legal and silent on purpose, an Effect child included.
|
|
158
|
+
|
|
159
|
+
**Load check on the output of a run.** xstate v5 gives a run one output: the `output` at the root of the Machine. The
|
|
160
|
+
`output` of a final state directly below the root goes to that root `output` as `event.output`, and to nothing else.
|
|
161
|
+
Thus a Machine with an `output` on such a final state and no root `output` ends with no output, and `types.output` does
|
|
162
|
+
not stop it. It was the most frequent error in the Machines that Assistants wrote, and an Assistant then told its user a
|
|
163
|
+
result that the run did not have. Thus the load walk refuses that shape: `createMachine` and the parts walk at `jr2 up`
|
|
164
|
+
and at the boot, so a Machine built with xstate's own `setup()`, and a child Machine, are refused too. The refusal names
|
|
165
|
+
each such final state and the two fixes: an `output` at the root from `context`, or
|
|
166
|
+
`output: ({ event }) => event.output` at the root, which passes on the output of the final state. A final state inside a
|
|
167
|
+
compound state is legal: its `output` is the `event.output` of the `onDone` of that state. A Machine with no `output`
|
|
168
|
+
anywhere is legal: its result is in its `context`, and it tells nothing false about an output. Considered and rejected:
|
|
169
|
+
a warning (a run with no output is never what the author meant, and a warning does not stop the false result); and a
|
|
170
|
+
root `output` that jr2 adds when there is none (it would make jr2 Machines behave differently from xstate Machines, and
|
|
171
|
+
a Machine that a different xstate host runs would lose its output again).
|
|
172
|
+
|
|
173
|
+
**Load checks on Effects** (ADR-0067). `createMachine` refuses a state below an Agent's state that invokes something and
|
|
174
|
+
that nothing enters: what it invokes never runs (a mode nothing enters is only the warning above). Both checks read one
|
|
175
|
+
reachability walk, history defaults included; where a history default the walk cannot resolve may enter such a state,
|
|
176
|
+
the walk does not know, and it warns instead of refusing. Both checks look below a state that invokes an Effect or a
|
|
177
|
+
child Machine, and stop at one that invokes another Agent or a Gate. Where a pick enters a state that invokes an Effect,
|
|
178
|
+
every param of the pick must be read by something — the check is per state, not per invoke — by the input of any invoke
|
|
179
|
+
of that state, an Effect's or not, so a second invoke beside the state's work counts (`fromPick` reads each param that
|
|
180
|
+
is a key of the Effect's input, a command's argv slots, and each one its `fill` reads; on an invoke that is no Effect it
|
|
181
|
+
reads every param, and on a command whose argv is Open it reads every param too, since the argv's slots are the
|
|
182
|
+
composer's — `customize()` runs this check again when it binds the argv and refuses there a param the bound argv does
|
|
183
|
+
not read, and gives the bound Machine the warnings the walk gives for that argv
|
|
184
|
+
([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)); a plain mapper is probed with
|
|
185
|
+
a recording event), by the entering transition's guard (inline, named, or inside `and`/`or`/`not`), or by an `assign` on
|
|
186
|
+
the entering transition or on the entry of a state it enters — else the Agent supplies it and nothing uses it: refused.
|
|
187
|
+
A pick enters what xstate enters: its target, the target's ancestors below the transition's domain, and their initial
|
|
188
|
+
descendants — and what an eventless `always` on a state it entered enters next, in the same macrostep with the same
|
|
189
|
+
event, whose guard and actions count as the entering transition's. So a pick that targets a compound state enters the
|
|
190
|
+
child that invokes, a pick that targets a child enters the ancestor that invokes, a pick that targets a state whose
|
|
191
|
+
`always` moves on enters where it moves, and a transition inside the invoking state does not enter it. A pick is a
|
|
192
|
+
writing an Agent may send (the position rule above); a Gate's event is a human's, and its params are not checked. The
|
|
193
|
+
walk does not carry a pick's params across a timer: where an `after` on a state the pick entered moves on to a state
|
|
194
|
+
that invokes an Effect, the Effect's input and every guard and action from the timer on read the timer's event, so only
|
|
195
|
+
a guard or `assign` before the timer reads them — a param that must cross is stored with `assign` on the pick's
|
|
196
|
+
transition, and one nothing stores is refused. A pick that targets a history state enters what was last active, which
|
|
197
|
+
the walk cannot know: where that may be a state that invokes an Effect, the pick's params there are unchecked, and it
|
|
198
|
+
warns. A guard and an assign are pure by contract, so the probe runs them; it runs no other action. TypeScript does not
|
|
199
|
+
catch a `fromPick` param that is no key of the input: it accepts an extra key on a spread. It refuses a `fill` that sets
|
|
200
|
+
a key that is also a param (the Machine's value would drop the Agent's), and a `fromPick` of a pick other than the event
|
|
201
|
+
that enters the state. Code the probe cannot or does not run may read a param, so it gives a warning, never a refusal: a
|
|
202
|
+
mapper, guard or assign that throws in the probe, and — where a param is left unread — any other action on the entering
|
|
203
|
+
transition or on the entry of a state it enters, named in the warning. Its params are unchecked. It warns where an
|
|
204
|
+
Agent's state answers on a mode's entry (`answer`) and an Effect below it has no `onDone`: the Effect settles in place,
|
|
205
|
+
that mode is not entered again, and the Agent reads the Effect's output, not the answer. A pick that enters no Effect,
|
|
206
|
+
directly or across a timer, is not checked for its params.
|
|
207
|
+
|
|
208
|
+
- **Two parties' writings of one name that can be active together** — one above the other on one path, or in parallel
|
|
209
|
+
regions — where the deeper writing (each other region's, for parallel regions) does not decline the other sender with
|
|
210
|
+
a `sentBy` the walk can read. xstate would select it for the other party's event, so delivery refuses that event while
|
|
211
|
+
both are active. The warning names both writings and shows the corrected line, with the guard on the DEEPER writing,
|
|
212
|
+
because xstate selects deepest first and only a declining deeper writing lets selection move outward:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
"approve" on "s2.release" (Gate "review-1") and on "s2.release.work.draft" (Agent "coder") can be active together;
|
|
216
|
+
Gate "review-1"'s "approve" would run Agent "coder"'s writing, so jr2 refuses it while both are active. Make the
|
|
217
|
+
deeper writing decline other senders:
|
|
218
|
+
draft: { on: { approve: { guard: sentBy("coder"), target: "#s2.doneB" } } }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A root writing is both parties', so the root-writing shape — a root `cancel`, a Gate below it, the Agent's `cancel` in
|
|
222
|
+
a mode — is the same case: the line goes on the mode's writing, and the Gate's caller's `cancel` then runs the root
|
|
223
|
+
writing. The Agent's writing above a party seated BELOW the Agent's state — a Gate, a nested Agent — is not the case:
|
|
224
|
+
while that party runs, the Agent's state is not at Rest, so every pick of the Agent is refused whatever its guard, and
|
|
225
|
+
a `sentBy` on the deeper writing would change nothing; the walk does not warn there. Siblings (a mode and a Gate's or
|
|
226
|
+
another Agent's sibling state) are not the case: neither writing is ever selected for the other, and delivery answers
|
|
227
|
+
a send there like any pick the state does not take. A Gate whose invoke id the walk derived is named so in the line;
|
|
228
|
+
giving its invoke an `id` names it. A `sentBy` under `or()` or `not()` leaves the warning in place.
|
|
229
|
+
|
|
230
|
+
- **One def, one meaning.** One party's writings of one name on one path with different targets: its sender reads one
|
|
231
|
+
description for both, and only the deepest active writing runs. The warning says to make the def's description true at
|
|
232
|
+
each writing or to give them distinct names. xstate's own transition `description` rides the Machine doc beside each
|
|
233
|
+
writing.
|
|
234
|
+
|
|
235
|
+
Known limits, not built: two states on one path that invoke one Agent slot share one sender id, so `sentBy` cannot tell
|
|
236
|
+
their Turns apart; and a child Machine that forwards a party's event to its parent (`sendParent`) hands it the child's
|
|
237
|
+
sender, whose id names a slot or Gate of the child — if the parent has one of that name, `sentBy` matches it. Only
|
|
238
|
+
author code forwards an event.
|
|
239
|
+
|
|
240
|
+
Considered and rejected for modes: a Menu per mode (the tools block would change inside a conversation and bill its
|
|
241
|
+
history again — ADR-0029), and refusing picks written below an Agent's state at build (that shape is a mode).
|
|
242
|
+
|
|
243
|
+
Considered and rejected for the audience: a default of `any` with a convention to tag the sensitive events (an in-Turn
|
|
244
|
+
Gate's caller could then send every pick of the Agent above it unless each was tagged `agent`, and a nested Agent got
|
|
245
|
+
the outer Agent's picks); a fixed default of `agent` (every Gate event then needs a tag, and the nested leak stays); and
|
|
246
|
+
a knob on the Gate (`accepts: "own"`), which fixes the Gate side only. The rule above reads the transition's position,
|
|
247
|
+
not a mark written in it, so it is not the per-transition demarcation rejected below.
|
|
248
|
+
|
|
249
|
+
Considered and rejected: per-transition demarcation (xstate transitions have no typed metadata slot for it without
|
|
250
|
+
inventing jr2-only config inside the transition table — a DSL by the back door) and purely structural
|
|
251
|
+
own-transitions-only derivation (breaks ADR-0011's blessed handle-`report_blocked`-once-at-an-ancestor idiom). Position
|
|
252
|
+
stays the default and the owner of every writing. `sentBy` is not demarcation: it is an ordinary xstate guard, written
|
|
253
|
+
only where a writing must decline other senders, and it cannot give a writing to anyone.
|
|
254
|
+
|
|
255
|
+
## Why there is no injection model (the retired ADR-0003)
|
|
256
|
+
|
|
257
|
+
jr2's first composability story — "workflows are templates of injected providers": Machines reference their moving parts
|
|
258
|
+
as named xstate slots, filled at deployment via `provide({ actors, actions, guards })`, with noop defaults — was tried
|
|
259
|
+
and retired (its ADR, 0003, is deleted; this section is its record). It never composed: xstate's `provide()` fills only
|
|
260
|
+
the machine it is called on and **cannot reach actors inside child machines**, so host-side injection cannot compose
|
|
261
|
+
past one level — and the host has injected nothing since ADR-0011's static-import doctrine ("actor logic is code; live
|
|
262
|
+
things are built per-invocation from serializable input"). Workflows author machines against statically-imported
|
|
263
|
+
mechanisms. What survives of the idea: the kernel "everything pluggable is an xstate actor", and `.provide()` as the
|
|
264
|
+
unit-test seam. The mock-vs-real swap it wanted lives at the wire instead (ADR-0011's stub Harness). GLOSSARY.md's
|
|
265
|
+
**Template** and **Provider** entries retired with it.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Agent-turn mechanics are jr2-internal: ambient coordinates, host-side offsets, absorbed retries, fresh sessions
|
|
2
|
+
|
|
3
|
+
Second of the workflow-API redesign trio ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md) has the context;
|
|
4
|
+
full record in
|
|
5
|
+
[docs/design/workflow-api/](https://github.com/snapwich/jr2/blob/71ebe36/docs/design/workflow-api/proposal.md)).
|
|
6
|
+
`agentRun`'s consumer input shrinks to workflow vocabulary — `{ agent, prompt }` plus opt-ins below — because everything
|
|
7
|
+
else it used to take was jr2 handing the consumer values jr2 already had. Each absorption follows, with what it deletes
|
|
8
|
+
from a jr-shaped workflow.
|
|
9
|
+
|
|
10
|
+
The opt-ins include two optional dials, `model` and `thinkingLevel`
|
|
11
|
+
([ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md) draws the identity-vs-dial line and
|
|
12
|
+
states where each is validated). The **absorption principle is what admits them**: what this ADR absorbed was values jr2
|
|
13
|
+
already held and was handing back to the consumer to hand in again — mechanism wearing a consumer's clothes. A dial is
|
|
14
|
+
the opposite case: a genuine per-call choice that jr2 cannot derive, because Agents are instance-scoped and only the
|
|
15
|
+
invoking workflow knows how hard this turn is. Both are serializable strings, so they ride the persisted child input; a
|
|
16
|
+
restore re-attaches to a Submission whose model is already fixed server-side and never re-resolves them.
|
|
17
|
+
|
|
18
|
+
## Ambient endpoint and sandbox (deletes the `turn()` helper and all handle threading)
|
|
19
|
+
|
|
20
|
+
`workspace()`'s `running` state co-invokes a registrar actor that records its handles in a `WeakMap<ActorRef, Handles>`;
|
|
21
|
+
`agentRun` walks `self._parent` to the nearest registered ancestor. Invoked-actor registration (not an entry action) is
|
|
22
|
+
what makes this restore-safe: invoked actors restart on snapshot restore, entry actions do not. Not under a workspace →
|
|
23
|
+
fail loudly, unless input carries an explicit `endpoint` (the dev-stub, workspace-less path). Body-facing workspace
|
|
24
|
+
handles are `{ repos, branch }` ([ADR-0012](0012-workspace-wrapper-machine.md)).
|
|
25
|
+
|
|
26
|
+
**[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)'s token scoping survives and
|
|
27
|
+
strengthens**: the registration records the Sandbox resolved from the enclosing wrapper — the same deterministic
|
|
28
|
+
`workspaceName()` the token was minted for — so the consumer can no longer forget to pass it (the baba71f incident:
|
|
29
|
+
`sandbox` omitted, every tool call 403'd, fail-closed but silent). The parent chain resolves the _enclosing_ wrapper
|
|
30
|
+
structurally, never a sibling's; cross-feature event injection stays impossible.
|
|
31
|
+
|
|
32
|
+
## Offsets live in a host ledger (deletes `context.offsets` and the `agent.offset` event)
|
|
33
|
+
|
|
34
|
+
`agentRun` reports `iid → offset` through the run binding into a ledger persisted beside the snapshot in the same
|
|
35
|
+
`RunBlob` save. Iids are globally unique, so restore's recursive nested-context offset folding dies with the context
|
|
36
|
+
leg; restore still rewrites the child's persisted input ([ADR-0007](0007-durable-machine-state.md)'s invariant stands
|
|
37
|
+
verbatim — only the offset's location moved). The Harness wire does the heavy lifting
|
|
38
|
+
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)): `wait` reconnects from the admitted
|
|
39
|
+
offset, and `abort` is the deliberate terminal cancel ([ADR-0002](0002-duplex-streaming-actor.md)).
|
|
40
|
+
|
|
41
|
+
The ledger's key is the iid, and an iid is not a Turn. Two Turns that continue one conversation share it, and nothing
|
|
42
|
+
removes a Turn's entry when the Turn ends. So the input mapper also mints a **Turn ID** (`turnId`), one per invocation,
|
|
43
|
+
and it persists with the child's input. The actor stamps it on every admission that it ledgers. The restore re-attaches
|
|
44
|
+
a child only to an admission with the child's own Turn ID. An admission with another Turn ID is an earlier Turn's: the
|
|
45
|
+
child's admission was not ledgered, so the child keeps its prompt and is admitted again. If its admit reached the
|
|
46
|
+
Harness before the crash, the prompt is then sent twice. This is ADR-0007's known durability edge, an at-most-once
|
|
47
|
+
re-POST, and it now applies to a continued Turn as it does to a first one. Before the Turn ID, a crash in that window
|
|
48
|
+
restored the Turn onto the earlier Turn's Submission. The Turn then followed a stream that was not its own, and its
|
|
49
|
+
prompt was never sent. A plain `setup()` Machine that passes finalized input mints no Turn ID, and the key alone
|
|
50
|
+
decides.
|
|
51
|
+
|
|
52
|
+
## Retries and nudges are absorbed; the workflow sees one terminal `agent.fault` (deletes the retry bookkeeping)
|
|
53
|
+
|
|
54
|
+
Two fault classes, deliberately not one knob ([ADR-0035](0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)
|
|
55
|
+
later adds the third: runaway):
|
|
56
|
+
|
|
57
|
+
- **Infra faults** (stream drop, pod restart, provider error): provider-stream retry lives inside the turn in the
|
|
58
|
+
Harness, and `wait` reconnects from the offset ledger without a deadline; a restarted Harness or a lost Workspace ends
|
|
59
|
+
the Turn through the Sandbox watch (ADR-0021), and a Harness that answers 404 is a lost conversation (ADR-0027). The
|
|
60
|
+
one exception is a restarted Harness while the Agent's state is busy, or with a pick's answer unread: that cuts the
|
|
61
|
+
Turn and faults nothing — the actor waits for Rest and **re-admits** on the same conversation, with the answer as a
|
|
62
|
+
notice (ADR-0024, ADR-0031). A re-admission is mechanism, like a nudge: telemetry on the feed, no marker, and it
|
|
63
|
+
spends no budget, because the model did nothing wrong.
|
|
64
|
+
- **No-signal** (the agent stops while its Turn is still open — jr's dominant failure mode): the wire treats that as a
|
|
65
|
+
normal completed Submission, so recovery is jr2's. **The ask-again rule**: while the Turn is open, a model that
|
|
66
|
+
stopped is asked again once its Allowed picks are not empty, and never with an empty list. A delivered pick does not
|
|
67
|
+
end the question — a pick with no target, or one into a mode, keeps the Turn open (ADR-0024) — only the Turn ending
|
|
68
|
+
does. One rule, asked at three moments:
|
|
69
|
+
- **The step boundary.** Inside the Submission, the Harness restates the Turn part when the Allowed picks changed
|
|
70
|
+
since the model last read them (ADR-0029); that message keeps the engine's loop going and costs no Submission. It
|
|
71
|
+
never restates an empty list to a model that stopped.
|
|
72
|
+
- **The nudge.** The Submission settled with picks left: the Agent's Actor admits a **nudge** on the same conversation
|
|
73
|
+
— "Your previous turn ended, and the workflow is still waiting for your pick." — absorbing ADR-0006's "forced final
|
|
74
|
+
pick is a Machine-level re-prompt" into the actor.
|
|
75
|
+
- **The wake.** The Submission settled with none left: the model stopped correctly, and the Turn waits on a Gate, a
|
|
76
|
+
child's invoke or a timer — the Allowed picks are empty while the Agent's state is not at Rest (ADR-0024). The actor
|
|
77
|
+
watches the invoking Machine and admits the same nudge the first time its Allowed picks are not empty, read after
|
|
78
|
+
the mailbox drains like Rest itself, so a state that sends to itself on its way to more work wakes nobody. The watch
|
|
79
|
+
ends with the state, so a Turn that ended is never woken, and a Turn that was never meant to pick waits until its
|
|
80
|
+
state exits.
|
|
81
|
+
|
|
82
|
+
The nudge and the wake are one Submission kind and spend one budget: a Machine that opens and closes picks under a
|
|
83
|
+
stopped model cannot ask it without bound. All three read one Allowed list, the one the surface serves (ADR-0029).
|
|
84
|
+
Their admission is for the conversation the Turn holds, so it says `kept` (ADR-0062). A Harness that lost the
|
|
85
|
+
conversation while the Turn waited refuses it and seats none, and the Turn is admitted again on a new conversation
|
|
86
|
+
with its task, or faults when it has no task to send. That ask was spent already, so the budget counts it once.
|
|
87
|
+
|
|
88
|
+
Budgets are defaulted knobs, not Machine context. Exhaustion emits the single terminal `agent.fault { reason }`; where
|
|
89
|
+
it routes (escalation, a triage/investigator state, a Failure) is workflow policy, but it must route in a Machine: the
|
|
90
|
+
walk refuses a state that invokes an Agent with no `agent.fault` route on itself or an ancestor, and an `agent.fault`
|
|
91
|
+
that no transition takes faults the run
|
|
92
|
+
([ADR-0074](0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md)). In a Chart the route is
|
|
93
|
+
optional: the Open body sends an unrouted fault to the Machine that invoked it, whose route takes it
|
|
94
|
+
([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)). Attempts surface as run-feed/visualize telemetry projected
|
|
95
|
+
to `{ child, attempt }` — no iids on the open observation feed ([ADR-0014](0014-observation-is-open-run-state-is-not.md)
|
|
96
|
+
unchanged).
|
|
97
|
+
|
|
98
|
+
## Sessions are fresh by default; continuation is opt-in
|
|
99
|
+
|
|
100
|
+
jr's most deliberate design decision is the **lossy handoff** — every relaunch is a fresh session; revision coders read
|
|
101
|
+
the notes and the code, never the prior agent's conversation. That is the default: each `agentRun` invocation is a new
|
|
102
|
+
conversation. `continue: true` opts into continuation
|
|
103
|
+
([ADR-0057](0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md)): the Turn lands on this Agent's one
|
|
104
|
+
conversation in this Machine instance, iid `<run>/<machine actor path>/<agent>` plus the epoch jr2 bumps on a terminal
|
|
105
|
+
fault; the per-invocation prompt lands as the next user turn; the tool menu still re-derives from the invoking state
|
|
106
|
+
(one conversation travels across the states of its Machine). Mid-turn restore re-attaches the in-flight turn in both
|
|
107
|
+
modes — `continue` governs only what a _new invocation_ means. The author names nothing: in a state machine the
|
|
108
|
+
conversation's identity is already on the page. (This corrects the jr-parity exercise's "same iid = jr's resume
|
|
109
|
+
machinery, free" — an inversion of jr's actual semantics.)
|
|
110
|
+
|
|
111
|
+
Two live invocations of one continued iid — parallel states — are refused at the registration table: one live surface
|
|
112
|
+
per address. A Submission that arrives behind a live one on the Harness queues: prompts for one instance enter one
|
|
113
|
+
per-instance queue in admission order, and a Submission is promoted only when it is the first unsettled one for its
|
|
114
|
+
conversation (ADR-0027 states this as jr2's own semantics). A queued prompt waits behind work that may never finish,
|
|
115
|
+
which is why [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) both ends the turn and orders the
|
|
116
|
+
abort ahead of the next `send`.
|
|
117
|
+
|
|
118
|
+
The minting doctrine — addresses are computed by jr2, never by the workflow — covers gate ids too: a gate with no
|
|
119
|
+
authored id derives one from its actor path below the run root, through the same walk `mintIid` uses (`actorPath` in
|
|
120
|
+
registration.ts; ADR-0011). The two mint at different seats for a reason: iids must be minted in the input mapper
|
|
121
|
+
because minting has a random component (the fresh-session suffix) and a ledger read (the continued conversation's epoch)
|
|
122
|
+
— the input is what persists — while a path-derived gate id is deterministic from machine structure, so it is recomputed
|
|
123
|
+
at every actor (re)start and is restore-stable by construction. The mapper rule is about non-determinism, not a blanket
|
|
124
|
+
rule about where ids are born.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# The run loop is a jr2 `pool` over a generalized source port
|
|
2
|
+
|
|
3
|
+
Third of the workflow-API redesign trio ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md),
|
|
4
|
+
[ADR-0016](0016-agent-turn-mechanics-are-internal.md); full record in
|
|
5
|
+
[docs/design/workflow-api/](https://github.com/snapwich/jr2/blob/71ebe36/docs/design/workflow-api/proposal.md)). The top
|
|
6
|
+
of every jr-shaped workflow — claim ready items, run one child per item under a cap, wake on push or poll, settle when
|
|
7
|
+
done — was ~90 lines of hand-rolled bookkeeping in the jr-parity exercise (`active[]`, `spawnChild`/`stopChild`,
|
|
8
|
+
`xstate.done.actor.*` casts, the discover/saturated/settling/idle choreography, and a comment-enforced "keep
|
|
9
|
+
`spawnChild` top-level or visualize goes blind" footgun). No xstate-ecosystem primitive exists for this (verified: the
|
|
10
|
+
canonical v5 pattern _is_ the hand-rolled loop), so jr2 ships one.
|
|
11
|
+
|
|
12
|
+
## `pool(worker, spec)` — a machine factory, like `workspace()`
|
|
13
|
+
|
|
14
|
+
`pool(worker, { source, itemId, cap, itemInput, onDrained, failures })` returns a plain machine that owns
|
|
15
|
+
spawn-under-cap, stable child identity, typed completion collection (`DoneActorEvent<Output, string>` + the
|
|
16
|
+
`xstate.done.actor.*` wildcard — zero casts), `stopChild` bookkeeping, the wake gate, and the re-query timer. The one
|
|
17
|
+
top-level `spawnChild` lives in jr2's pool code, once — the visualize footgun is dissolved by construction, and
|
|
18
|
+
everything durable is a registered string src (xstate cannot persist inline-src children, which independently forces
|
|
19
|
+
this shape). The pool composes: it returns a plain machine and can itself be nested as a child. The typed idioms
|
|
20
|
+
(wildcard done events, `assertEvent`, outcome-in-context root output) ship as documented patterns regardless, so
|
|
21
|
+
outgrowing the pool means hand-rolling without penalty, not fighting a framework.
|
|
22
|
+
|
|
23
|
+
## The source port is generalized; Work Source is one adapter
|
|
24
|
+
|
|
25
|
+
The pool draws from a **Source** port — `next(active) → item | null`, plus an optional `wake` event def (webhook /
|
|
26
|
+
`jr2 send` push seam) and `pollEvery` (the set mutates underneath us) — a queue, a generator, or a re-queried set. tk's
|
|
27
|
+
claim actor is one adapter; GLOSSARY.md's **Work Source** reframes as _the Source adapter for tickets_, and the
|
|
28
|
+
Ready-set doctrine (re-query, never materialize) survives as that adapter's semantics, not the primitive's.
|
|
29
|
+
|
|
30
|
+
A Work Source — `tk`, GitHub issues, Jira, a task queue — is a behavior port, not a canonical data schema: verbs like
|
|
31
|
+
`claimNext`, `updateStatus` and `comment`, each gated by a capability the adapter advertises. It owns the dependency
|
|
32
|
+
graph and hands out the Ready-set, the currently-unblocked items in dependency order, so the Machine never encodes a
|
|
33
|
+
dependency edge. The Ready-set mutates during execution — items can be reopened or created mid-run — so it is not a
|
|
34
|
+
static FIFO queue.
|
|
35
|
+
|
|
36
|
+
## Terminal triage is three-valued, surfaced as status
|
|
37
|
+
|
|
38
|
+
Forced by parking-is-retention (ADR-0012) plus workflows whose children park on gates (escalation, human review):
|
|
39
|
+
|
|
40
|
+
- **drained** — source empty and all children settled → final, or the run ends `failed` when items failed (below);
|
|
41
|
+
- **deadlocked** — open-but-never-ready items with nothing progressing → surfaced distinctly (jr exit 2), not an
|
|
42
|
+
indistinguishable idle poll, or the run ends `failed` when items failed (below);
|
|
43
|
+
- **waiting** — children parked in gates → the run stays open and the gates list is the "what needs me" surface (jr exit
|
|
44
|
+
3).
|
|
45
|
+
|
|
46
|
+
The pool can make this call because it owns both the source and the children — the strongest single argument for the
|
|
47
|
+
primitive over an authored loop. Triage surfaces as run _status_ (feed, `jr2 runs`), not machine events; a
|
|
48
|
+
`pool.deadlock` event can be added if a workflow ever needs to react rather than a human.
|
|
49
|
+
|
|
50
|
+
## The pool catches a worker's Failure
|
|
51
|
+
|
|
52
|
+
A worker that fails ([ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)) ends
|
|
53
|
+
only its own item. The pool catches the Failure on `xstate.error.actor.*`, keeps its payload in `failures` by item id,
|
|
54
|
+
stops that worker, and claims again: the output is `{ status, items, failures }`. The pool is a kit-owned parent, so an
|
|
55
|
+
author cannot give it an `onError`; if it let the Failure go up, one lost `task` would end the whole run `failed`, and
|
|
56
|
+
the host would delete the Sandbox of each sibling, also one that waits at its `review` Gate with commits that it did not
|
|
57
|
+
push. That breaks parking-is-retention for work that did not fail. A defect in a worker still goes up and ends the run
|
|
58
|
+
`error`.
|
|
59
|
+
|
|
60
|
+
The option `failures: "fail" | "report"` says how a pool ends when one or more items failed. It catches each worker's
|
|
61
|
+
Failure in both modes, so one failed item never stops its siblings. With `"fail"`, the default, a pool that ends
|
|
62
|
+
`drained` or `deadlocked` with failures invokes `fail` with the reason `"<n> of <m> items failed"` and the detail
|
|
63
|
+
`{ status, items, failures }`, so the run ends `failed` and `jr2 run` exits 1. At either end no worker is live, because
|
|
64
|
+
a pool with a live worker parks, so the Failure deletes no sibling's Sandbox. With `"report"`, the pool ends `done` and
|
|
65
|
+
`failures` in its output says which items did not succeed: for a pool whose job is to try each item, where a failed item
|
|
66
|
+
is a correct answer. The default is `"fail"` because `done` means that the work succeeded (ADR-0073), and a CI loop must
|
|
67
|
+
not exit 0 while every item failed. The deadlock is the usual end of a dependency backlog with a failure: a Source over
|
|
68
|
+
a tracker keeps the failed item open, so its dependents never become ready. The `status` in the detail tells the two
|
|
69
|
+
ends apart, and the two details have one shape.
|