@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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Menu-only Agents run on the Instance Harness
|
|
2
|
+
|
|
3
|
+
A workflow should be able to run an Agent whose whole job is a decision — read some inputs, pick the next event — with
|
|
4
|
+
no Workspace, no worktree, and no per-run pod: the statelyai/agent shape, at conversational latency, on any configured
|
|
5
|
+
model. jr2's Menu already _is_ that decision surface (the invoking state derives what the Agent may say —
|
|
6
|
+
[ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)/[ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)),
|
|
7
|
+
and the mechanism was already Sandbox-agnostic: `agentRun` needs only an endpoint URL, a registration may carry no
|
|
8
|
+
Sandbox (`tokens.ts`), and the stub Harness proved a Harness is "only a different URL". What was missing was the
|
|
9
|
+
hosting: the only real Harness anywhere was the one inside a Sandbox pod, so a Turn without a `workspace()` had nowhere
|
|
10
|
+
to run. Adopting statelyai/agent instead was rejected outright — it would be a second, weaker agent mechanism (no
|
|
11
|
+
Submission/Admission ledger, no re-attach, no Settlement, a menu that is not guard-narrowed) beside the one jr2 already
|
|
12
|
+
built.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
- **One Instance Harness per instance, deployed by convention.** `jr2 up` converges a Harness StatefulSet + Service —
|
|
17
|
+
stock `jr2-harness:<kitversion>` image, same wire — whenever any Agent slot carried by a registered Machine
|
|
18
|
+
(ADR-0049's walk) declares `workspace: "none"`
|
|
19
|
+
([ADR-0028](0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md)). The scan is static and
|
|
20
|
+
definition-level, deliberately not workflow-level (workflow internals are not statically recoverable — the same line
|
|
21
|
+
ADR-0018 drew for model preflight); a declared-but-never-invoked `"none"` Agent over-deploys, erring toward "the
|
|
22
|
+
convention works when you need it". No config key names, sizes, addresses, or enables it.
|
|
23
|
+
- **Placement is definition-wins, not nearest-wins.** A `workspace: "none"` Agent's Turn runs on the Instance Harness
|
|
24
|
+
_always_ — even invoked from inside a `workspace()`. It is ADR-0028's thesis extended one step: an Agent that touches
|
|
25
|
+
no Workspace has no reason to run in one, and a Sandbox — torn down at its Workspace's final state, replaced after an
|
|
26
|
+
eviction — is the wrong home for a conversation whose definition says it needs no Workspace. A conversation is an
|
|
27
|
+
Instance ID _on one Harness_ (the server holds the history), so placement must be a function of the definition, not of
|
|
28
|
+
where an invocation happens to sit; nearest-wins would make the Harness that holds a `"none"` conversation an accident
|
|
29
|
+
of authoring. Definition-wins also makes "which pod's logs" a function of the Agent alone.
|
|
30
|
+
- **The cohesive per-run log is solved by projection, not placement.** The cost of definition-wins — decisions vanishing
|
|
31
|
+
from the Workspace pod's log — is repaid by [ADR-0023](0023-the-harness-prints-the-conversation.md)'s echo: the
|
|
32
|
+
enclosing Workspace's Harness prints the run's narrative (admission + pick markers for remotely-hosted Turns, Emits,
|
|
33
|
+
the backfilled preamble). Instance Harness log = every `"none"` conversation, instance-wide, interleaved (the less
|
|
34
|
+
useful top-to-bottom read, accepted); Workspace log = everything that ran there plus the narrative of the run that
|
|
35
|
+
owns it.
|
|
36
|
+
- **The Instance Harness pod is Harness + Custodian — the one Harness shape, minus the Workspace.** No user container,
|
|
37
|
+
no `/work` volume, no attach step. The Custodian stays although `"none"` Agents cannot execute code (the
|
|
38
|
+
[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) rationale technically lapses):
|
|
39
|
+
the Harness has exactly one route to its Menu — the Custodian on `localhost` — and forking that path for one pod buys
|
|
40
|
+
a divergence in the component this ADR keeps deliberately uniform. The pod is composed by the same function a
|
|
41
|
+
Sandbox's is ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)), so its Custodian
|
|
42
|
+
carries the Instance Harness's model calls the same way. It holds only the model keys of the `"none"` Agents it hosts
|
|
43
|
+
— the keys their providers read — and none of the held secrets a `workspace()` lists: a `"none"` Agent cannot execute
|
|
44
|
+
code, so it had no way to use the rest. Its held objects are its own (`held-instance.json`,
|
|
45
|
+
`jr2-held-secrets-instance`). Defense-in-depth is the bonus, not the reason.
|
|
46
|
+
- **The Instance Effector is the second Instance-level pod, beside it**
|
|
47
|
+
([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), on the same convention: `jr2 up`
|
|
48
|
+
converges it when a registered Machine places an Effect there — an `http` Effect — and deletes it otherwise. It runs
|
|
49
|
+
the same `jr2-harness` image with `/opt/jr2/bin/effector` as its command, holds no conversation and runs no Agent, and
|
|
50
|
+
is a Deployment, not a StatefulSet: it keeps nothing, and a rollout must run the new pod beside the old one. No config
|
|
51
|
+
key names, sizes, addresses, or enables it.
|
|
52
|
+
- **No image config at all.** The Harness image was once configured as `sandbox.image` because the Sandbox pod was the
|
|
53
|
+
only place a Harness ran — a misnomer once the Instance Harness exists, and config is the wrong seat regardless:
|
|
54
|
+
`jr2 up` builds and resolves every image it deploys ([ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md)), and
|
|
55
|
+
the per-Workspace images (Sandbox Image, User Container) are static `workspace()` options carried by the Machine
|
|
56
|
+
itself ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md),
|
|
57
|
+
[ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md),
|
|
58
|
+
[ADR-0005](0005-sandbox-pod-composition.md)). `sandbox.image`, the per-component image keys, and `operator.image`
|
|
59
|
+
dissolve; the `sandbox` config section disappears until something genuinely pod-shaped and user-tunable exists.
|
|
60
|
+
|
|
61
|
+
## The Instance Harness holds a conversation as long as its run, and frees the rest
|
|
62
|
+
|
|
63
|
+
The scaling review (R11, 2026-09-30) found the `conversations` map with no delete path: every Menu-only Agent instance
|
|
64
|
+
of every run stayed until the pod restarted, and the restart lost every conversation at once. The research that preceded
|
|
65
|
+
it checked Agent Substrate's own model against the primary sources: an actor is stateful, the platform snapshots its
|
|
66
|
+
memory and its `DurableDir` volumes and resumes it on any worker, and its control plane holds metadata and snapshot
|
|
67
|
+
pointers only; memory snapshots are tied to one template version, so the state with a documented path across a code
|
|
68
|
+
change is a volume the actor reloads on a cold boot. These decisions take that shape.
|
|
69
|
+
|
|
70
|
+
- **A conversation is persisted to a directory, and rebuilt from it on boot.** The Harness writes each conversation as
|
|
71
|
+
it goes: jr2's own record — the stream log, the Settlements, the history messages, in the wire's shapes — plus the
|
|
72
|
+
engine part, in a folder the engine names (`engine/<name>`), which the record does not read. Today it is jr2's
|
|
73
|
+
transcript (ADR-0027): `engine/pi/transcript.jsonl`, the pi-ai messages the model read and each Compaction. Rebuilding
|
|
74
|
+
reads jr2's record first; an engine that cannot read the engine part (another engine, another version) continues from
|
|
75
|
+
the history messages. The record is the contract; the engine part is not. A Submission in flight at the restart
|
|
76
|
+
settles `failed` (the typed error `harness_restarted`, which the Agent's Actor reads as "Turn lost") on the rebuilt
|
|
77
|
+
stream, so the Orchestrator reads a Settlement, not a 404, and the conversation's next Turn continues it.
|
|
78
|
+
- **A restart while the Agent's state is busy cuts the Turn and is no fault.** When the Agent's state is not at Rest —
|
|
79
|
+
its pick is outstanding, or work it started in runs below it — or its pick's answer was read and never reached the
|
|
80
|
+
model, the Agent's Actor raises no `agent.fault` for a restart: the Machine would route it and stop the work the Turn
|
|
81
|
+
did not want stopped (ADR-0024). It waits for Rest, then re-admits the Agent on the same conversation, which the
|
|
82
|
+
rebuilt Harness holds: a jr2 prompt ("Your previous turn was cut: the process that ran you restarted while the
|
|
83
|
+
workflow was busy. The workflow did not stop. Continue your task from here.") with the answer as a `pick-answer`
|
|
84
|
+
notice (`cut: true`) in the Briefing's Turn part, and the Allowed picks after it (ADR-0062). The re-admission asks the
|
|
85
|
+
rebuilt Harness for the conversation first, as a continued Turn does: one it cannot read gets a `conversation-new`
|
|
86
|
+
notice ahead of the answer, and the notices pending for the Turn ride along. In the rebuilt engine part the cut Menu
|
|
87
|
+
call has no result; the Harness answers it in the history with jr2's text — the pick reached the workflow, which does
|
|
88
|
+
not run it twice, and its result comes in the next message — before pi-ai would answer "No result provided", which
|
|
89
|
+
reads as a failed pick. That text prints once in the rebuilt Harness's log, before its first prompt (ADR-0023). The
|
|
90
|
+
engine runs a model answer's calls in order
|
|
91
|
+
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)). So only the first call with no
|
|
92
|
+
result can have been out. Every call after it never started, a Working-tool call too. Each one gets jr2's text "This
|
|
93
|
+
call did not run: your turn ended before it started.", as an error result. A Menu call that an abort cut while the
|
|
94
|
+
model was still writing it (the abort lands mid-stream) did not reach the workflow: the engine runs no call of a
|
|
95
|
+
message the model did not finish. The Harness tells the two cuts apart by whether the call was delivered, read before
|
|
96
|
+
it re-tags an aborted message so that pi-ai replays it: a call in a finished message keeps the text above, and a call
|
|
97
|
+
in an aborted message gets jr2's text in the same form — the Turn was cut while the model was still writing this call,
|
|
98
|
+
it did not reach the workflow, nothing ran, and the model may call it again if the newest "Allowed now" still lists
|
|
99
|
+
it. That text prints once too, before the conversation's next prompt, in the same process or the next. The Harness
|
|
100
|
+
reads whether a call was delivered off its message, not off a record of the POST, and that is the limit of the test: a
|
|
101
|
+
SIGKILL of the Harness after the model finished the message and before the pick's POST reached the Orchestrator leaves
|
|
102
|
+
a call that never reached the workflow, and the rebuilt Harness answers it with the restart's text. Closing that
|
|
103
|
+
window needs a delivery record written before the POST, which is not built. A Menu call in a message that a provider
|
|
104
|
+
error ended mid-stream (stopReason `error`) is neither cut: pi runs no call of such a message (pi-agent-core 1.4.0
|
|
105
|
+
`agent-loop.js` line 148 returns after a message that ended `error` or `aborted`) and drops it from every later
|
|
106
|
+
request (pi-ai 1.4.0 `transform-messages.js` lines 172 to 180), so the Harness does not re-tag it and adds no answer
|
|
107
|
+
for its call: an answer would be a tool result with no call before it, which the providers refuse. The pick did not
|
|
108
|
+
reach the workflow, the Submission settles `failed` as any provider error does, and on the next request the model
|
|
109
|
+
reads no trace of the call and is free to make it again. A poll takes the answer only while its connection lives: the
|
|
110
|
+
killed Harness's last poll, still waiting at the Orchestrator, ends unanswered when its connection closes, so an
|
|
111
|
+
answer that lands after the kill, and before the rebuilt stream reports the restart, is still unread. The same holds
|
|
112
|
+
for a Sandbox's Harness, whose restart the watch sees (ADR-0021). A restart at Rest with nothing unread is the fault
|
|
113
|
+
it always was. Only the Turn's OWN pick counts as unread: a pick of an earlier Turn on the same continued address,
|
|
114
|
+
whose route left the Agent's state, was answered as a notice to this Turn, and a restart at Rest in this Turn is still
|
|
115
|
+
the fault. A route that leaves the Agent's state ends the Turn while it waits, as any route does.
|
|
116
|
+
- **Both placements persist, to different volumes.** The Instance Harness to a PersistentVolumeClaim of its own; a
|
|
117
|
+
Sandbox's Harness to an emptyDir mounted into the Harness container alone (the User Container must not read it), so a
|
|
118
|
+
Harness container that restarts inside a living pod keeps its conversations. A pod that is gone takes the emptyDir
|
|
119
|
+
with it — that Workspace is `Lost` (ADR-0021), and no conversation persistence should say otherwise.
|
|
120
|
+
- **The Instance Harness is a StatefulSet of one, with a per-ordinal volume.** The value is the stable volume; the
|
|
121
|
+
stable name is what N > 1 would pin a conversation to (the admission ledger already records its endpoint). N is a kit
|
|
122
|
+
value, 1 until a measured need; never an HPA, which would kill conversations on a scale-down and land a continuation
|
|
123
|
+
on a replica that never saw it. A rollout at N = 1 has no surge, so admissions are refused while the new pod starts;
|
|
124
|
+
the Orchestrator already absorbs an admission refused at the connection (ADR-0042), so a rollout delays a Turn and
|
|
125
|
+
faults nothing.
|
|
126
|
+
- **A rollout drains Turns.** On SIGTERM the Harness stops admitting, finishes the Turns it holds (the queued ones too),
|
|
127
|
+
then exits; the termination grace is a Turn's worst case. A Turn still generating or running a tool keeps that
|
|
128
|
+
graceful wait. A Turn whose only pending work is an outstanding pick's poll — whatever the pick waits on: a Gate, an
|
|
129
|
+
Effect, a child Machine — is cut at once, or as soon as it comes to that: its answer can wait on a human (a Gate),
|
|
130
|
+
often the one who is stopping the Harness, so no grace period would help. The cut drops the poll unanswered (the
|
|
131
|
+
answer stays unread at the Orchestrator), leaves the Menu call without a result, asks the model nothing more, and
|
|
132
|
+
settles the Submission `failed`, `harness_restarted`, with everything queued behind it. To the Orchestrator it is a
|
|
133
|
+
restart while the Agent's state is busy (the bullet above): the pick stays outstanding, and once the workflow is back
|
|
134
|
+
at Rest jr2 re-admits the Agent on the same conversation with the answer, and the replacement Harness answers the cut
|
|
135
|
+
call with jr2's text. It refuses an admission with a 503 before it reads or queues anything, and closes that
|
|
136
|
+
connection, so the Orchestrator re-sends it on ADR-0042's ladder and the re-send dials the Service anew. Readiness
|
|
137
|
+
stays up (the probe is the Harness's own `/healthz`, which it keeps answering), and the dialed Service is a ClusterIP
|
|
138
|
+
one, which kube-proxy still routes to a terminating pod that serves while no other is ready — so reads keep answering
|
|
139
|
+
and a `wait` on the draining pod still reads its Settlement. A headless name would not: DNS stops naming a terminating
|
|
140
|
+
pod, so the StatefulSet's headless governing Service is a second one, dialed by no one. The Custodian is a native
|
|
141
|
+
sidecar, stopped only after the Harness exits, so the drain keeps its Menu and its model to the end. A deploy loses no
|
|
142
|
+
Turn and, with the volume, no conversation — as long as the drain and the replacement's start fit ADR-0042's window; a
|
|
143
|
+
longer one faults the waiting admission with the Harness's own 503 words.
|
|
144
|
+
- **The Orchestrator states the live set; the Harness frees the rest.** After every restore and every 5 minutes (the
|
|
145
|
+
Lease's cadence) the Orchestrator tells the Instance Harness which conversations its live runs hold — from the
|
|
146
|
+
admission ledgers in memory, never from the store — and the Harness frees memory and files of every other one.
|
|
147
|
+
Level-triggered, idempotent, a missed statement costs nothing, and a crashed Orchestrator's leftovers go at its next
|
|
148
|
+
boot. No idle TTL: a run parked on a Gate for a day keeps its conversation, and nothing else can decide that. A
|
|
149
|
+
Sandbox's Harness is not told; its Workspace's teardown frees everything.
|
|
150
|
+
- **The Orchestrator keeps holding handles only** (ADR-0007): no conversation state crosses to it. Storing sessions
|
|
151
|
+
centrally and re-sending them to a fresh pod was considered and rejected — it is the design Substrate does not have,
|
|
152
|
+
and it puts every Menu-only Turn's history on the single writer's path.
|
|
153
|
+
|
|
154
|
+
## Considered options
|
|
155
|
+
|
|
156
|
+
- **statelyai/agent (or any in-process decision actor).** Rejected above; the long form: every ADR from 0011 to 0029
|
|
157
|
+
would stop being the single story of how models drive Machines, and the copy would be the lesser one — its picks
|
|
158
|
+
bypass the Menu's guard-narrowing, its runs bypass the admission ledger, its failures bypass Settlement.
|
|
159
|
+
- **Unconditional ambient fallback** (no enclosing `workspace()` → Instance Harness, for every Agent). Rejected: a coder
|
|
160
|
+
invoked outside its `workspace()` by mistake would run bash in the shared pod instead of failing loudly — precisely
|
|
161
|
+
the mistake today's error catches. The fallback is gated on `workspace: "none"`; everything else keeps the loud error,
|
|
162
|
+
now sharper ("agent `x` has `workspace: "write"` — invoke it inside a `workspace()`").
|
|
163
|
+
- **Nearest-wins placement** (a `"none"` Agent invoked inside a `workspace()` runs on that Workspace's Harness).
|
|
164
|
+
Genuinely attractive — the Workspace pod log stays the one coherent story with zero new mechanism — and rejected on
|
|
165
|
+
the continuation amnesia above plus three lesser costs: decisioner turns die with workspace churn, share limits with
|
|
166
|
+
builds, and scatter one Agent's history across pods. The log story it bought is recovered by ADR-0023's echo.
|
|
167
|
+
- **On-demand pod per workspace-less run.** Rejected: pays pod-start latency on the path whose whole point is a fast
|
|
168
|
+
pick.
|
|
169
|
+
- **Hosting the Turn in the Orchestrator process.** Rejected: no pod logs, and it crosses the line the architecture
|
|
170
|
+
draws hardest — the Orchestrator holds handles; models act remotely.
|
|
171
|
+
- **A host-local dev Harness.** Considered as a dev story and dropped when the premise died:
|
|
172
|
+
[ADR-0019](0019-one-converging-command-against-the-current-context.md) removed `jr2 dev` — there is no host dev mode
|
|
173
|
+
to serve. Local development of a decisioning workflow is `jr2 up` against kind, where the Instance Harness converges
|
|
174
|
+
like everything else; an instructions tweak rides the next admission, no image build. The stub Harness keeps its one
|
|
175
|
+
job: a test fixture reached by explicit `endpoint`.
|
|
176
|
+
|
|
177
|
+
## Consequences
|
|
178
|
+
|
|
179
|
+
- **The Agent's resolution grows one arm**: explicit `endpoint` (tests) → `workspace: "none"` → Instance Harness
|
|
180
|
+
(deterministic Service DNS) → enclosing `workspace()`'s ambient handles → loud error naming the definition's
|
|
181
|
+
`workspace` value.
|
|
182
|
+
- **A `"none"` conversation survives the Instance Harness pod's restart**: the StatefulSet restores the endpoint and the
|
|
183
|
+
volume restores the history. A Turn in flight at the restart settles `failed`; nothing is continued silently — a Turn
|
|
184
|
+
whose state is busy is re-admitted with a prompt that says it was cut.
|
|
185
|
+
- **An instance with no `"none"` definitions deploys nothing new.** The feature is invisible until the first
|
|
186
|
+
`agent({ ..., workspace: "none" })` a Machine carries, which is its entire user-facing surface.
|
|
187
|
+
- **The harness conformance suite (ADR-0027) gains the Menu-only shape**: a Turn whose definition withholds every
|
|
188
|
+
Working tool, settled by pick alone — no infra, same scripted provider.
|
|
189
|
+
- **GLOSSARY.md**: Harness's "inside a Sandbox" loosens to name the second placement; **Instance Harness** becomes a
|
|
190
|
+
glossary term.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# The Console unlocks with the Instance token, and the bands do not move
|
|
2
|
+
|
|
3
|
+
[ADR-0014](0014-observation-is-open-run-state-is-not.md) built the three-band HTTP surface on one premise: the
|
|
4
|
+
visualizer page is a browser, and a browser holds no token — so it got a projection instead of a credential, and "give
|
|
5
|
+
the page a token" was explicitly rejected. That page is now growing into the **Console**: it starts runs and answers
|
|
6
|
+
Gates, both of which are writes on the guarded surface. This ADR records how a browser earns those writes without moving
|
|
7
|
+
a single band — and it can stay this small because of a fact ADR-0014 did not lean on: the Instance token is already one
|
|
8
|
+
`kubectl get secret` away for exactly the audience the Console serves. There is no new distribution problem, only a new
|
|
9
|
+
presentation surface.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
- **The human presents the token; the page never ships with one.** The Console's nav carries a password field. Empty,
|
|
14
|
+
the page is precisely ADR-0014's observer: open structure and observation, no control rendered. Filled, the page sends
|
|
15
|
+
`Authorization: Bearer` on the guarded surface — which unlocks both directions at once: the writes
|
|
16
|
+
(`POST /workflows/:name/runs`, `POST /runs/:id/gates/:gate/events`) and the `instanceOnly` reads the control UI needs
|
|
17
|
+
anyway (`GET /runs`, and `GET /runs/:id` — where `GateView` lives). The page as _served_ stays open and inert;
|
|
18
|
+
ADR-0014's rejection stands, because what it rejected was baking the credential into the page.
|
|
19
|
+
- **The token lives in `sessionStorage`** — survives a reload, dies with the tab, never a cookie (no ambient credential,
|
|
20
|
+
no CSRF surface). It is validated on entry with a cheap guarded read so the field shows live/invalid immediately; any
|
|
21
|
+
later 401 drops the UI back to observer mode rather than failing per-widget.
|
|
22
|
+
- **Gate discovery is a frame-triggered guarded read, not an open-band extension.** A Gate exists exactly while its
|
|
23
|
+
invoking state is entered (ADR-0011), and every entry/exit lands a status frame on the open workflow feed (ADR-0022) —
|
|
24
|
+
so on each frame, a token-holding Console re-fetches `GET /runs/:id` for the affected run and folds `gates` into its
|
|
25
|
+
store. Perfectly synchronized, no polling loop, and the observation projection is untouched. A delivery's success is
|
|
26
|
+
likewise confirmed by the next frame emptying the card, never by local bookkeeping.
|
|
27
|
+
- **Page addresses content-negotiate; JSON is the default dialect.** `/` and `/workflows/:name` serve the Console shell
|
|
28
|
+
to `Accept: text/html` and JSON to everything else — so the workflow-detail resource keeps its natural address (the
|
|
29
|
+
run-input schema's home, [ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)) and no future page has to
|
|
30
|
+
squat on a mangled sibling path. Nothing existing moves: EventSource asks `text/event-stream`, and the Harness's Menu
|
|
31
|
+
and the CLI never ask for HTML. `/viz/*` is deleted (greenfield, no redirects); assets move to `/assets/*`, which also
|
|
32
|
+
retires the "a workflow named `assets` is shadowed" hazard.
|
|
33
|
+
|
|
34
|
+
## Considered options
|
|
35
|
+
|
|
36
|
+
- **`jr2 ui` — the CLI opens the browser through an authenticated local proxy.** A convenience layer worth building _on
|
|
37
|
+
top of_ the token field later, not a substitute: it has no answer for the deployed jr2 Application behind an ingress,
|
|
38
|
+
where the browser talks to the Service directly and the CLI is not in the path.
|
|
39
|
+
- **A fourth, scoped credential** ("may answer gates, may not cancel or read context"). Still rejected, as in ADR-0014 —
|
|
40
|
+
but the calculus has narrowed: it becomes worth writing only when gate-answering is delegated to people who must _not_
|
|
41
|
+
hold cluster RBAC. That is a real future (an approval inbox for non-operators); it is not this one.
|
|
42
|
+
- **Announce gates on the open observation feed** (even just `{runId, gate}`). Rejected: it widens the projection for a
|
|
43
|
+
convenience the token already solves, and the open band has an unresolved exposure question as it stands (spawn ids —
|
|
44
|
+
the ADR-0014 open call). The projection stays the one place the line is drawn.
|
|
45
|
+
- **A `/ui` path prefix, or moving the API under `/api`.** Rejected for negotiation: the first is ceremony on the
|
|
46
|
+
address a human types, the second churns the Harness's Menu, the Custodian's route allowlist, the CLI, and the
|
|
47
|
+
ADR-0009/0013 route tables for cosmetics.
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
- **Zero new server surface except negotiation.** No new token, no new band, no new route class; `auth.test.ts`'s
|
|
52
|
+
boundary assertions are untouched. The server-side diff is: negotiate two addresses, serve the shell, retire `/viz/*`.
|
|
53
|
+
- **Possession remains cluster RBAC.** Whoever can read the instance Secret could already do everything the Console
|
|
54
|
+
unlocks; the field changes where the token is typed, not who holds it.
|
|
55
|
+
- **The XSS caveat is the real one and is bounded by what the page is:** a token in `sessionStorage` is readable by
|
|
56
|
+
scripts on the page, so the Console must keep serving only its own assets — no CDN, no third-party script, exactly the
|
|
57
|
+
constraint the viz page already lives under.
|
|
58
|
+
- **GLOSSARY.md**: **Console** becomes a glossary term (visualizer, dashboard, UI, viz on its Avoid list).
|
|
59
|
+
- **The Console's UX gets a browser tier** — `@console`-tagged Cucumber scenarios holding a Playwright page, in
|
|
60
|
+
`@jr2/e2e`, excluded from the default profile ([ADR-0010](0010-bdd-acceptance-tests.md) as amended). What the page
|
|
61
|
+
_believes_ stays unit-tested in `console-store.test.ts`; the browser tier asserts what a user does and sees.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# A Machine declares the input that starts it
|
|
2
|
+
|
|
3
|
+
`POST /workflows/:name/runs` takes a JSON body and hands it to the machine as xstate `input` — unvalidated, and
|
|
4
|
+
undescribed: the module contract is `export const machine` alone (ADR-0011/0015), so nothing at runtime says what a run
|
|
5
|
+
of a given workflow should be started _with_. The Console's start form
|
|
6
|
+
([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) forced the question, but the gap predates it —
|
|
7
|
+
`jr2 run` has the same blindness, and a typo'd input surfaces as a confusing mid-run failure instead of a refusal at the
|
|
8
|
+
door. Meanwhile every event a machine _accepts_ already carries a zod schema, attached to the machine itself
|
|
9
|
+
([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)'s vocabulary-on-the-machine). The input that starts a
|
|
10
|
+
machine is the one piece of its vocabulary that pattern missed.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
- **The schema rides the machine, like the rest of the vocabulary.** `jr2Setup.createMachine` accepts an `input` (a zod
|
|
15
|
+
object) and attaches it the same way event defs ride today (WeakMap-keyed on `machine.config`, so a `.provide()` clone
|
|
16
|
+
keeps it — ADR-0011). `input` is deliberately xstate's own word for what a machine receives at creation — the
|
|
17
|
+
authoring surface teaches nothing new.
|
|
18
|
+
- **A door that no one reads is refused.** The `setup()` of xstate keeps an `input` in the config of `createMachine`,
|
|
19
|
+
but it does not read it. TypeScript accepts that `input`. Thus a Machine that `setup()` made and that has a zod
|
|
20
|
+
`input` has no door. A run starts with any input, and the Console start form is only a field for JSON. `jr2 up` and
|
|
21
|
+
the publish door use the same walk (`doorRefusals`, ADR-0069). The walk refuses a registered Machine if its config has
|
|
22
|
+
a zod `input` and no door is attached to it. The refusal line gives the fix: make the Machine with `jr2Setup`. If the
|
|
23
|
+
Machine takes no workflow event, leave out `events` (ADR-0015). The walk tests the attached door, not the factory.
|
|
24
|
+
Thus a `workspace()` or `pool()` wrapper with its own door passes. A `.provide()` clone keeps the config of its
|
|
25
|
+
original, so the walk gets the same result for the clone. A Machine with no `input`, or with an `input` that is not
|
|
26
|
+
zod, can use `setup()`. The `ping` Workflow that `jr2 init` writes uses it. The walk checks the registered roots only,
|
|
27
|
+
because the `input` of a nested Machine is never the door (see below). `workspace()` refuses a body with a zod `input`
|
|
28
|
+
when you call it. It reads the config and the attached door, so it also refuses a body that `setup()` made. Considered
|
|
29
|
+
and rejected:
|
|
30
|
+
- Refuse all `setup()` Machines. This adds work to the first Workflow and gives nothing.
|
|
31
|
+
- Refuse at boot. Then an Assembly that is already published stops the Orchestrator. The walk refuses only the next
|
|
32
|
+
publish.
|
|
33
|
+
- Take a zod `input` on a `setup()` Machine as the door at load. The loader can read that `input`. But then there are
|
|
34
|
+
two ways to declare one door. Also, the zod schema stays in `machine.config`, and a schema must not go into the
|
|
35
|
+
fingerprint and serialization paths that read the config. And that Machine does not get the Vocabulary checks of
|
|
36
|
+
`jr2Setup`.
|
|
37
|
+
- **A wrapper declares its own door; it never borrows its child's.** `workspace(body, { input, spec })` and
|
|
38
|
+
`pool(worker, { input, … })` each take an `input` of their own, and neither propagates one upward. The reason is the
|
|
39
|
+
same in both, at different strengths: a wrapper does not feed its child what a caller sent. `pool` is the sharp case —
|
|
40
|
+
a worker is fed per-item, never the run body, so propagating its schema would demand fields the pool never passes and
|
|
41
|
+
strip the pool-level fields `cap`/`itemInput` read. `workspace` is the mild one — its body gets the run input _plus_
|
|
42
|
+
the injected `workspace` handles, so the body's contract is the door PLUS a field no caller can send (the handles do
|
|
43
|
+
not exist until a Sandbox is provisioned and attached). Propagating it would make `createMachine({ input })` mean "my
|
|
44
|
+
input" on a bare machine and "my input minus what my wrapper injects" on a wrapped body — a context-dependent
|
|
45
|
+
contract, and a trap: a body that declared its input honestly, handles included, would 400 every valid run start. The
|
|
46
|
+
vocabulary does not propagate either — event names resolve against the Machine that invoked the actor, so a body keeps
|
|
47
|
+
its own defs and nothing is merged upward (ADR-0011, ADR-0049). The door is one more part a Machine carries and scopes
|
|
48
|
+
to itself. The composition the body needs a name for is exported instead:
|
|
49
|
+
`Workspaced<T, Slot> = WithoutEmptyIndex<T> & { workspace: WorkspaceHandles<Slot> }`, the handles keyed by the Repo
|
|
50
|
+
Slots the body names (ADR-0050, ADR-0051). A door with no fields gets the same name: `z.infer` of `z.object({})` is
|
|
51
|
+
`Record<string, never>`, an index signature that says each key holds `never`, and intersected with the handles it
|
|
52
|
+
makes each `assign` to the body's context a type error that does not name the cause. So `Workspaced` removes a string
|
|
53
|
+
index signature whose value is `never`, and only that: named fields beside it stay (`HostInjectedInput & …` keeps
|
|
54
|
+
`instanceId`), and an input that is a real map is not changed. A separate name for the empty door was the alternative;
|
|
55
|
+
it was rejected because the author then chooses between two names for one statement, and finds the second only after
|
|
56
|
+
the opaque error. A body that _still_ declares a schema is refused by `workspace()` with a message naming the fix:
|
|
57
|
+
nothing would ever serve or validate it, and a contract nobody enforces is worse than none.
|
|
58
|
+
- **The door types the mapping, and constrains the body.** `WorkspaceOptions.spec` reads its `input` from the declared
|
|
59
|
+
schema (`z.infer`), so a workflow states its shape once and the workspace mapping is checked against it — the drift
|
|
60
|
+
this ADR exists to close, closed in the one place that used to re-declare the shape by hand. The same schema checks
|
|
61
|
+
the _body_, in **one direction only**: the body may not demand more than the wrapper will hand it, which is
|
|
62
|
+
`Workspaced<door, slots>`; demanding _less_ is safe, because it is fed a superset. So a body reading a field the door
|
|
63
|
+
never carries is a build error at the `workspace()` call instead of an `undefined` mid-run. The guard counts the raw
|
|
64
|
+
door as provided beside `Workspaced<door, slots>`: for the door `z.object({})` the two differ, and a body that writes
|
|
65
|
+
`z.infer<typeof door>`, alone or with the handles, stays legal. Comparing against `Workspaced` alone was the
|
|
66
|
+
alternative; it was rejected because it refused those bodies, which compiled before `Workspaced` removed the empty
|
|
67
|
+
index signature. One field sits outside that check, named as `HostInjectedInput`: the host adds `instanceId` to the
|
|
68
|
+
input of the machine a run _starts_ with, so a root-placed wrapper hands its body the door, the handles, _and_ that —
|
|
69
|
+
while a nested one does not, and no type can see which. It is not door material either (no caller sends it, `start`
|
|
70
|
+
overwrites it after the parse, nothing serves it), so widening the door would be the wrong fix. The guard takes the
|
|
71
|
+
permissive answer: it counts those keys as _provided_, so a body may declare the field honestly. Counting them as
|
|
72
|
+
provided rather than subtracting them from what the body demands is deliberate — subtraction would drop the keys from
|
|
73
|
+
the comparison and with them two cases the check owns: a body that mistypes `instanceId`, and a body whose input is a
|
|
74
|
+
union, which subtraction compares on its members' shared keys alone. The typed wrapper also checks the seam above it —
|
|
75
|
+
a parent invoking a nested `workspace()` has its input mapper checked against that wrapper's door, and gets the body's
|
|
76
|
+
output typed back. With no schema declared the door is permissive, so there is nothing to infer and the mapper's
|
|
77
|
+
argument is `unknown` — an honest "jr2 does not know", not `any`; a wrapper fed by something other than a caller (a
|
|
78
|
+
pool worker) states what it is fed by annotating the parameter, as `PoolSpec.cap`/`itemInput` do. `spec` takes the
|
|
79
|
+
same permissive answer as the guard: its `input` is the door plus `HostInjectedInput`, so a `spec` gives each run a
|
|
80
|
+
branch from `instanceId` with no cast, and a nested wrapper whose `spec` reads it needs a parent that gives it.
|
|
81
|
+
`Partial<HostInjectedInput>` was the alternative; it was rejected because each root `spec` would then write a fallback
|
|
82
|
+
for a value that is always there. The input of the wrapper stays the door alone, so no parent must send `instanceId`.
|
|
83
|
+
For the door `z.object({})` that input is `{}`, the door without its `Record<string, never>`, so a parent can give
|
|
84
|
+
`instanceId` to a nested `spec` that reads it; with the raw `z.infer` it could not, and the nested Workspace then
|
|
85
|
+
pushed each run to `fix/undefined`. `z.infer<…> & Partial<HostInjectedInput>` was the alternative; it was rejected
|
|
86
|
+
because the index signature makes `instanceId` `never` there too. A per-run Repo Slot's mapper keeps the door alone,
|
|
87
|
+
because a url does not come from a run's id.
|
|
88
|
+
- **The schema is structure, so it is open.** Workflow-detail JSON (`GET /workflows/:name`, the address ADR-0032's
|
|
89
|
+
negotiation kept) serves it as JSON Schema — same band as the Machine doc, and what drives the Console's start form
|
|
90
|
+
before any token is entered. The _submit_ stays `authenticated`; schema open, trigger guarded.
|
|
91
|
+
- **A declared schema is enforced at the door.** `POST /workflows/:name/runs` validates the body and 400s naming the
|
|
92
|
+
accepted shape — parity with what Gates already do for deliveries. No schema declared → accept anything (today's
|
|
93
|
+
behavior), and the Console falls back from a generated form to a raw-JSON textarea.
|
|
94
|
+
- **The door takes an idempotency key, out of band.** A start may carry one in a header, never in the body (a body field
|
|
95
|
+
would be one every door had to reserve); a repeat with the same key answers with the run the first one made. A
|
|
96
|
+
`dispatch` Effect sends its ledger row's id ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)), so a
|
|
97
|
+
start whose answer was lost is sent once where it may have arrived (ADR-0067) and makes one run. `jr2 run` and the
|
|
98
|
+
Console send none.
|
|
99
|
+
|
|
100
|
+
## Considered options
|
|
101
|
+
|
|
102
|
+
- **`export const input` beside `export const machine`.** Rejected: it re-opens what ADR-0015 closed — a workflow's
|
|
103
|
+
self-description split across module exports, when the machine object already carries every other piece of the
|
|
104
|
+
workflow's self-description.
|
|
105
|
+
- **`workspace` propagates its body's schema onto the wrapper** (the shape this ADR first took, before `pool` forced the
|
|
106
|
+
question and `workspace` turned out to be the same species). Rejected: the wrapper's pass-through is not the body's
|
|
107
|
+
input — the body also gets the injected `workspace` field — so the propagated schema is right only for a body that
|
|
108
|
+
under-declares its own contract. Two ways to declare one door also cost more than the line of ceremony they save: an
|
|
109
|
+
author reading a wrapped workflow could not tell, without opening the factory, which declaration the runtime serves.
|
|
110
|
+
- **Derive from xstate's `types.input`.** Impossible: TypeScript types are erased; there is nothing at runtime to serve
|
|
111
|
+
or validate with.
|
|
112
|
+
- **Constrain the body by pinning its `TInput` slot** (`body: StateMachine<…, Workspaced<door, slots>, …>`). Rejected as
|
|
113
|
+
unsound, not as style: `StateMachine`'s members include methods, method parameters are bivariant, and a body demanding
|
|
114
|
+
_more_ than the door provides therefore compiles — the one case the constraint exists to catch. The shipped
|
|
115
|
+
formulation intersects the body with a conditional guard whose failure branch is an object type keyed by the sentence
|
|
116
|
+
to read, so the diagnostic names the fix rather than printing a structural diff.
|
|
117
|
+
- **Require a schema on every workflow.** Rejected: it taxes the zero-ceremony hello-world for a guarantee only
|
|
118
|
+
input-taking workflows need. Absence is permissive, and permissive is what the wire already was.
|
|
119
|
+
|
|
120
|
+
## Consequences
|
|
121
|
+
|
|
122
|
+
- **Both entry points sharpen for free**: the Console generates its form from the same JSON Schema `jr2 run` can later
|
|
123
|
+
use to refuse bad input client-side before the POST.
|
|
124
|
+
- **The open band grows a schema and nothing else** — the same class of thing as the Machine doc it sits beside;
|
|
125
|
+
ADR-0014's projection is untouched.
|
|
126
|
+
- **Validation parity means error parity**: a bad run start and a bad gate delivery now fail the same way, a 400 naming
|
|
127
|
+
what is accepted.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# The Console renders with Preact, and the server erases the types
|
|
2
|
+
|
|
3
|
+
The Console ([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) outgrew its framework-free form: `main.js`
|
|
4
|
+
reached ~1,500 lines, much of it hand-rolled DOM reconciliation (`memo()`, `paint()`, manual list diffing) — work a view
|
|
5
|
+
library exists to own. Three standing constraints shaped the replacement: the repo is zero-build (Node 24 strips types;
|
|
6
|
+
nothing compiles), the Console serves only its own assets (ADR-0032), and `@jr2/orchestrator` is a library that serves
|
|
7
|
+
its files from disk wherever the process starts. A fourth arrived with this decision: Console source is TypeScript like
|
|
8
|
+
everything else.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
- **Preact, called through `h()` — no JSX.** JSX is codegen, and codegen needs a compiler; `h()` calls are plain
|
|
13
|
+
TypeScript, typechecked against Preact's own types, so the views sit inside `tsc --noEmit` with no toolchain. Preact
|
|
14
|
+
is React's API and mental model in ~4KB, shipped as browser-native ESM the orchestrator can serve verbatim.
|
|
15
|
+
- **Console source is `.ts`; the server erases the types.** The browser requests `/assets/*.ts` and receives the file
|
|
16
|
+
with its types replaced by whitespace (`ts-blank-space`), erased once and cached in memory. Erasure is not
|
|
17
|
+
compilation: positions are preserved, so browser stack traces point at the real source line with no sourcemaps, and
|
|
18
|
+
import specifiers keep their `.ts` extensions — the orchestrator does for the browser exactly what Node does for
|
|
19
|
+
itself. The failure class is gated where it belongs: what `ts-blank-space` cannot erase, `tsc --noEmit` already
|
|
20
|
+
rejected in CI.
|
|
21
|
+
- **Vendor rides the dep edge.** Preact's ESM files are served at `/assets/vendor/*`, resolved through this package's
|
|
22
|
+
own dependency edge and mapped to bare specifiers by an import map in `page.html` — the elkjs precedent, extended. No
|
|
23
|
+
CDN, no third-party origin; ADR-0032's asset clause does not move.
|
|
24
|
+
- **The store stays a pure reducer; the vdom replaces the painter.** `store.js` + `store.d.ts` collapse into one
|
|
25
|
+
`store.ts` read by both consumers (tests through Node's stripping, the browser through the server's). `dispatch` stays
|
|
26
|
+
the one door: fold the frame, render the app from the top, and let vdom diffing do what `memo()`/`paint()` did by
|
|
27
|
+
hand. Signals were rejected: finer-grained updates solve a performance problem SSE-paced frames do not have, at the
|
|
28
|
+
cost of the reducer's tests and the store-first language.
|
|
29
|
+
- **The machine canvas stays an imperative island.** elk's layout is async and pan/zoom is a 60Hz transform; neither
|
|
30
|
+
wants a vdom. A component owns the `<svg>` by ref and the existing rendering code moves house without being rebuilt.
|
|
31
|
+
|
|
32
|
+
## Considered options
|
|
33
|
+
|
|
34
|
+
- **SolidJS** (the requested starting point). Its fine-grained reactivity lives in its compiler, and the compiler is
|
|
35
|
+
babel-only (`babel-plugin-jsx-dom-expressions`; `esbuild-plugin-solid` wraps babel rather than replacing it). Every
|
|
36
|
+
route to it costs an invariant: babel at runtime widens the production dep chain of the token-guarded package, vite
|
|
37
|
+
middleware puts a dev server in production as the asset pipeline, and a pack-time build hands the repo its only
|
|
38
|
+
toolchain plus the stale-artifact problem class. Solid without its compiler has no reason to exist.
|
|
39
|
+
- **React.** Verified against the `react@19.2.8` / `react-dom@19.2.8` tarballs: still CJS
|
|
40
|
+
(`module.exports = require(...)` behind a `process.env.NODE_ENV` branch) — a browser evaluates neither. Self-serving
|
|
41
|
+
it means a resolve-and-bundle step at boot. Preact offers the same API for a page that ADR-0032 already bars from
|
|
42
|
+
React's real advantage, the third-party component ecosystem.
|
|
43
|
+
- **Runtime vite.** Works mechanically (middleware mode); rejected because vite's dev server is explicitly not for
|
|
44
|
+
production serving, and it relocates compile errors to the worst place — a 500 from a deployed orchestrator instead of
|
|
45
|
+
a red CI step.
|
|
46
|
+
- **`htm` tagged templates.** A no-build JSX-alike, but props inside template strings sit outside the type checker — an
|
|
47
|
+
untyped gap exactly where the Console's bugs live (rail rows, gate cards, schema forms). `h()` keeps the views typed.
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
- The erasable-syntax constraint (no enums, no constructor parameter properties) now applies to Console code, as it
|
|
52
|
+
already does to everything Node runs in this repo.
|
|
53
|
+
- `ts-blank-space` and `preact` become production dependencies of `@jr2/orchestrator`; the package's `typecheck` must
|
|
54
|
+
cover `console/` with DOM types.
|
|
55
|
+
- TSX later is additive — an `esbuild.transform()` at the same serve point — not a rework.
|
|
56
|
+
- The refactor is behavior-preserving: `console-store.test.ts` retargets to `store.ts`, the `@console` browser tier and
|
|
57
|
+
the default e2e profile stay the acceptance bar, and no route moves except how `/assets/*` bytes are produced.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# A runaway turn is ended by the Harness, rerolled once, then a fault
|
|
2
|
+
|
|
3
|
+
[ADR-0016](0016-agent-turn-mechanics-are-internal.md) absorbed turn mechanics behind one terminal
|
|
4
|
+
`agent.fault { reason }` and named two fault classes: **infra** (the turn broke — provider retries in the Harness, then
|
|
5
|
+
fault) and **no-signal** (the turn ended without a menu pick — budgeted nudge in `agentRun`, then fault). Both assume
|
|
6
|
+
the turn ends. Production found the third case: a turn that never does. Two live `task-with-review` runs degenerated — a
|
|
7
|
+
reviewer handed an empty diff collapsed into ~100 byte-identical `grep` calls; a coder rewrote the same README
|
|
8
|
+
indefinitely after committing real work. Nothing bounded either: `agentRun` awaits settlement, `nudgeBudget` covers only
|
|
9
|
+
turns that end, and neither jr2's Harness nor the retired flue runtime ever capped steps.
|
|
10
|
+
|
|
11
|
+
The investigation ruled the refactor out as the cause. Replays of both failure shapes against the live endpoint —
|
|
12
|
+
including the exact empty-diff reviewer state — terminated 13/13 with max 2 consecutive identical calls, in both the
|
|
13
|
+
current bare-instructions prompt and the flue-shaped one. The degeneration is stochastic model tail behavior,
|
|
14
|
+
concentrated where a task gives the model nothing real to do. That shapes the design: the guard is liveness insurance
|
|
15
|
+
against a tail event, not a correctness mechanism, and the recovery that works is a fresh roll of the dice.
|
|
16
|
+
|
|
17
|
+
**Runaway** names what jr2 observed — the turn ran past the point where jr2 stops believing it will conclude — on the
|
|
18
|
+
same principle as "no-signal": fault classes are named for the observation, not the model's inner pathology.
|
|
19
|
+
"Degeneration" is the behavior the guard usually catches; the budget trigger also ends honest dithering that never
|
|
20
|
+
collapses into repetition.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
- **Detection lives in the Harness turn loop** (`turn.ts`) — the one seat that watches steps as they happen; the
|
|
25
|
+
Orchestrator only sees the settlement, and a runaway never settles. Two triggers, both jr2-owned defaults with **no
|
|
26
|
+
author surface** (ADR-0016: budgets are defaulted knobs): a **step budget** (initial 256; healthy turns peaked at 53
|
|
27
|
+
on a trivial task, and an honest live bug-hunt burned 161 steps before exhausting its context) as the unconditional
|
|
28
|
+
backstop, and **K consecutive byte-identical tool calls** (initial K=4; healthy max observed 2, the production loop
|
|
29
|
+
~100) as the early exit. If a surface is ever demanded, note it would be a Dial question (ADR-0018) — how hard to run
|
|
30
|
+
this one Turn — but no workflow has needed one. Both count tool calls alone. A step is a tool call that starts while
|
|
31
|
+
the Turn is live: the Harness counts it at the engine's `tool_start` event
|
|
32
|
+
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)), before the engine checks the tool or
|
|
33
|
+
the arguments. So these calls are steps: a call that runs; a call of an unknown tool, or with arguments that do not
|
|
34
|
+
validate, which gets the engine's own error; a pick that the Harness blocks as the second pick of its model answer
|
|
35
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)); each call of a model answer that hit the
|
|
36
|
+
output limit, which the engine answers with an error and does not run; and the tripping call itself, which is counted
|
|
37
|
+
and does not run. A tripping call of a tool the run has, with valid arguments, reads `Operation aborted`. A tripping
|
|
38
|
+
call of an unknown tool, or with invalid arguments, reads the engine's own error for it. A model that repeats any of
|
|
39
|
+
them is a Runaway. Only one kind of call is not a step: a call after a pick that ended the Turn, which the Harness
|
|
40
|
+
blocks. The Turn that it would count for is over, so a trip cannot make an ended Turn a Runaway. A Turn part the
|
|
41
|
+
Harness restates at a step boundary
|
|
42
|
+
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)) is not a step and
|
|
43
|
+
neither breaks nor extends an identical-call streak. An outstanding pick
|
|
44
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)) is one call however long the workflow runs
|
|
45
|
+
it — the guard has no clock, so a held Turn is never a Runaway for waiting — and its answer **resets the
|
|
46
|
+
identical-call streak**: the world moved, so the same pick again is a new question. Four `run_tests` in a row, each
|
|
47
|
+
answered by a flaky suite, is a model reading answers, not a loop. A pick whose child replies inside the pick's own
|
|
48
|
+
send resets it the same way. A guard straight back does not: it is answered, but nothing ran and the state is where it
|
|
49
|
+
was, so the same pick four times is a Runaway.
|
|
50
|
+
- **Tripping settles the Submission `failed` with `SettlementError.type: "runaway"`** and a legible message ("repeated
|
|
51
|
+
an identical tool call 4 times", "exceeded 256 steps"). A Runaway is a kind of failure, so it stays `failed`, and the
|
|
52
|
+
typed error is what lets `agentRun` switch on the class without parsing prose. A Turn that its own pick ended is a
|
|
53
|
+
different end, not a failure. So it has an outcome of its own, `ended`. `aborted` is a Turn that something else
|
|
54
|
+
stopped ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). A Runaway is neither: the Harness
|
|
55
|
+
ended a Turn that nothing had ended.
|
|
56
|
+
- **`agentRun` retries once, as a fresh conversation, with the identical prompt.** The degenerate context is poisoned —
|
|
57
|
+
a model that just emitted a hundred identical calls conditions on them, so a nudge into the same conversation rerolls
|
|
58
|
+
loaded dice; the fresh session is ADR-0016's lossy handoff applied as recovery. The prompt carries no annotation:
|
|
59
|
+
mechanism must not bleed into the one thing the workflow authored, and the failure being stochastic means the fix is
|
|
60
|
+
the reroll, not different instructions. Budget 1 (a defaulted knob): a runaway retry costs an entire turn, and two
|
|
61
|
+
independent rolls both running away is evidence the task itself is pathological — that belongs with the workflow's
|
|
62
|
+
fault routing, not a third attempt.
|
|
63
|
+
- **A `continue: true` invocation gets no retry — straight to the fault.** The one recovery jr2 knows is the identical
|
|
64
|
+
prompt on a fresh conversation, and a continued Turn's prompt is meaningless fresh. The fault retires that
|
|
65
|
+
conversation: jr2 bumps its epoch, so the next `continue` on the Agent lands on a virgin one (ADR-0057), and the fault
|
|
66
|
+
routing decides what to brief it with.
|
|
67
|
+
- **Exhaustion emits the same single terminal `agent.fault { reason }`.** The workflow surface does not change; routing
|
|
68
|
+
(park at a Gate, re-invoke at a higher Dial) stays workflow policy, and `task-with-review`'s existing `agent.fault`
|
|
69
|
+
arm already handles it. Retry attempts surface as run-feed telemetry like nudge attempts.
|
|
70
|
+
- **The claims are proven in the ADR-0027 conformance suite** — a scripted provider that loops deterministically is the
|
|
71
|
+
only tier that can produce a runaway on demand; it runs in the default `pnpm -r test` gate.
|
|
72
|
+
|
|
73
|
+
## Considered options
|
|
74
|
+
|
|
75
|
+
- **Windowed duplicate-fraction trigger** (catches A-B-A-B alternation early). Rejected for now: no observed instance,
|
|
76
|
+
the step budget backstops it, and it is the only trigger with a plausible false positive — read → edit → read of the
|
|
77
|
+
same path is an identical signature twice, legitimately.
|
|
78
|
+
- **Retry into the same conversation** (nudge-style). Rejected: the poisoned-context argument above; a nudge explains
|
|
79
|
+
itself to a conversation that must continue, a runaway retry exists because the conversation must not.
|
|
80
|
+
- **Workflow-side detection.** Rejected: workflows speak vocabulary, not step counts (ADR-0015/0016), and the
|
|
81
|
+
Orchestrator cannot see inside an unsettled turn without growing a new observation surface for mechanism.
|
|
82
|
+
- **A per-Agent or per-Turn knob now.** Rejected: greenfield, no evidence any turn needs a bound other than the default;
|
|
83
|
+
adding surface first invites tuning before data exists.
|
|
84
|
+
|
|
85
|
+
## Consequences
|
|
86
|
+
|
|
87
|
+
- ADR-0016's taxonomy is three classes: infra, no-signal, runaway. Its "either budget exhausting emits the one terminal
|
|
88
|
+
fault" sentence now reads across three budgets.
|
|
89
|
+
- A runaway that survives the reroll parks where `agent.fault` routes — for `task-with-review`, the `humanReview` Gate
|
|
90
|
+
with the runaway reason in `meta`, Sandbox intact for inspection.
|
|
91
|
+
- The bound makes non-termination cost at most `(1 + retry) × step budget` instead of a context window; it does nothing
|
|
92
|
+
for output quality — a no-op task still yields invented churn, which is task authoring, not mechanics.
|