@jr2/orchestrator 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/bin/server.ts +7 -4
- package/console/canvas.ts +27 -14
- package/console/components/app.ts +12 -1
- package/console/components/fleet.ts +52 -6
- package/console/components/machine-pane.ts +128 -2
- package/console/components/nav.ts +1 -1
- package/console/components/schema-form.ts +12 -4
- package/console/json-field.ts +23 -0
- package/console/main.ts +122 -39
- package/console/store.ts +321 -21
- package/console/style.css +111 -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 +6 -4
- package/src/actor.ts +976 -61
- package/src/agent.ts +114 -38
- package/src/ambient.ts +4 -1
- package/src/assembly-checks.ts +347 -0
- package/src/assembly-tar.ts +494 -0
- package/src/assembly.ts +721 -0
- package/src/cause.ts +234 -0
- package/src/chart.ts +2182 -0
- package/src/charts.ts +748 -0
- package/src/config.ts +520 -162
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +6 -5
- package/src/custodian.ts +143 -39
- package/src/customize.ts +388 -55
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect-checks.ts +557 -0
- package/src/effect.ts +1202 -0
- package/src/effector-wire.ts +674 -0
- package/src/fail.ts +209 -0
- package/src/fingerprint.ts +4 -3
- package/src/gate.ts +8 -0
- package/src/git-clone.ts +65 -0
- package/src/git-push.ts +336 -0
- package/src/harness-client.ts +764 -48
- package/src/held-secrets.ts +82 -26
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +1423 -0
- package/src/http.ts +916 -113
- package/src/images.ts +47 -21
- package/src/index.ts +62 -43
- package/src/instance.ts +505 -59
- package/src/internal.ts +84 -0
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +106 -6
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +66 -8
- package/src/notices.ts +141 -0
- package/src/open-body.ts +1580 -0
- package/src/open-effect.ts +217 -0
- package/src/open.ts +59 -6
- package/src/parts.ts +709 -41
- package/src/party.ts +534 -0
- package/src/pick.ts +761 -0
- package/src/pool.ts +248 -37
- package/src/registration.ts +382 -19
- package/src/repo-fetch.ts +60 -85
- package/src/repos.ts +65 -75
- package/src/rest.ts +133 -0
- package/src/run-host.ts +2383 -115
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +1126 -0
- package/src/sandbox-effector.ts +111 -0
- package/src/sandbox-kube.ts +1721 -0
- package/src/sandbox-watch.ts +260 -0
- package/src/secret-placement.ts +529 -0
- package/src/server.ts +284 -84
- package/src/setup.ts +819 -117
- package/src/size.ts +454 -0
- package/src/snapshot-store.ts +115 -20
- package/src/stub-harness.ts +49 -16
- package/src/sync-client.ts +54 -0
- package/src/timers.ts +191 -0
- package/src/tokens.ts +35 -4
- package/src/vocabulary.ts +135 -15
- package/src/wire.ts +238 -10
- package/src/workspace.ts +1195 -338
- package/src/sandbox-kubectl.ts +0 -1108
package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# The Harness is jr2's own server: flue retires, the wire stays
|
|
2
|
+
|
|
3
|
+
jr2 has been programming around its Harness, not with it. The evidence is the ADR trail itself.
|
|
4
|
+
[ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md)'s entire codegen-at-pod-start mechanism —
|
|
5
|
+
generated shims, a boot-time `flue build`, agent names frozen at that build — exists because flue offers no programmatic
|
|
6
|
+
server assembly; its own Considered options says so. [ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md) was
|
|
7
|
+
forced by a foreign advisory session jr2 never asked for, and records a rejection ("there is nothing on the wire to
|
|
8
|
+
branch on") that is only true because someone else writes the wire.
|
|
9
|
+
[ADR-0023](0023-the-harness-prints-the-conversation.md)'s printer needed a loopback SSE client plus a 40-attempt
|
|
10
|
+
404-chase against flue's own parking behavior. The `flue-contract` tier ([ADR-0010](0010-bdd-acceptance-tests.md)) holds
|
|
11
|
+
a test asserting a defect we are waiting on someone else to fix: on pinned beta.9, an abort landing mid-stream erases
|
|
12
|
+
the assistant message from every later turn — fixed only on flue's unreleased line. And the Agent tool-restriction gap
|
|
13
|
+
has no seat in beta.9 at all: `SandboxFactory.tools` replaces the whole list, the default tools are private to a bundled
|
|
14
|
+
chunk, and ADR-0018's recorded escape hatch — "eject to a real flue project" — un-decides ADR-0018.
|
|
15
|
+
|
|
16
|
+
Waiting was the standing plan, and flue 2.0 ends it. The 2.0 nightlies do export tool constructors, but as part of a
|
|
17
|
+
rewrite: `defineAgent` is gone (an `AgentFunction` with hooks replaces it), `connectMcpServer` is renamed, the workflow
|
|
18
|
+
exports are absent from the runtime index, and the schema resets. `latest` has sat frozen at `1.0.0-beta.9` for a month
|
|
19
|
+
while all activity lands on an unannounced 2.0 line. That is a framework heading somewhere else, carrying our defect fix
|
|
20
|
+
with it.
|
|
21
|
+
|
|
22
|
+
Meanwhile the thing jr2 actually needs from flue is small, and jr2 already owns its specification. The Orchestrator
|
|
23
|
+
imports `@flue/sdk` in exactly one module (`flue-client.ts`), uses three verbs — send, wait, abort — behind
|
|
24
|
+
`AgentRunPort`, and persists a three-string admission handle. The stub Harness (`stub-harness.ts`) is a hand-written,
|
|
25
|
+
SDK-verified model of the five wire endpoints. And the base flue builds on is public: `@earendil-works/pi-agent-core`
|
|
26
|
+
(MIT, independent of flue's authors) — from which flue imports only the low-level `Agent` loop and reimplements the
|
|
27
|
+
rest. jr2 takes the same slim core: pi-agent-core gives the `Agent` loop, its tool hooks (`beforeToolCall`,
|
|
28
|
+
`finishTurn`, `prepareRequest`), the abort, the steering queue and an in-process event stream. The tool-restriction gap
|
|
29
|
+
does not exist for a caller that owns tool assembly. jr2 owns the rest: the engine part on disk, Compaction
|
|
30
|
+
([ADR-0036](0036-a-turn-compacts-its-own-context-mid-flight.md)) and the six Working tools, whose text the model reads.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
- **jr2 owns its Harness: `@jr2/harness` at `packages/harness`**, built on `@earendil-works/pi-agent-core` +
|
|
35
|
+
`@earendil-works/pi-ai` (pinned exact; a minor can break, and pi 1.0 removed the session, Compaction and the coding
|
|
36
|
+
tools from pi-agent-core) and Hono, which the repo already uses. The stock image keeps its tag
|
|
37
|
+
(`jr2-harness:<kitversion>`) and every operator contract: numeric uid, binding `:8080` is Ready, `git` in the image —
|
|
38
|
+
plus `ripgrep` for the grep tool.
|
|
39
|
+
- **pi is the engine, not the contract.** The Harness keeps its contract in jr2's own shapes. One seam (`Engine`,
|
|
40
|
+
`engine.ts`) is the interface between the Turn and the engine. The Turn gives the engine one Submission's run: the
|
|
41
|
+
model, the thinking level, the system prompt, the tools, the context and the prompt. It hears the run as events
|
|
42
|
+
(`message_end`, `step_boundary`, `tool_start`, `tool_end`), and it can `steer` and `abort` the run. It counts the
|
|
43
|
+
Runaway watch's steps at `tool_start` (ADR-0035): the engine sends `tool_start` for every call of a model answer, in
|
|
44
|
+
order, before it checks the tool or the arguments, and the event carries the call's name and arguments. A listener can
|
|
45
|
+
abort the run there. Inside the run, the Turn decides through four hooks:
|
|
46
|
+
- `convertToLlm`: the messages that the provider gets.
|
|
47
|
+
- `beforeRequest`: Compaction (ADR-0036).
|
|
48
|
+
- `beforeToolCall`: the blocks of ADR-0024 (a second pick of one model answer, and a call after a pick that ended the
|
|
49
|
+
Turn), and the block of a tripped runaway watch (ADR-0035). A block can also abort the run.
|
|
50
|
+
- `endsRun`: the end of the run at the step boundary, after a trip or when the Turn is over.
|
|
51
|
+
|
|
52
|
+
The engine runs the tool calls of one model answer one at a time, in the order the model wrote them. A call's
|
|
53
|
+
`beforeToolCall` comes after the result of the call before it is complete. This rule is part of the seam, not a choice
|
|
54
|
+
of pi's. pi's own default is `parallel`, which starts each call before the call ahead of it ends. So the pi adapter
|
|
55
|
+
sets `toolExecution: "sequential"`. The reason is a measurement on the real Anthropic API (Haiku 5.5, 2026-10-22), in
|
|
56
|
+
the `@model` claim `continue-after-signed-push`
|
|
57
|
+
([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)). The model sent
|
|
58
|
+
`bash` with `git add … && git commit …` and the pick `publish` in one model answer. In 3 of 10 trials, `publish`
|
|
59
|
+
pushed before the commit existed. A `write` and a `bash` test run in one model answer had the same race. The rule is
|
|
60
|
+
for all tools, the Working tools and the Menu. The cost is that independent reads of one model answer do not run at
|
|
61
|
+
the same time. Four facts follow:
|
|
62
|
+
- Only the first pick of a model answer runs
|
|
63
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). The model wrote a later one before it
|
|
64
|
+
read the first one's answer. The seam states two more facts, so that the Turn can find the first pick by itself. The
|
|
65
|
+
engine tells a model answer (`message_end`) before it asks `beforeToolCall` about a call of it. And it asks only
|
|
66
|
+
about a call of a tool the run has, with valid arguments. So the first pick is the first that reached
|
|
67
|
+
`beforeToolCall` in that model answer. A call of an unknown tool, or with invalid arguments, gets the engine's own
|
|
68
|
+
error, and it is never the first pick. A list of the model answer's calls would name such a call, and it would need
|
|
69
|
+
an id that some providers do not send.
|
|
70
|
+
- A call after a pick whose answer ended the Turn does not run: `beforeToolCall` blocks it. `endsRun` then ends the
|
|
71
|
+
run at the step boundary, so no model request follows. The Submission does not settle then: it waits for the
|
|
72
|
+
Orchestrator's sweep, which says how the Turn ended (ADR-0024), at most 10 s.
|
|
73
|
+
- After an abort or a Runaway's trip ([ADR-0035](0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)), the
|
|
74
|
+
calls before it have run. The engine runs no later call of that model answer and gives it no result. When the run
|
|
75
|
+
ends, the Turn writes jr2's answer for each such call in the engine part: "This call did not run: your turn ended
|
|
76
|
+
before it started." Without it, pi-ai answers "No result provided", which does not say whether the call ran.
|
|
77
|
+
[ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md) gives the restart's case.
|
|
78
|
+
- Each "did not run" result is an error result: a blocked call, and a call that the run never started. The call did
|
|
79
|
+
not do its work, and the model may make it again. A cut or unsent pick's answer (ADR-0031) is not an error, and
|
|
80
|
+
neither is the answer of a pick that the Harness may have delivered (ADR-0002), because an error reads as a failed
|
|
81
|
+
pick, and a cut pick may have run.
|
|
82
|
+
|
|
83
|
+
The seam's tools, events and hooks are jr2's types. The seam's messages are pi-ai's provider messages, plus jr2's
|
|
84
|
+
Compaction summary. pi-ai is the provider library, not the engine (see below), so these shapes are not the engine's.
|
|
85
|
+
An adapter (`EngineAdapter`) opens the conversation's engine part and makes its engine. `pi-engine.ts` is the one
|
|
86
|
+
adapter, and the one module that imports pi-agent-core. It holds pi's `Agent` loop, its hooks and events, and the
|
|
87
|
+
leading system message that carries the system prompt and the tool declarations (pi 1.x's way).
|
|
88
|
+
|
|
89
|
+
The engine part (ADR-0031) is engine-named: the adapter chooses its directory (`engine/pi`). Its format is jr2's
|
|
90
|
+
transcript (`transcript.ts`), not pi's, because pi 1.x keeps no conversation. The Turn and Compaction (ADR-0036) read
|
|
91
|
+
and write it. So the adapter does not own the format. It owns the name. The name is the guard: only the engine that
|
|
92
|
+
wrote a part is known to read it correctly, because that engine's loop decides what goes into it. So a Harness on
|
|
93
|
+
another engine opens another directory, does not read pi's part, and continues from the record's history.
|
|
94
|
+
|
|
95
|
+
pi-ai is the provider library that every engine sends its requests through, so the Harness uses it directly:
|
|
96
|
+
- the model catalog and the custom provider (`provider.ts`);
|
|
97
|
+
- the message and tool-declaration types;
|
|
98
|
+
- TypeBox `Type` for the Working tools' schemas (pi-ai coerces the arguments of a TypeBox schema differently from
|
|
99
|
+
those of a plain JSON Schema);
|
|
100
|
+
- `uuidv7`, for two ids that go to a provider as a prompt-cache key (pi-ai's `sessionId`): the engine part's id, and
|
|
101
|
+
the id of each Compaction summary request;
|
|
102
|
+
- `contentText`: the summary request joins a message's text by pi-ai's rule, as pi 0.82.1 did;
|
|
103
|
+
- `retryAssistantCall`: the summary call retries by the same rule as each provider request.
|
|
104
|
+
|
|
105
|
+
A second engine is a second adapter. One module chooses the Harness's adapter (`engine-choice.ts`), and the Turn names
|
|
106
|
+
no adapter: a test rig can pass another (`TurnDeps.engineAdapter`). So a second engine changes `engine-choice.ts`, not
|
|
107
|
+
the Turn. The Working tools, the printer, the transcript and Compaction do not change.
|
|
108
|
+
|
|
109
|
+
- **Assembly is runtime construction, not codegen.** `main.ts` constructs the server and listens; each admission carries
|
|
110
|
+
its Agent's definition (ADR-0049), validated loudly and re-read per Submission. `boot.mjs`, the generated shims, the
|
|
111
|
+
boot-time `flue build`, and the readiness lag it caused are deleted, and so — later — was the `JR2_AGENTS_JSON` roster
|
|
112
|
+
this cut read at boot. The ADR-0018 contract — stock image, user-owned plain-data definitions — survives with its
|
|
113
|
+
mechanism simplified.
|
|
114
|
+
- **The wire stays, and becomes normative.** The five endpoints the stub documents (`POST /agents/:name/:id`,
|
|
115
|
+
updates/history views, long-poll, `POST .../abort {cause?, submissionId?} → {aborted}`) and the three-string admission
|
|
116
|
+
`{streamUrl, offset, submissionId}` (jr2 now mints its own opaque values) are jr2's protocol, not flue's. The stub
|
|
117
|
+
Harness, the admission ledger ([ADR-0016](0016-agent-turn-mechanics-are-internal.md)), `AgentRunPort`, and every
|
|
118
|
+
mechanics/`@kind` assertion came through the move from flue byte-identical. Later decisions changed them in place. The
|
|
119
|
+
sweep's cause and the outcome `ended` ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md))
|
|
120
|
+
changed the stub's settlements, the port's `abort`, and the mechanics and `@kind` assertions of a Turn that its own
|
|
121
|
+
pick ended. Long-poll is the only wait transport; the SDK's `?wait=result` and SSE paths are not part of the contract.
|
|
122
|
+
The admit body is `{message, model?, thinkingLevel?}` — the optional dials
|
|
123
|
+
([ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md)) are this Submission's override layer
|
|
124
|
+
over the Agent's definition, omitted when unset, and an unresolvable one is a 400 at admission rather than a
|
|
125
|
+
Submission that settles `failed` — a one-line addition precisely because the protocol is jr2's own.
|
|
126
|
+
|
|
127
|
+
- **The re-owned semantics are stated, not inherited:**
|
|
128
|
+
- **Per-instance queue: accept and queue.** A Submission is promoted when it is the first unsettled Submission of its
|
|
129
|
+
conversation; admission order. [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)'s correction
|
|
130
|
+
— flue queues, and the ordered abort depends on it — is now a design jr2 asserts rather than a behavior it
|
|
131
|
+
discovered.
|
|
132
|
+
- **Abort sweeps, and says its cause.** The active Submission and everything queued at the moment the abort is
|
|
133
|
+
processed settle in admission order. The response answers `{aborted: boolean}` (`false` on an idle instance).
|
|
134
|
+
Arrival order at the Harness is the order; the client-side pending-abort await (ADR-0024) stays as the cross-request
|
|
135
|
+
guarantee. The body is optional, and it is the Orchestrator's judgment of how the Turn ended:
|
|
136
|
+
- `{cause: "pick", submissionId}` says that the Turn's own pick ended it. The active Submission settles `ended` when
|
|
137
|
+
the body names it.
|
|
138
|
+
- Every other body settles it `aborted`: no body, a body that does not parse, a cause the Harness does not know, or
|
|
139
|
+
a `submissionId` of another Submission (or none).
|
|
140
|
+
- The active Submission may still be running. Or its run may have ended because a pick found the Turn over, and it
|
|
141
|
+
waits for this sweep. Either way the sweep settles it. When no sweep comes within 10 s, it settles `aborted` and
|
|
142
|
+
says so.
|
|
143
|
+
- A queued Submission never ran, so it settles `aborted` for every cause.
|
|
144
|
+
- An Orchestrator from before the field still works on the wire. But it gives no cause, so its Turns settle
|
|
145
|
+
`aborted`, every time.
|
|
146
|
+
- **Settlement is `completed | ended | failed | aborted`**, and the Orchestrator is the one judge of how a Turn ended
|
|
147
|
+
(ADR-0024). `completed`: the run returned, and the Turn is still open, so the Agent's Actor asks again (the nudge or
|
|
148
|
+
the wake, ADR-0016). `ended`: the Turn's own pick ended the Turn, and the sweep said `cause: "pick"`. The pick may
|
|
149
|
+
be from an earlier Submission of the same Turn: a nudge or a wake starts a new Submission in the same Turn.
|
|
150
|
+
`failed`: the run threw, a Runaway included
|
|
151
|
+
([ADR-0035](0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)). `aborted`: something else ended the Turn,
|
|
152
|
+
or no sweep came in time, or it was over before it started
|
|
153
|
+
([ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md)). `failed` and `aborted` carry an error, `aborted` the
|
|
154
|
+
type `{type: "submission_aborted"}` — continuity with what ADR-0024/0025 recorded. `completed` and `ended` carry
|
|
155
|
+
none: nothing went wrong. A Settlement also carries its Submission's `usage`: the input, output, cache-read and
|
|
156
|
+
cache-write tokens and the cost, summed over the Submission's model answers. The cost is pi-ai's, from its catalog
|
|
157
|
+
rates, so a `harness.provider` model with no rates costs 0. A Compaction summary's model call is not in it. The
|
|
158
|
+
field is optional. A Submission whose model never answered carries none, and so does one lost with a restarted
|
|
159
|
+
Harness, because its spend was lost with it. The reason is the `@model` tier
|
|
160
|
+
([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)): it records the
|
|
161
|
+
tokens and the cost of each trial, and the Settlement is the one place outside the pod that knows them. The
|
|
162
|
+
Orchestrator does not read it yet.
|
|
163
|
+
- **An unknown conversation is 404**, on both views. `POST` creates (that is admission), except an admission with
|
|
164
|
+
`kept: true`, which is 404 (with `kept: false` in the body) on an unknown conversation and creates nothing
|
|
165
|
+
([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)); abort answers `{aborted: false}`. This is
|
|
166
|
+
a deliberate divergence from the stub, which answers every read with an empty stream: the stub is inert by design,
|
|
167
|
+
but a real Harness that answered a lost conversation with silence would park a re-attached `wait` forever. Honesty
|
|
168
|
+
about loss beats an endless poll.
|
|
169
|
+
- **The durability contract is explicit: a conversation lives as long as its Sandbox, and on the Instance Harness as
|
|
170
|
+
long as its run** (ADR-0031). The Harness persists each conversation to a directory as it goes and rebuilds it on
|
|
171
|
+
boot, so a Harness restart inside a living pod keeps it; a Submission in flight at the restart settles `failed`.
|
|
172
|
+
What is durable across a lost pod is Orchestrator-side — the admission ledger and snapshot
|
|
173
|
+
([ADR-0007](0007-durable-machine-state.md)), and `wait`'s reconnect-from-offset. A conversation that is gone
|
|
174
|
+
surfaces as 404 → fault → workflow policy, the same family as `workspace.lost`. Provider-stream retry moves inside
|
|
175
|
+
the turn: the Harness states 2 retries, and pi-ai runs them (`maxRetries`) — the seat ADR-0016 delegated to flue's
|
|
176
|
+
`durability{}`.
|
|
177
|
+
- **[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)'s enabling fact is reproduced
|
|
178
|
+
as explicit code.** Each Submission reads `$JR2_CUSTODIAN_URL/agents/<iid>/surface` afresh and presents it to pi as
|
|
179
|
+
the Menu, so the menu is per-turn with no push channel, no turn index, no second port. Menu tools keep the
|
|
180
|
+
`mcp__jr2__<name>` naming — shipped instructions and the printer's prefix-stripping depend on it — though no MCP
|
|
181
|
+
carries them ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The
|
|
182
|
+
retire-the-previous idiom and ADR-0023's 404-chase existed to work around flue API gaps that are gone.
|
|
183
|
+
- **The Orchestrator side shrinks.** `harness-client.ts` replaces `flue-client.ts`: a fetch client for the three verbs,
|
|
184
|
+
with `wait` as a long-poll loop from the admitted offset (capped backoff, indefinite reconnect — pod death is
|
|
185
|
+
`workspace.lost`'s job) that resolves on `completed` alone and raises a typed `SettlementFault` on
|
|
186
|
+
`failed`/`ended`/`aborted`/404 (an `ended` reaches only an actor that its own pick already stopped, and a stopped
|
|
187
|
+
actor drops it). `actor.ts` did not change in the move from flue: the port, the admission shape, the nudge loop, and
|
|
188
|
+
the abort ordering stayed as they were. ADR-0024's sweep cause later changed it in two places. The port's `abort`
|
|
189
|
+
takes the cause and the Submission it names, and the Actor's `abandon` reads the cause before its registration goes.
|
|
190
|
+
|
|
191
|
+
## Considered options
|
|
192
|
+
|
|
193
|
+
- **Wait for flue 2.0.** Rejected: unreleased, unscheduled, and a rewrite of the surfaces jr2 generates against
|
|
194
|
+
(`defineAgent`, MCP wiring, routing). The structural problems — foreign advisory sessions, no programmatic assembly —
|
|
195
|
+
are architecture, not backlog. Meanwhile the pinned defect erases assistant messages on real abort timings.
|
|
196
|
+
- **Fork flue at beta.9.** Rejected: ~17K lines of built output plus a CLI codegen owned to keep seven imported symbols,
|
|
197
|
+
diverging from a codebase whose maintainers are mid-rewrite — the defect fix would have to be backported from a line
|
|
198
|
+
that left. And its durability layer, the one genuinely load-bearing-looking piece, is in-memory as jr2 deploys it.
|
|
199
|
+
- **Fresh on pi, new wire.** Rejected: redesigning the protocol throws away the tested contract, the stub, the ledger
|
|
200
|
+
shape, and the mechanics tier for no identified semantic gain. The wire was never the problem; the runtime behind it
|
|
201
|
+
was.
|
|
202
|
+
- **Fresh on pi, wire kept** — chosen.
|
|
203
|
+
- **Run in order only a model answer that holds a Menu pick** (pi's per-tool `executionMode: "sequential"` on each Menu
|
|
204
|
+
tool), and the other model answers in parallel. Rejected. It is narrower, and independent reads keep their speed. But
|
|
205
|
+
it leaves the race between Working tools: a `write` and the `bash` test run that reads its file. Also, the seam would
|
|
206
|
+
have no rule of its own: the order would be a fact of which tools a model answer holds.
|
|
207
|
+
- **Give `beforeToolCall` the names of the calls ahead of the call in its model answer** (`earlier`), for the first-pick
|
|
208
|
+
rule. Rejected. The list names a call that the engine did not prepare (an unknown tool, invalid arguments), so the
|
|
209
|
+
rule blocked a valid pick behind it. And the position of a call in the list needs the call's id, which a provider can
|
|
210
|
+
leave empty.
|
|
211
|
+
|
|
212
|
+
## Consequences
|
|
213
|
+
|
|
214
|
+
- **Deleted**: `packages/orchestrator/harness/` (boot.mjs, the flue project skeleton, the boot build and its readiness
|
|
215
|
+
lag), the printer's loopback client and 404-chase, ADR-0026's foreign-advisory rationale (the decision stands on its
|
|
216
|
+
other leg: the turn-start race), and the "eject to a real flue project" escape hatch — which means the tool contract
|
|
217
|
+
must now be answered, not re-deferred
|
|
218
|
+
([ADR-0028](0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md)).
|
|
219
|
+
- **Kept verbatim**: the wire, the stub as the mechanics fixture, `AgentRunPort` and the admission ledger, ADR-0024's
|
|
220
|
+
rule, receipt, and ordering, and the container split — restated in jr2 terms: the pod's credentials live in a separate
|
|
221
|
+
container (the Custodian, ADR-0059) _because working tools execute in the Harness container_, so the
|
|
222
|
+
credential-isolation premise survives while `local()` leaves the vocabulary.
|
|
223
|
+
- **The `flue-contract` tier dissolves into the conformance suite.** Its rig — scripted OpenAI-compatible provider, the
|
|
224
|
+
Menu over a real socket to a killable fake Orchestrator, real server — moves into `packages/harness/test/` and runs in
|
|
225
|
+
the default `test` gate: the opt-in-ness existed only because the tier owned a foreign pin and build. Its claims
|
|
226
|
+
become jr2 requirements, with the pinned-defect assertion inverted: an abort mid-stream must **not** erase the
|
|
227
|
+
assistant message. This suite is the first canary for pi bumps; `@kind` (ADR-0038) drives the same turn loop in a real
|
|
228
|
+
pod against a scripted model, and `@model`
|
|
229
|
+
([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)) against a real one —
|
|
230
|
+
run all three before bumping the pin.
|
|
231
|
+
- **The pinning risk moves; it does not vanish.** pi releases often and a minor can break. The pin is exact, a bump is a
|
|
232
|
+
deliberate kit change gated by the conformance suite, and the breaking surface is now the `Agent` loop's hooks and
|
|
233
|
+
pi-ai's provider adapters — not a code generator, a CLI build, and a process-global registry. What pi removed jr2 now
|
|
234
|
+
owns, so a bump cannot move it: the Working tools keep their text, and Compaction its algorithm.
|
|
235
|
+
- **Migration is phased behind the wire.** Because both ends speak the same protocol, the image swap and the
|
|
236
|
+
Orchestrator client swap are independent commits, each leaving every tier green; an old Orchestrator drives the new
|
|
237
|
+
image and vice versa. `@flue/*` leaves the repo when the last pin (the retired contract tier) is deleted.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# What an Agent may do to the Workspace is part of its definition
|
|
2
|
+
|
|
3
|
+
jr2 is exact about what an Agent may **say** and silent about what it may **do**. The control plane is a menu the
|
|
4
|
+
Machine sets per turn — derived from the invoking state's transitions
|
|
5
|
+
([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)), served from a separate container
|
|
6
|
+
([ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)), unforgettable by construction
|
|
7
|
+
([ADR-0016](0016-agent-turn-mechanics-are-internal.md)). The data plane is one identical working toolset with full write
|
|
8
|
+
access to the shared Workspace, for every Agent, in every state.
|
|
9
|
+
|
|
10
|
+
The gap is not hypothetical. In a live `task-with-review` run (2026-07-27), the coder found nothing to do and committed
|
|
11
|
+
nothing; the reviewer then edited `README.md`, set a `user.email`, committed, and approved its own commit — against
|
|
12
|
+
instructions that say "Do not modify the code yourself".
|
|
13
|
+
[ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) already established what that means: the same
|
|
14
|
+
prompt, model, and state produce compliance on one run and not the next, "which is what rules out fixing this with
|
|
15
|
+
words." The prose ban is a persona, not a boundary. ADR-0024 also named the stakes — an Agent with write access is a
|
|
16
|
+
concurrent writer in a Workspace the Machine believes is single-threaded — and
|
|
17
|
+
[ADR-0012](0012-workspace-wrapper-machine.md) puts coder and reviewer in that one Workspace on purpose.
|
|
18
|
+
|
|
19
|
+
Under the retired flue runtime this had no seat — its tool list was take-it-or-replace-it, and the only recorded out was
|
|
20
|
+
ejecting to a foreign harness project. [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)
|
|
21
|
+
retires that constraint: jr2 assembles the working tools per Submission, so what an Agent may do is finally something a
|
|
22
|
+
definition can state.
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
- **`AgentDefinition` gains `workspace?: "write" | "read" | "none"`, default `"write"`.** One word that describes the
|
|
27
|
+
persona — a reviewer reads, a decisioner needs no Workspace at all — not a tool inventory. It is deliberately **not**
|
|
28
|
+
named `tools`: that word already means the control-plane menu at the invoke seam (`actor.ts`'s escape hatch) and would
|
|
29
|
+
name the Harness's menu rather than the Agent. And it is named for the thing accessed: the field was first shipped as
|
|
30
|
+
`access`, and `"none"` exposed the dropped word as load-bearing — access _to what?_ The object was always the
|
|
31
|
+
Workspace (it is this ADR's title). A string field, JSON-serializable, riding the existing ConfigMap channel unchanged
|
|
32
|
+
(ADR-0018's constraint holds).
|
|
33
|
+
- **`"none"` is the Menu-only Agent — and the value also names the Harness that hosts the Agent.** `"none"` withholds
|
|
34
|
+
the entire Working toolset: the Agent converses and picks from its Menu, nothing else — the decisioning persona (read
|
|
35
|
+
inputs, pick the next event). Because such an Agent touches no Workspace, it does not run in one: `"write"`/`"read"`
|
|
36
|
+
place a Turn on the enclosing Workspace's Harness; `"none"` places it on the
|
|
37
|
+
[Instance Harness](0031-menu-only-agents-run-on-the-instance-harness.md), always — even when a `workspace()` encloses
|
|
38
|
+
the invocation. Placement is definitional, which is this ADR's thesis extended one step: what an Agent may do to the
|
|
39
|
+
Workspace decides where it runs. (`cwd` is moot for `"none"` — only Working tools consume it.)
|
|
40
|
+
- **The tool layer enforces the honest path.** `access: "read"` withholds `write` and `edit` from the assembled working
|
|
41
|
+
tools (the tool set that the Harness gives the loop for each Submission). `bash` stays — the reviewer's own
|
|
42
|
+
instructions require running the tests — and the ADR says plainly what that means: a shell can write, so the tool
|
|
43
|
+
layer states intent and stops the honest path. It is known leaky, and it is not the guarantee.
|
|
44
|
+
- **The worktree layer contains the blast radius: a reviewer works in a detached review worktree.** The attach step —
|
|
45
|
+
already jr2-owned (`sandbox-kubectl.ts`'s `attachScript`) — adds a worktree at the sha under review with a detached
|
|
46
|
+
HEAD, sibling to the branch worktree per the Project layout. A write there cannot move the branch or dirty the coder's
|
|
47
|
+
worktree; a rogue commit lands on a detached HEAD and evaporates with the checkout. This is not a permission boundary
|
|
48
|
+
— it is containment, the property ADR-0024 bought for turns, bought for the filesystem.
|
|
49
|
+
- **Two layers, one structure: the tool list is the hint, the worktree is the guarantee** — the same shape as ADR-0024's
|
|
50
|
+
receipt (hint) and abort (guarantee). Neither layer alone: tools without the worktree leak through bash; the worktree
|
|
51
|
+
without tools invites the model to fight its checkout instead of declining politely.
|
|
52
|
+
- **The reviewing state requests the review worktree per round.** The sha under review moves between rounds, so the
|
|
53
|
+
detached worktree is attached (idempotently, by sha) on entry to the reviewing state, and its path frames the
|
|
54
|
+
reviewer's turn — cwd and prompt — instead of the branch worktree. The exact Workspace-port verb is settled at
|
|
55
|
+
implementation; what is decided here is the seat: worktree mechanics stay in the attach step and the Workspace
|
|
56
|
+
wrapper, never in Agent instructions.
|
|
57
|
+
|
|
58
|
+
## Considered options
|
|
59
|
+
|
|
60
|
+
- **Prose alone** (today). Rejected by the incident; the coin flip is already on record.
|
|
61
|
+
- **A `SessionEnv` wrapper that throws on denied operations.** The right stopgap against the retired runtime's fixed
|
|
62
|
+
toolset — ~20 lines, no drift — and moot the day jr2 owns tool assembly. Building and testing a mechanism the
|
|
63
|
+
replacement deletes was the argument for folding this decision into ADR-0027.
|
|
64
|
+
- **Wait for flue 2.0's tool exports.** Rejected with ADR-0027.
|
|
65
|
+
- **A read-only filesystem for the reviewer.** Rejected: the reviewer must run tests and builds, which write caches,
|
|
66
|
+
`node_modules`, temp files. Read-only breaks the persona's own job description.
|
|
67
|
+
- **A separate reviewer Sandbox.** Rejected: it pays a pod per review round and breaks ADR-0012's premise — coder and
|
|
68
|
+
reviewer share one Workspace so the review sees exactly the bytes the coder produced, on the shared clone's economics.
|
|
69
|
+
- **Naming the field `tools: ["read", "grep", ...]`.** Rejected: it names the Harness's menu, breaks when a tool is
|
|
70
|
+
renamed, and says nothing about the Agent. `workspace` describes the persona's relationship to the thing it works in;
|
|
71
|
+
the tool list is derived mechanism.
|
|
72
|
+
- **Keeping the name `access`.** Rejected when `"none"` arrived: without the object in the name, `access: "none"` reads
|
|
73
|
+
as "no access to anything," which is false — the Agent has its full Menu. `workspace: "none"` states exactly what is
|
|
74
|
+
absent, and doubles as the placement statement above.
|
|
75
|
+
|
|
76
|
+
## Consequences
|
|
77
|
+
|
|
78
|
+
- **The reviewer definition becomes `workspace: "read"`**, and the prose ban stays in its instructions as intent — the
|
|
79
|
+
half of the contract a model reads — no longer as the mechanism. A decisioner becomes `workspace: "none"`.
|
|
80
|
+
- **The definition stays plain data.** No code enters the ConfigMap channel; composition by re-export/spread (ADR-0018)
|
|
81
|
+
is preserved. Custom tool _implementations_ remain out of the contract — this decision adds a restriction vocabulary,
|
|
82
|
+
not an extension one. Growing extension tools is a separate, later decision.
|
|
83
|
+
- **GLOSSARY.md arbitrates the three senses of "tools"**: the Menu (control plane — what an Agent may say) and Working
|
|
84
|
+
tools (data plane — what it may do) become glossary entries; unqualified "tools" joins the Avoid lists.
|
|
85
|
+
- **The `@kind` tier gains the containment scenario**: from the review worktree, a write probe followed by a commit must
|
|
86
|
+
leave the branch ref and the coder's worktree untouched.
|
|
87
|
+
- **The zero-support guard stays available to workflows**: a workflow can still have both Agents report the sha and gate
|
|
88
|
+
on `event.head === context.head`. Self-reported and unenforced — but it catches the exact incident above, and it needs
|
|
89
|
+
nothing from this ADR.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# A Menu is fixed for a conversation, and a Turn is told its Allowed picks
|
|
2
|
+
|
|
3
|
+
[ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md) derives what an Agent may pick from the invoking state's
|
|
4
|
+
transitions. Its config walk reads transition **keys** and nothing else, so a guard is invisible to it. Two failures
|
|
5
|
+
follow from where that answer is put.
|
|
6
|
+
|
|
7
|
+
**A pick that moves nothing reads as progress.** An event whose every transition is guarded false validates, reaches the
|
|
8
|
+
invoking state, and moves nothing. The receipt said _"the workflow is still in the state that asked for this turn"_ —
|
|
9
|
+
the same sentence a successful in-state move produces.
|
|
10
|
+
[ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) exists because a receipt that says nothing
|
|
11
|
+
actionable gets answered by calling again.
|
|
12
|
+
|
|
13
|
+
**A tools block that changes bills the whole conversation again.** The first fix for the failure above narrowed the
|
|
14
|
+
tools block to what the guards would accept, per Submission. But provider prompt caches are prefix caches in the order
|
|
15
|
+
tools → system → messages: Anthropic invalidates every level when a tool definition changes, and vLLM and
|
|
16
|
+
OpenAI-compatible chat templates render the tools into the head of the prompt. Measured on the home-lab vLLM Instance
|
|
17
|
+
(2026-09-28, a 24.8k-token history): the next Turn with the same tools hit 24288 cached tokens; with one pick added, 0;
|
|
18
|
+
with the same picks reordered, 0. A continued conversation (`continue: true`, ADR-0057) changes state by design — a
|
|
19
|
+
coder in `implement` may `finish`, the same coder in `fix` may `finish` or `dispute` — so every such step prefilled the
|
|
20
|
+
whole history again. It also left the history calling tools the request no longer defined.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
- **The Menu is the whole of what an Agent can ever be offered in its Machine, and it is the tools block.** It is the
|
|
25
|
+
union of the derived sets of every state that invokes an Agent of that name — the same scope as its continued
|
|
26
|
+
conversation's id (ADR-0057). `jr2Setup.createMachine` derives it in ADR-0015's walk, in a fixed order, so it is
|
|
27
|
+
byte-stable for every Turn of the conversation and for every fresh conversation of that Agent. The descriptions are
|
|
28
|
+
the event defs' own and do not vary by state, so a def's description must hold at every writing of it the Agent can
|
|
29
|
+
reach; the load warns where its writings on one path differ (ADR-0015).
|
|
30
|
+
- **What one Turn may pick is its Allowed picks**: the invoking state's derived set (its own, its ancestors' and its
|
|
31
|
+
modes' picks — ADR-0015), narrowed to what the live Machine would currently accept **from this Agent**: xstate's
|
|
32
|
+
selection for an event that carries the Agent's sender, a guard that reads the payload counted as passing, must move
|
|
33
|
+
something, and every writing it selects must be the Agent's (ADR-0015). Read off the live snapshot, they follow the
|
|
34
|
+
active mode as well as the guards; the Menu stays byte-identical in every mode. They are EMPTY while the Agent's state
|
|
35
|
+
is not at Rest — something it started runs below it, or a timer holds — because every pick is refused then
|
|
36
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)); an outstanding pick's answer carries them
|
|
37
|
+
read after the route. The surface serves both (`GET /agents/:iid/surface` → `menu` and `allowed`); the Harness
|
|
38
|
+
presents `menu` as the tools block and states `allowed` in the Briefing's Turn part, after the Frame's prompt
|
|
39
|
+
([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)). It rides the newest message, so it never
|
|
40
|
+
touches the cached prefix. The Briefing's standing part says how to read every such list: a pick is a call that the
|
|
41
|
+
newest "Allowed now" lists, and each pick's answer says if the Turn is over.
|
|
42
|
+
- **The Orchestrator enforces, at delivery, and only there.** A pick while the Agent's state is not at Rest is refused
|
|
43
|
+
before anything else, `finish` included (ADR-0024). A pick off the Menu is refused. A pick on the Menu is not refused
|
|
44
|
+
before delivery: one inside the invoking state's derived set that the Machine does not take — a pick of another mode
|
|
45
|
+
included — is delivered and answered `moved: false`; one that only another state of this Agent handles is answered
|
|
46
|
+
`moved: false` whatever its arguments, and never reaches the Machine, because only a state the Agent does not own (a
|
|
47
|
+
nested Agent's, a Gate's) could act on it there (ADR-0015). A pick whose selection, with its real payload, holds
|
|
48
|
+
another party's writing — a mode and a sibling Gate's or nested Agent's state that write the same name, say — is
|
|
49
|
+
answered `moved: false` the same way and does not reach the Machine (ADR-0015). Every answer carries `allowed`, read
|
|
50
|
+
**after** the delivery, and the Harness renders it ("Allowed now: `finish`"). A `moved: false` pick outside `allowed`
|
|
51
|
+
gets the **correction receipt** — "`<pick>` is not allowed in the workflow's current state, whatever its arguments.
|
|
52
|
+
Your turn is not over. Do not repeat this call — pick one of the allowed tools." — because the state refused it and
|
|
53
|
+
new arguments cannot help; one inside `allowed` was refused by a guard that read its payload, and its receipt says to
|
|
54
|
+
change the arguments. The Harness does not pre-check: its list is as old as what it last read, and an in-state move
|
|
55
|
+
(`moved: true, turnComplete: false`) can change the guards and the mode mid-Turn.
|
|
56
|
+
- **The Turn part is restated at a step boundary when the Allowed picks changed under the Agent.** A Gate's answer, an
|
|
57
|
+
outside event or a child's settled invoke the Agent did not pick can move the Machine while the Agent works with its
|
|
58
|
+
Working tools and picks nothing. So the Harness keeps the last Allowed-picks list the model read — the Turn part, then
|
|
59
|
+
every receipt and refusal — and after each model answer's tool results reads the surface once and compares the two as
|
|
60
|
+
sets. If they differ, it appends one message before the next model request: "The workflow's state changed since you
|
|
61
|
+
last read the Allowed picks. Allowed now: …", in a `<jr2-turn>` block. The Agent's own pick is never restated, because
|
|
62
|
+
its receipt already told the model. The read is one bounded attempt and a failure is silence; the next boundary asks
|
|
63
|
+
again. Nothing is read or said after the Turn ended or after an abort. The Harness knows the Turn ended from a pick's
|
|
64
|
+
answer (`turnComplete`, or a 404), not only from a read: under `continue: true` the next state re-registers the same
|
|
65
|
+
address before the abort lands, so a read there would answer for the next Turn. An empty list is not restated to a
|
|
66
|
+
model that stopped; the Orchestrator's wake asks it when a pick opens (ADR-0016's ask-again rule). The comparison is
|
|
67
|
+
engine-neutral (`between-steps.ts`); only the placement is the engine's — pi's between-steps input (`steer`). An
|
|
68
|
+
engine with no such input falls back to the next receipt. No route is added and no cap is set; the Orchestrator
|
|
69
|
+
already serves the truth on `GET /agents/:iid/surface`.
|
|
70
|
+
- **Presentation stays in the Harness's neutral layer.** `menu.ts` renders the Allowed-picks text and every receipt; a
|
|
71
|
+
presenter (`menu-tools.ts` for pi) only maps Menu items to its tool API and places the text it is handed. A second
|
|
72
|
+
harness — one that takes MCP tools fixed per session and the prompt as text — needs a presenter, not a second Menu.
|
|
73
|
+
|
|
74
|
+
**The guard question is asked twice, because the two askings can see different things.**
|
|
75
|
+
|
|
76
|
+
- The Allowed picks: payload-blind, asked as the Agent's sender (`takesNow`, ADR-0015) — or `mayMove(invoker, name)`
|
|
77
|
+
where no seat can be read (a plain `setup()` actor tree). The pick has not happened, so there are no arguments to
|
|
78
|
+
judge.
|
|
79
|
+
- Delivery: `wouldMove(invoker, event)` and xstate's own selection on the real event. The validated payload and the
|
|
80
|
+
sender are in hand, so the answer is exact.
|
|
81
|
+
|
|
82
|
+
**A guard that reads the pick is never hidden.** A guard like `({ event }) => event.verdict === "approved"` answers
|
|
83
|
+
`false` on a payload it was not given — by plain comparison against `undefined`, not by throwing. So the payload-blind
|
|
84
|
+
reading passes the event behind a Proxy that records any read outside `type` and `sender`, per guard, and trusts a
|
|
85
|
+
`false` only when the guard never looked. A guard that looked is allowed, and settled at delivery. This rests on xstate
|
|
86
|
+
handing a guard the object passed to `can()`, unspread — pinned by a test asserting a payload-reading guard stays
|
|
87
|
+
allowed. A future xstate that copies the event first would degrade this to allowing more, never less.
|
|
88
|
+
|
|
89
|
+
**Both readings fail open.** No invoker, or a guard that throws, reads as allowed. A wrong `true` costs one refused
|
|
90
|
+
pick; a wrong `false` tells an Agent its work is not wanted when the workflow would have taken it, and nothing recovers
|
|
91
|
+
from that.
|
|
92
|
+
|
|
93
|
+
**Absent `moved` is not `false`, and absent `allowed` is not empty.** The Harness ships as a stock image and the
|
|
94
|
+
Orchestrator (ADR-0027), so the two can skew. A surface without `menu` is read as today's narrowed list, presented as
|
|
95
|
+
tools.
|
|
96
|
+
|
|
97
|
+
**A Gate's accepted set stays whole; `acceptsNow` sits beside it.** `GET /runs/:id` lists each Gate's `accepts` — every
|
|
98
|
+
name it may ever send, with its schema, which drives a form — and `acceptsNow`, the same payload-blind question the
|
|
99
|
+
Allowed picks ask, asked as the Gate's sender (ADR-0011). Neither is filtered to empty into a fault: a Gate whose
|
|
100
|
+
`acceptsNow` is empty waits. A send the run would not take is refused 409 with `acceptsNow` (ADR-0013); a human reads
|
|
101
|
+
the Gate's `meta` and the refusal and can tell why, which is precisely what an Agent needs the Allowed picks for.
|
|
102
|
+
|
|
103
|
+
## Why the text comes after the prompt, and why refusal is not optional
|
|
104
|
+
|
|
105
|
+
Measured on the home-lab vLLM model (Qwen3.6-35B-A3B, 10 trials per cell, a prompt arguing for a pick the Turn did not
|
|
106
|
+
allow): with the whole Menu as tools and no statement, 0/10 first picks were allowed; with the Allowed picks stated
|
|
107
|
+
**ahead** of the prompt, 0/10 (thinking off) and 5/10 (on), whatever the wording; stated **after** it, 10/10 both. On a
|
|
108
|
+
weaker pull the statement alone sufficed. After one refusal naming the Allowed picks, 57 of 58 wrong picks recovered on
|
|
109
|
+
the next call. So the statement carries the everyday case, and the refusal is the net under it. Narrowing the tools
|
|
110
|
+
block was not perfect either: once the model invented a tool it had never been given.
|
|
111
|
+
|
|
112
|
+
## Considered options
|
|
113
|
+
|
|
114
|
+
- **Narrow the tools block per Turn** (this ADR's first form). Rejected: it breaks the prefix cache on every Menu
|
|
115
|
+
change, and the history then calls tools the request no longer defines — which models are observed to call anyway.
|
|
116
|
+
- **Fix the tools per invoking state, drop guard narrowing.** Rejected: fixes the within-state case, not ADR-0057's
|
|
117
|
+
cross-state `continue`.
|
|
118
|
+
- **The Machine's whole agent-audience vocabulary as the Menu.** Rejected: it offers picks only another Agent's states
|
|
119
|
+
handle.
|
|
120
|
+
- **Put the Menu tools last.** Rejected: measured, a reordered tools block misses as completely as a changed one.
|
|
121
|
+
- **Mask at decode time** (OpenAI `allowed_tools`, logit masking). Deferred: it is provider-specific — Anthropic has no
|
|
122
|
+
subset form, and changing `tool_choice` there drops the messages cache; vLLM's Chat Completions refuses it. The design
|
|
123
|
+
must hold without it; where it is cache-safe, it can be added in the presenter.
|
|
124
|
+
- **Refuse the delivery outright when `moved` is false.** Rejected for a guard that says no: `deliver` is the single
|
|
125
|
+
validation path behind both dialects, and the receipt already carries the distinction. A pick whose selection holds
|
|
126
|
+
another party's writing is a different class — delivering it would run that party's writing — so it is answered like a
|
|
127
|
+
pick off the invoking state's derived set, and never delivered.
|
|
128
|
+
- **Filter mid-Turn and push `list_changed`.** Rejected: nothing pushes to the Harness (ADR-0013), and a list that moves
|
|
129
|
+
under an Agent invites the retry loop from the other direction.
|
|
130
|
+
- **The Orchestrator pushes the restated Turn part over a new route.** Rejected: pi appends only at a step boundary, so
|
|
131
|
+
a push arrives no earlier than the Harness's read, and it needs a route that writes text into a conversation, change
|
|
132
|
+
detection over every run, and a copy of what the model read that an Orchestrator restart loses.
|
|
133
|
+
- **Parse a JSON-text argument where the pick's schema declares an object** (pi's `prepareArguments`, before the engine
|
|
134
|
+
validates). Rejected, measured: on the home-lab instrument writing a Chart
|
|
135
|
+
([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)) about one call in three carried the object as a string, and
|
|
136
|
+
0 of 89 such strings were JSON — each had its braces miscounted around a `{{...}}` reference — so the parse would have
|
|
137
|
+
fired on none. A leniency that no run can show working is not kept on a hypothesis; a model that stringifies cleanly
|
|
138
|
+
will show it in its own trials, and the ten lines come back with that number.
|
|
139
|
+
- **The Allowed picks on every Working-tool result.** Rejected: a statement repeated where nothing changed is one the
|
|
140
|
+
model learns to skip.
|
|
141
|
+
|
|
142
|
+
## Consequences
|
|
143
|
+
|
|
144
|
+
- **One state can allow different picks as its context changes or its active mode changes; the Menu does not change.**
|
|
145
|
+
The conformance tier asserts it: two Submissions with different Allowed picks send a byte-identical tools block
|
|
146
|
+
(ADR-0027).
|
|
147
|
+
- **A restatement is appended, never rewritten.** Conformance asserts that each request of a Submission is a byte prefix
|
|
148
|
+
of the next, with the tools and the system prompt unchanged, and that the restatement sits right after the model
|
|
149
|
+
answer's last tool result.
|
|
150
|
+
- **A workflow expresses "not available yet" with a guard alone,** and the Agent is told so rather than finding a tool
|
|
151
|
+
that silently does nothing.
|
|
152
|
+
- **`on: { X: {} }` — targetless and actionless — is never allowed,** matching xstate's `can()`.
|
|
153
|
+
- **The receipt declarations stay hand-synchronized** across `run-host.ts` and the Harness's `menu.ts`, now carrying
|
|
154
|
+
`allowed` too — the cost of the packages not importing each other.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# A snapshot names the Machine it was written under
|
|
2
|
+
|
|
3
|
+
[ADR-0007](0007-durable-machine-state.md) persists a run's snapshot so a restarted Orchestrator resumes it.
|
|
4
|
+
[ADR-0008](0008-orchestrator-as-deployed-app.md)/[ADR-0019](0019-one-converging-command-against-the-current-context.md)
|
|
5
|
+
publish the Instance's Assembly to an Orchestrator whose volume outlives every publish
|
|
6
|
+
([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). And a persisted run is
|
|
7
|
+
matched to its workflow by **filename stem**: by name inside the Assembly its snapshot names, and, for a run that no
|
|
8
|
+
Assembly started (an in-process test host), by name in the registry.
|
|
9
|
+
|
|
10
|
+
Those three facts compose into a hole. Park a run at a Gate, edit `workflows/task.ts`, `jr2 up`. The name still
|
|
11
|
+
resolves. The def found under it is a different Machine. Nothing checks, and — this is the part that decides the shape
|
|
12
|
+
of the fix — **xstate does not validate a restored state value.** A snapshot whose `value` names a state the new chart
|
|
13
|
+
does not have does not throw: `createActor` accepts it and the run starts with `value: undefined`, carrying on from
|
|
14
|
+
nowhere. There is no downstream check to fall back on. Either restore refuses, or nothing does.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
**Every save stamps the shape of the Machine that wrote it; every restore compares before interpreting.** `RunBlob`
|
|
19
|
+
gains `machine`, a 12-hex digest — the same width `jr2 up`'s content hash uses, and for the same reason: it is read by
|
|
20
|
+
people in error messages more often than by code. A mismatch is refused before `reconcile`, because proving a Sandbox is
|
|
21
|
+
alive for a run that cannot be read is wasted work.
|
|
22
|
+
|
|
23
|
+
**The digest covers restorability, not behavior.** It is built from `machine-doc.ts`'s traversal — already deterministic
|
|
24
|
+
(states sorted by `order`), JSON-pure, provider-independent, and descending into invoked and spawned child machines —
|
|
25
|
+
projected down to the facts a snapshot has to agree with:
|
|
26
|
+
|
|
27
|
+
- state ids, keys, types, nesting, and which child is `initial`;
|
|
28
|
+
- each state's invokes by `id` **and** `src` (the id is the key in the persisted `children` map, so re-attach depends on
|
|
29
|
+
it — the most easily missed of the three, and a reason a `src`-only hash would not do);
|
|
30
|
+
- every transition as `source|event|targets`, **sorted**, so reordering `on:` keys is not drift.
|
|
31
|
+
|
|
32
|
+
Deliberately excluded: guard bodies, actions and assigns, prompts, descriptions. Editing a guard changes what a run does
|
|
33
|
+
_next_; it does not make the snapshot unreadable. The alternative — any edit to a workflow strands its parked runs — is
|
|
34
|
+
defensible in the abstract and unusable in practice: it makes `jr2 dev`'s reload a migration event and prompt-tuning a
|
|
35
|
+
stop-the-world operation. **Drift means "I can no longer read what I saved," not "something changed."**
|
|
36
|
+
|
|
37
|
+
**Absent means drift.** A blob written before the stamp existed cannot be vouched for, and the point is to never
|
|
38
|
+
interpret one that cannot be. Greenfield, so a stale local `.jr2/state.db` is deleted, not migrated.
|
|
39
|
+
|
|
40
|
+
**A drifted run is refused, kept, and readable.** Not `markLost`: that nulls the snapshot, and `read()` returns
|
|
41
|
+
undefined for a null blob, so `jr2 status <id>` would answer `no run "<id>"` — a refusal indistinguishable from a run
|
|
42
|
+
that never existed, on precisely the run a human most needs to look at. `drifted` is its own status, the snapshot is
|
|
43
|
+
untouched, and `RunStatus` gains `reason`, which nothing on any HTTP route surfaced before. The refusal names the
|
|
44
|
+
workflow and both digests.
|
|
45
|
+
|
|
46
|
+
**A throw during restore is a different claim from drift, and is not recorded as one.** Drift is durable — the Machine
|
|
47
|
+
will still have changed on the next boot. A `reconcile` failure is a kubectl blip, and condemning a run for it would
|
|
48
|
+
make a transient fault permanent (or, via `markLost`, throw the snapshot away). Those rows are left `live` for the next
|
|
49
|
+
boot to retry, reported through `onRestoreError`, and counted separately as `failed`.
|
|
50
|
+
|
|
51
|
+
**One unresumable run is one run.** The restore loop is sequential, and a throw used to reject `restore()`, which
|
|
52
|
+
rejects `startInstance`, which crash-loops the pod — taking every _later_ run in the store down with it, unrestored.
|
|
53
|
+
Each run is now handled independently.
|
|
54
|
+
|
|
55
|
+
**The boot announces what it did not resume.** `restore()` returns `{ reattached, lost, drifted, failed }`, and the
|
|
56
|
+
entrypoint puts the last three on its announce line. A boot with no current Assembly lists its runs as `waiting`
|
|
57
|
+
instead, and the first publish, which restores them, announces one line `{ assembly, lost?, drifted?, failed? }` when
|
|
58
|
+
any was not resumed ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)).
|
|
59
|
+
Resumed runs are routine and stay quiet. Without this, `drifted` is reachable only by asking after a run id nobody knows
|
|
60
|
+
to ask about.
|
|
61
|
+
|
|
62
|
+
## Considered options
|
|
63
|
+
|
|
64
|
+
- **Hash the workflow module source, or the whole config including guards and actions.** Rejected — see above. Maximal
|
|
65
|
+
safety at the cost of making every workflow edit a migration.
|
|
66
|
+
- **Reuse `JR2_CONTENT_HASH`**, which `jr2 up` already computes and injects. Rejected: it is a digest of the whole
|
|
67
|
+
staged bundle, so it moves on a lockfile bump, a README edit, or a kit upgrade — constant false drift — and it is
|
|
68
|
+
absent under `jr2 dev` entirely.
|
|
69
|
+
- **Topology plus named guards** (`setup()` keys and named functions). Rejected: inline arrows collapse to `"inline"` in
|
|
70
|
+
the doc and would stay invisible, so the coverage is uneven in a way no one could predict from reading a workflow.
|
|
71
|
+
- **A `machine_snapshots` schema column.** Rejected: `init()` is a single `CREATE TABLE IF NOT EXISTS` with no migration
|
|
72
|
+
mechanism, so a new column would silently not appear on an existing PVC. The blob is versioned by being JSON.
|
|
73
|
+
- **Warn and resume anyway.** Rejected: there is no safe interpretation to fall back on. xstate resumes a missing state
|
|
74
|
+
as `undefined` rather than failing, so a warning would be the only signal, on a run already running wrong.
|
|
75
|
+
- **Fold the workflow name into the digest.** Rejected: restore resolves the def by name first, inside the run's own
|
|
76
|
+
Assembly, so a rename never reaches the comparison — it would only make a rename look like a shape change on a run
|
|
77
|
+
that no longer resolves at all.
|
|
78
|
+
|
|
79
|
+
## Consequences
|
|
80
|
+
|
|
81
|
+
- **Editing a workflow strands nothing.** A run holds the Assembly it started under, through restarts too
|
|
82
|
+
([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)), so a publish never
|
|
83
|
+
changes the Machine a live run resolves to. Drift is reached only when a held Assembly cannot be read any more: it
|
|
84
|
+
does not load under a bumped Kit version, or the volume lost it. The stamp still stands as the last check, because the
|
|
85
|
+
Assembly hash says which tree was loaded and the stamp says the Machine in it still has the shape it had.
|
|
86
|
+
- **There is no migration path.** A drifted run can be read and cancelled; it cannot be adapted onto a new Machine. That
|
|
87
|
+
is deliberate for now — a migration story needs a way to express what a state maps to, which nothing here does.
|
|
88
|
+
- **A run with an Open body keeps its wrapper's stamp.** A Chart
|
|
89
|
+
([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)) is the run's data, not its Machine's shape: the Chart's
|
|
90
|
+
text and its hash ride the body's own snapshot, restore admits the text again and compares the hash, and a mismatch or
|
|
91
|
+
a refusal is drift naming the body slot and the reason, after the stamp check and in the same words.
|
|
92
|
+
- **A drifted run is not in `jr2 runs`**, which lists only the live registry. It is reachable through the announce line
|
|
93
|
+
and `jr2 status <id>`. If stranded runs become common enough to browse, the listing is where that belongs.
|
|
94
|
+
- **The digest is only as complete as the walk.** A state running `enqueueActions` can spawn children no static analysis
|
|
95
|
+
recovers (`machine-doc.ts`'s `opaqueStates`); the opacity is folded into the hash, but what it hides is not. Drift
|
|
96
|
+
inside such a subtree can go unnoticed.
|
|
97
|
+
- **`fingerprintOf` is memoized per machine object** — `persist` runs on every snapshot microtask, and a dev reload's
|
|
98
|
+
fresh machine object correctly gets a fresh entry. That is not `vocabularyOf`'s key, which is `machine.config` so a
|
|
99
|
+
`.provide()` clone keeps the defs it was built with (ADR-0011); a shape has nothing to keep, being derived from the
|
|
100
|
+
config alone, so a clone recomputes the identical digest.
|