@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/src/harness-client.ts
CHANGED
|
@@ -15,32 +15,74 @@
|
|
|
15
15
|
// (`{ streamUrl, offset, submissionId }` — jr2's durable re-attach handle, stored in the host
|
|
16
16
|
// ledger), and `wait(admission)` follows the durable stream from the admission offset to the
|
|
17
17
|
// Submission's Settlement — a long-poll loop that advances by the `stream-next-offset` header and
|
|
18
|
-
// reconnects from the SAME offset on network failure (capped backoff, indefinitely: pod
|
|
19
|
-
// `workspace.lost`'s
|
|
18
|
+
// reconnects from the SAME offset on network failure (capped backoff, indefinitely: a lost pod is
|
|
19
|
+
// `workspace.lost`'s to report and a restarted Harness the Agent actor's, both off the watch —
|
|
20
|
+
// ADR-0021 — never this loop's to guess). Re-attach after a restart is `wait`
|
|
20
21
|
// with the SAME persisted admission; replay cost is bounded by one Submission's chunks. A 404 is a
|
|
21
|
-
// LOST conversation (ADR-0027:
|
|
22
|
-
//
|
|
22
|
+
// LOST conversation (ADR-0027: the Harness no longer holds it — a persisted conversation survives a
|
|
23
|
+
// Harness restart, ADR-0031, and one it cannot rebuild or has freed does not) — a
|
|
24
|
+
// `SettlementFault`, never an endless poll. A Submission a restart cut settles `failed` on the
|
|
25
|
+
// rebuilt stream (ADR-0031), which is read like any other Settlement.
|
|
23
26
|
//
|
|
24
27
|
// Both verbs re-send on a network failure, for one reason stated twice: an unanswered request is
|
|
25
28
|
// not an answer. They differ in bound, and the difference is where the Submission is — `wait`'s is
|
|
26
|
-
// already admitted, so the
|
|
29
|
+
// already admitted, so the watch may report a dead pod or Harness and this loop need never give up, while
|
|
27
30
|
// `send`'s does not exist yet, so its window closes and faults. `send` also re-sends ONLY what
|
|
28
31
|
// provably never left this host (`postAdmission` — ADR-0042: it is the first thing ever to dial a
|
|
29
32
|
// Sandbox's Service, and a CR at `phase: Ready` is not yet routable).
|
|
30
33
|
//
|
|
31
34
|
// `abort` is the third verb, and ADR-0024 wires it to the END OF THE INVOCATION: a turn ends with
|
|
32
|
-
// the state that asked for it. It sweeps the running Submission and everything queued behind it
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
+
// the state that asked for it. It sweeps the running Submission and everything queued behind it,
|
|
36
|
+
// and says its cause: `pick` when the Turn's own pick ended it, which settles the running
|
|
37
|
+
// Submission `ended`; no cause otherwise, which settles it `aborted`. The outcome is worth having for
|
|
38
|
+
// observability, though jr2 never reads it (the actor is stopped by then; see actor.ts).
|
|
35
39
|
|
|
36
40
|
import { agentActorWith } from "./actor.ts";
|
|
37
|
-
import type {
|
|
41
|
+
import type {
|
|
42
|
+
AgentAdmission,
|
|
43
|
+
AgentAdmitOptions,
|
|
44
|
+
AgentLogic,
|
|
45
|
+
AgentRunInput,
|
|
46
|
+
AgentRunPort,
|
|
47
|
+
AgentSettleOptions,
|
|
48
|
+
ListedKill,
|
|
49
|
+
} from "./actor.ts";
|
|
38
50
|
import type { AgentDeclaration, AgentDefinition, ThinkingLevel } from "./agent.ts";
|
|
51
|
+
import {
|
|
52
|
+
commandWith,
|
|
53
|
+
ExecNotSent,
|
|
54
|
+
ExecRefused,
|
|
55
|
+
type Arg,
|
|
56
|
+
type CommandInput,
|
|
57
|
+
type CommandOptions,
|
|
58
|
+
type CommandOutput,
|
|
59
|
+
type OpenCommandInput,
|
|
60
|
+
type EnvOpenParts,
|
|
61
|
+
type EffectLogic,
|
|
62
|
+
type ExecPort,
|
|
63
|
+
} from "./effect.ts";
|
|
64
|
+
import { open } from "./open.ts";
|
|
65
|
+
import type { OpenEffect, OpenOf } from "./open-effect.ts";
|
|
39
66
|
import {
|
|
40
67
|
LIVE_LONG_POLL,
|
|
41
68
|
STREAM_NEXT_OFFSET_HEADER,
|
|
69
|
+
VIEW_HISTORY,
|
|
42
70
|
VIEW_UPDATES,
|
|
71
|
+
type AbortCause,
|
|
72
|
+
type AbortRequest,
|
|
43
73
|
type EchoEvent,
|
|
74
|
+
type ExecRead,
|
|
75
|
+
type ExecRequest,
|
|
76
|
+
type LiveConversation,
|
|
77
|
+
type LiveSetRequest,
|
|
78
|
+
type LiveSetResponse,
|
|
79
|
+
type MemoryKillsResponse,
|
|
80
|
+
type NotKeptResponse,
|
|
81
|
+
MAX_KILL_NAME,
|
|
82
|
+
MAX_KILL_STAMP,
|
|
83
|
+
MAX_KILLS_BODY,
|
|
84
|
+
MAX_TOLD,
|
|
85
|
+
type Notice,
|
|
44
86
|
type Settlement,
|
|
45
87
|
type StreamEvent,
|
|
46
88
|
type SubmissionSettledEvent,
|
|
@@ -49,8 +91,8 @@ import {
|
|
|
49
91
|
/** The three-verb wire client (the injectable seam — structurally what `@flue/sdk`'s
|
|
50
92
|
* `agents.{send,wait,abort}` was, minus the SDK). */
|
|
51
93
|
export type HarnessClient = {
|
|
52
|
-
/** `POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?}`
|
|
53
|
-
* with `streamUrl` resolved absolute. The FRAME is `message` and `cwd` — what this Turn is about
|
|
94
|
+
/** `POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?, notices?, kept?}`
|
|
95
|
+
* → the Admission, with `streamUrl` resolved absolute. The FRAME is `message` and `cwd` — what this Turn is about
|
|
54
96
|
* and where it works (ADR-0057), flat siblings on the body. The DEFINITION is the slot's,
|
|
55
97
|
* carried by the Machine and sent with every admission (ADR-0049) — the Harness keeps no roster,
|
|
56
98
|
* so `:name` alone would name nothing there — and it travels as IDENTITY alone, because this
|
|
@@ -67,14 +109,45 @@ export type HarnessClient = {
|
|
|
67
109
|
definition: AgentDefinition;
|
|
68
110
|
model?: string;
|
|
69
111
|
thinkingLevel?: ThinkingLevel;
|
|
112
|
+
/** The pending notices in this admission's scope (ADR-0062). Omitted when there are none. */
|
|
113
|
+
notices?: Notice[];
|
|
114
|
+
/** The admission is for a conversation this process was told the Harness keeps (ADR-0062).
|
|
115
|
+
* The Harness that keeps none answers 404 and seats none: this verb throws
|
|
116
|
+
* {@link ConversationNotKept}. */
|
|
117
|
+
kept?: boolean;
|
|
70
118
|
signal?: AbortSignal;
|
|
71
119
|
},
|
|
72
120
|
): Promise<AgentAdmission>;
|
|
73
|
-
/** Follow the stream to this Submission's Settlement: resolve on `completed
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
121
|
+
/** Follow the stream to this Submission's Settlement: resolve on `completed` alone — the run
|
|
122
|
+
* returned with the Turn still open, which the Agent actor answers with the nudge or the wake —
|
|
123
|
+
* and reject with `SettlementFault` on `failed`/`ended`/`aborted`/404. `ended` is no fault, and no
|
|
124
|
+
* live actor reads it: a Submission settles `ended` only after the Turn's own pick took the
|
|
125
|
+
* Machine out of the Agent's state. That stopped the actor that waits on it (ADR-0024), and a
|
|
126
|
+
* stopped actor drops every rejection. A guard kill read on the way is handed to `onMemoryLimit` (ADR-0061). */
|
|
127
|
+
wait(admission: AgentAdmission, opts?: AgentSettleOptions): Promise<void>;
|
|
128
|
+
/** `GET /agents/:name/:id?view=history`: does the Harness still hold this conversation? Only a
|
|
129
|
+
* 404 says no (ADR-0027: a GET on an unknown conversation is 404); anything else — another
|
|
130
|
+
* status, a transport failure — says yes, because unknown is never loss (ADR-0062). */
|
|
131
|
+
holds(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<boolean>;
|
|
132
|
+
/** `GET /memory-kills`: the pod's guard kills (ADR-0061, ADR-0062). A request that never left
|
|
133
|
+
* this host, and a draining Harness's 503, are sent again until `signal` ends the read — the
|
|
134
|
+
* caller owns the read's limit (the Agent actor's 5 s); with no signal, it is sent once. Any other
|
|
135
|
+
* answer but a 200 that reads — a Harness that keeps none, a transport failure, the caller's
|
|
136
|
+
* timeout — is `undefined`, "could not tell": the Harness still holds what it kept, and the next
|
|
137
|
+
* admission reads again. A kill with no id or time, with an id or a time outside the wire's bounds
|
|
138
|
+
* (`wire.ts`), or with a time that does not parse, is skipped; one whose `peak` or `limit` is not
|
|
139
|
+
* a size is kept without sizes, and one whose `told` holds an entry that is not a conversation, or
|
|
140
|
+
* a name outside the bounds, is kept without that entry — each fault said. Absent sizes are no fault: the pod left them out.
|
|
141
|
+
* Each fault is one line, said when it starts. A read that works says nothing, and the next fault
|
|
142
|
+
* after it is said again. */
|
|
143
|
+
memoryKills(opts?: { signal?: AbortSignal }): Promise<ListedKill[] | undefined>;
|
|
144
|
+
/** `POST /agents/:name/:id/abort`, with the body `{ cause, submissionId }` when a cause is given
|
|
145
|
+
* (`AbortRequest`). The `{ aborted }` answer is dropped (ADR-0024). */
|
|
146
|
+
abort(
|
|
147
|
+
agentName: string,
|
|
148
|
+
instanceId: string,
|
|
149
|
+
opts?: { signal?: AbortSignal; cause?: AbortCause; submissionId?: string },
|
|
150
|
+
): Promise<void>;
|
|
78
151
|
};
|
|
79
152
|
|
|
80
153
|
export type HarnessClientOptions = {
|
|
@@ -109,18 +182,100 @@ export type HarnessClientOptions = {
|
|
|
109
182
|
log?: (line: string) => void;
|
|
110
183
|
};
|
|
111
184
|
|
|
112
|
-
/** A Submission that settled `failed`/`aborted` — or whose conversation the Harness no
|
|
113
|
-
* has (404) — surfaced to the actor as an `agent.fault
|
|
185
|
+
/** A Submission that settled `failed`/`ended`/`aborted` — or whose conversation the Harness no
|
|
186
|
+
* longer has (404) — surfaced to the actor as an `agent.fault` while it still waits on the Turn.
|
|
187
|
+
* Replaces the SDK's execution error. */
|
|
114
188
|
export class SettlementFault extends Error {
|
|
115
189
|
/** The Settlement as the stream carried it; absent when the conversation itself was lost. */
|
|
116
190
|
readonly settlement?: Settlement;
|
|
117
|
-
|
|
191
|
+
/** The conversation itself is gone (404): its Harness no longer holds it (ADR-0027). One of the
|
|
192
|
+
* faults the Agent actor asks the Sandbox about, to name a memory kill (ADR-0061). */
|
|
193
|
+
readonly lost: boolean;
|
|
194
|
+
constructor(message: string, settlement?: Settlement, opts: { lost?: boolean } = {}) {
|
|
118
195
|
super(message);
|
|
119
196
|
this.name = "SettlementFault";
|
|
120
197
|
this.settlement = settlement;
|
|
198
|
+
this.lost = opts.lost === true;
|
|
121
199
|
}
|
|
122
200
|
}
|
|
123
201
|
|
|
202
|
+
/** An admission for a kept conversation (`kept`) that the Harness refused with 404: it keeps no
|
|
203
|
+
* such conversation, and it seated none (ADR-0062). `notKept` is what the Agent actor reads, as it
|
|
204
|
+
* reads `lost`: that module is wire-free. */
|
|
205
|
+
export class ConversationNotKept extends Error {
|
|
206
|
+
readonly notKept = true;
|
|
207
|
+
constructor(message: string) {
|
|
208
|
+
super(message);
|
|
209
|
+
this.name = "ConversationNotKept";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The Harnesses whose memory kills this process could not read, each said once (`memoryKills`).
|
|
214
|
+
* Per process, not per client: an Agent actor builds a client for each Turn. */
|
|
215
|
+
const killReadFaults = new Set<string>();
|
|
216
|
+
|
|
217
|
+
/** The Harnesses whose kills — on the list or on a stream — held a field this process could not
|
|
218
|
+
* use, each said once until a clean read. Per process, like `killReadFaults`. Neither set is cleaned
|
|
219
|
+
* when a Workspace goes: no hook reaches this module from the Sandbox's destroy, and an entry is
|
|
220
|
+
* one URL per Sandbox this process ever read. */
|
|
221
|
+
const badKillFaults = new Set<string>();
|
|
222
|
+
|
|
223
|
+
/** Say once, until a clean read, that a Harness's kills held a field this process could not use. */
|
|
224
|
+
function badKill(baseUrl: string, log: (line: string) => void, what: string): void {
|
|
225
|
+
if (badKillFaults.has(baseUrl)) return;
|
|
226
|
+
badKillFaults.add(baseUrl);
|
|
227
|
+
log(`harness ${baseUrl}: ${what} (ADR-0062)`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A size as the guard writes one (`1.9Gi`, `512Mi`), short. The Agent's uid can write the file
|
|
231
|
+
* the list is read from, and these words go into the next Agent's Briefing as they are, so
|
|
232
|
+
* nothing else passes (ADR-0062). */
|
|
233
|
+
function isSize(value: unknown): value is string {
|
|
234
|
+
return typeof value === "string" && value.length <= 16 && /^\d{1,6}(\.\d{1,3})?(Ki|Mi|Gi|Ti)$/.test(value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A string no longer than `max` characters. The bounds are the wire's (`wire.ts`), the same as
|
|
238
|
+
* the Harness's: the Harness counts the bytes of the JSON text, which are never fewer, so a kill it
|
|
239
|
+
* keeps is one this process keeps. An answer larger than `MAX_KILLS_BODY` is no list to act on:
|
|
240
|
+
* the Harness answers no list that large. */
|
|
241
|
+
const fits = (v: unknown, max: number): v is string => typeof v === "string" && v.length <= max;
|
|
242
|
+
|
|
243
|
+
/** One kill of the list as this process can use it, or undefined when it has no id or time, when
|
|
244
|
+
* its id or time is outside the wire's bounds, or when its time does not parse — the only kills
|
|
245
|
+
* that are dropped. Sizes that are not sizes are left out, so the kill is kept with no sizes; a
|
|
246
|
+
* `told` entry that is not a conversation, or whose name is outside the bounds, is left out of
|
|
247
|
+
* `told`. */
|
|
248
|
+
function listedKill(value: unknown): ListedKill | undefined {
|
|
249
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
250
|
+
const { id, at, peak, limit, told } = value as Record<string, unknown>;
|
|
251
|
+
if (!fits(id, MAX_KILL_STAMP) || !fits(at, MAX_KILL_STAMP) || Number.isNaN(Date.parse(at))) return undefined;
|
|
252
|
+
const conversations = (Array.isArray(told) ? told.slice(0, MAX_TOLD) : []).filter(
|
|
253
|
+
(t): t is { agent: string; instanceId: string } =>
|
|
254
|
+
typeof t === "object" && t !== null && fits(t.agent, MAX_KILL_NAME) && fits(t.instanceId, MAX_KILL_NAME),
|
|
255
|
+
);
|
|
256
|
+
const sized = isSize(peak) && isSize(limit);
|
|
257
|
+
return {
|
|
258
|
+
id,
|
|
259
|
+
at,
|
|
260
|
+
told: conversations.map(({ agent, instanceId }) => ({ agent, instanceId })),
|
|
261
|
+
...(sized ? { peak, limit } : {}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Whether a listed kill came through as the wire states it. */
|
|
266
|
+
function whole(raw: unknown, kill: ListedKill | undefined): boolean {
|
|
267
|
+
if (kill === undefined) return false;
|
|
268
|
+
const { peak, limit, told } = raw as { peak?: unknown; limit?: unknown; told?: unknown };
|
|
269
|
+
// Sizes the pod left out are no fault; sizes it sent that are not sizes are.
|
|
270
|
+
if (kill.limit === undefined && (peak !== undefined || limit !== undefined)) return false;
|
|
271
|
+
return Array.isArray(told) && Math.min(told.length, MAX_TOLD) === kill.told.length && told.length <= MAX_TOLD;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** A timeout's abort reason (`AbortSignal.timeout`): a read that ran out of time, not an abandon. */
|
|
275
|
+
function isTimeout(reason: unknown): boolean {
|
|
276
|
+
return reason instanceof DOMException && reason.name === "TimeoutError";
|
|
277
|
+
}
|
|
278
|
+
|
|
124
279
|
/** Build the real wire client from connection options. */
|
|
125
280
|
export function createHarnessClient(options: HarnessClientOptions): HarnessClient {
|
|
126
281
|
const { baseUrl } = options;
|
|
@@ -138,11 +293,11 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
138
293
|
* POST the admission, re-sending it while the request DEMONSTRABLY never left this host.
|
|
139
294
|
*
|
|
140
295
|
* This is `wait`'s reconnect rule, applied one step earlier and for the same reason: a request
|
|
141
|
-
* that could not connect is not a Harness that refused the prompt. It matters here because
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* not routable: the EndpointSlice behind the ClusterIP is programmed after the pod passes its
|
|
296
|
+
* that could not connect is not a Harness that refused the prompt. It matters here because
|
|
297
|
+
* provisioning waits on the CR's `phase: Ready`, which the operator computes from the POD, and
|
|
298
|
+
* a Workspace-less Turn (the Instance Harness) has no attach before it — so this can be the first
|
|
299
|
+
* request ever sent over a Service. (A Sandbox's attach, `POST /attach` since ADR-0063, dials it
|
|
300
|
+
* first and retries the same way.) Ready is not routable: the EndpointSlice behind the ClusterIP is programmed after the pod passes its
|
|
146
301
|
* probe, and until it is, kube-proxy REJECTs — which arrives here as a refused connection.
|
|
147
302
|
* Un-retried, that single blip lost the whole turn (the actor calls an admission failure a
|
|
148
303
|
* terminal `agent.fault`), which is exactly the flake that kept the `@kind` tier serial.
|
|
@@ -153,9 +308,15 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
153
308
|
* question "did this reach the wire at all", never "does this look transient".
|
|
154
309
|
*
|
|
155
310
|
* And the window is BOUNDED where `wait`'s is not. `wait` may reconnect forever because its
|
|
156
|
-
* Submission is already admitted, so the
|
|
311
|
+
* Submission is already admitted, so the watch owns the reporting (`workspace.lost`, or the
|
|
312
|
+
* Harness restart that ends the Turn — ADR-0021).
|
|
157
313
|
* Nothing is admitted yet here, so there is no turn for a lease to be about: an address that
|
|
158
314
|
* never answers is a fault this call has to name itself.
|
|
315
|
+
*
|
|
316
|
+
* A draining Harness's 503 is re-sent on the same ladder (ADR-0031): the Harness answers it
|
|
317
|
+
* before it reads the body, so it queued nothing, and it closes the connection with it, so the
|
|
318
|
+
* next attempt dials the Service again — which reaches the replacement once it serves. It is the
|
|
319
|
+
* one answer re-sent: every other status is the Harness's word about this admission.
|
|
159
320
|
*/
|
|
160
321
|
const postAdmission = async (url: string, init: RequestInit, signal?: AbortSignal): Promise<Response> => {
|
|
161
322
|
const startedAt = Date.now();
|
|
@@ -165,12 +326,9 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
165
326
|
let lastCode = "?";
|
|
166
327
|
for (;;) {
|
|
167
328
|
attempts += 1;
|
|
329
|
+
let res: Response;
|
|
168
330
|
try {
|
|
169
|
-
|
|
170
|
-
// Only when it actually cost something. A window that is never approached should be silent,
|
|
171
|
-
// so that a line appearing at all is already the signal (see `routabilityLine`).
|
|
172
|
-
if (attempts > 1) log(routabilityLine("admission", url, attempts, Date.now() - startedAt, lastCode));
|
|
173
|
-
return res;
|
|
331
|
+
res = await fetchImpl(url, init);
|
|
174
332
|
} catch (err) {
|
|
175
333
|
lastCode = transportCode(err);
|
|
176
334
|
// A local abandon propagates untranslated, exactly as in `wait` — the stopped actor
|
|
@@ -186,7 +344,19 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
186
344
|
}
|
|
187
345
|
await sleep(jittered(backoffMs), signal);
|
|
188
346
|
backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
|
|
347
|
+
continue;
|
|
189
348
|
}
|
|
349
|
+
if (res.status === DRAINING && Date.now() < deadline) {
|
|
350
|
+
lastCode = `HTTP${DRAINING}`;
|
|
351
|
+
await res.body?.cancel();
|
|
352
|
+
await sleep(jittered(backoffMs), signal);
|
|
353
|
+
backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// Only when it actually cost something. A window that is never approached should be silent,
|
|
357
|
+
// so that a line appearing at all is already the signal (see `routabilityLine`).
|
|
358
|
+
if (attempts > 1) log(routabilityLine("admission", url, attempts, Date.now() - startedAt, lastCode));
|
|
359
|
+
return res;
|
|
190
360
|
}
|
|
191
361
|
};
|
|
192
362
|
|
|
@@ -215,11 +385,29 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
215
385
|
// Omitted when unset, so an admission with no dials is byte-identical to before.
|
|
216
386
|
...(sendOptions.model ? { model: sendOptions.model } : {}),
|
|
217
387
|
...(sendOptions.thinkingLevel ? { thinkingLevel: sendOptions.thinkingLevel } : {}),
|
|
388
|
+
// The Briefing's news (ADR-0062): typed data, the Harness writes the words. Omitted
|
|
389
|
+
// when there is none, so an admission without notices is byte-identical to before.
|
|
390
|
+
...(sendOptions.notices?.length ? { notices: sendOptions.notices } : {}),
|
|
391
|
+
// Omitted when unset, so an admission that is not for a kept conversation is
|
|
392
|
+
// byte-identical to before.
|
|
393
|
+
...(sendOptions.kept ? { kept: true } : {}),
|
|
218
394
|
}),
|
|
219
395
|
signal: sendOptions.signal,
|
|
220
396
|
},
|
|
221
397
|
sendOptions.signal,
|
|
222
398
|
);
|
|
399
|
+
if (sendOptions.kept && res.status === 404) {
|
|
400
|
+
// "Not kept" only with the Harness's marker: a 404 from anything else — a proxy, a route
|
|
401
|
+
// that moved — is a failed admission, as before.
|
|
402
|
+
const body = (await res.json().catch(() => undefined)) as Partial<NotKeptResponse> | undefined;
|
|
403
|
+
const detail = typeof body?.error === "string" ? body.error : res.statusText || "no detail";
|
|
404
|
+
if (body?.kept === false) {
|
|
405
|
+
throw new ConversationNotKept(
|
|
406
|
+
`harness admission refused (404): ${detail} — the Harness does not keep this conversation (ADR-0062)`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
throw new Error(`harness admission failed (404): ${detail}`);
|
|
410
|
+
}
|
|
223
411
|
if (!res.ok) {
|
|
224
412
|
throw new Error(`harness admission failed (${res.status}): ${await errorDetail(res)}`);
|
|
225
413
|
}
|
|
@@ -247,7 +435,9 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
247
435
|
if (res.status === 404) {
|
|
248
436
|
throw new SettlementFault(
|
|
249
437
|
`conversation lost: the harness answered 404 for submission "${admission.submissionId}" — ` +
|
|
250
|
-
`
|
|
438
|
+
`the Harness no longer holds this conversation (ADR-0027, ADR-0031)`,
|
|
439
|
+
undefined,
|
|
440
|
+
{ lost: true },
|
|
251
441
|
);
|
|
252
442
|
}
|
|
253
443
|
// Refused, not unreachable: the bearer is derived, so a Harness that rejects it now
|
|
@@ -276,6 +466,22 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
276
466
|
backoffMs = backoffInitialMs;
|
|
277
467
|
|
|
278
468
|
for (const event of events) {
|
|
469
|
+
if (event.type === "memory-limit") {
|
|
470
|
+
const { id, peak, limit, conversationId, position } = event;
|
|
471
|
+
// A size that is not one is not put in a Briefing (the Agent's uid can write the record):
|
|
472
|
+
// the kill is kept, with no sizes, and the fault is said.
|
|
473
|
+
const sized = isSize(peak) && isSize(limit);
|
|
474
|
+
if (!sized)
|
|
475
|
+
badKill(baseUrl, log, "a memory kill on a stream has sizes that are not sizes — kept without them");
|
|
476
|
+
// An older Harness's event has no id: keyed by its place on the stream, exactly as the
|
|
477
|
+
// Orchestrator before raised it, so a replayed read is the same kill and it can never
|
|
478
|
+
// mark a later kill seen. An id too long to be one is no id.
|
|
479
|
+
const kill = fits(id, MAX_KILL_STAMP)
|
|
480
|
+
? { id }
|
|
481
|
+
: { id: `${conversationId}@${position.batch}.${position.index}`, legacy: true as const };
|
|
482
|
+
opts?.onMemoryLimit?.({ ...kill, ...(sized ? { peak, limit } : {}) });
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
279
485
|
if (event.type !== "submission-settled" || event.submissionId !== admission.submissionId) continue;
|
|
280
486
|
if (event.outcome === "completed") return;
|
|
281
487
|
throw new SettlementFault(faultMessage(event), toSettlement(event));
|
|
@@ -284,10 +490,111 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
|
|
|
284
490
|
}
|
|
285
491
|
},
|
|
286
492
|
|
|
493
|
+
async holds(agentName, instanceId, opts) {
|
|
494
|
+
const url = new URL(conversationUrl(agentName, instanceId));
|
|
495
|
+
url.searchParams.set("view", VIEW_HISTORY);
|
|
496
|
+
// Asked as a restart is seen, when the new container may not listen yet: a request that
|
|
497
|
+
// never left this host, and a draining Harness's 503, are sent again until the caller's
|
|
498
|
+
// signal ends the wait — then, and on any other failure, unknown is never loss.
|
|
499
|
+
let backoffMs = backoffInitialMs;
|
|
500
|
+
for (;;) {
|
|
501
|
+
let again = false;
|
|
502
|
+
try {
|
|
503
|
+
const res = await fetchImpl(url.toString(), { headers: authorization, signal: opts?.signal });
|
|
504
|
+
// Drain the body so the socket is released; the snapshot itself is not the question.
|
|
505
|
+
await res.body?.cancel().catch(() => undefined);
|
|
506
|
+
if (res.status !== DRAINING) return res.status !== 404;
|
|
507
|
+
again = true;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
if (opts?.signal?.aborted && !isTimeout(opts.signal.reason)) throw err;
|
|
510
|
+
again = !opts?.signal?.aborted && neverDelivered(err);
|
|
511
|
+
}
|
|
512
|
+
if (!again || opts?.signal === undefined) return true;
|
|
513
|
+
try {
|
|
514
|
+
await sleep(jittered(backoffMs), opts.signal);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
if (opts.signal.aborted && !isTimeout(opts.signal.reason)) throw err;
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
async memoryKills(opts) {
|
|
524
|
+
const url = new URL("/memory-kills", baseUrl).toString();
|
|
525
|
+
// Idempotent, so re-sent on what `postAdmission` re-sends — a request that never left this
|
|
526
|
+
// host, a draining Harness's 503 — on the same ladder, until the caller's signal ends it. The
|
|
527
|
+
// limit is the caller's alone, so there is one clock for it.
|
|
528
|
+
let backoffMs = backoffInitialMs;
|
|
529
|
+
let why: string;
|
|
530
|
+
for (;;) {
|
|
531
|
+
let again = false;
|
|
532
|
+
try {
|
|
533
|
+
const res = await fetchImpl(url, { headers: authorization, signal: opts?.signal });
|
|
534
|
+
if (res.status === 200 && Number(res.headers.get("content-length") ?? 0) > MAX_KILLS_BODY) {
|
|
535
|
+
await res.body?.cancel().catch(() => undefined);
|
|
536
|
+
why = `the answer is larger than ${MAX_KILLS_BODY} bytes`;
|
|
537
|
+
} else if (res.status === 200) {
|
|
538
|
+
const text = await res.text();
|
|
539
|
+
const kills =
|
|
540
|
+
text.length > MAX_KILLS_BODY ? undefined : (JSON.parse(text) as Partial<MemoryKillsResponse>).kills;
|
|
541
|
+
if (Array.isArray(kills)) {
|
|
542
|
+
// A read that works ends the fault: the next fault is said again.
|
|
543
|
+
killReadFaults.delete(baseUrl);
|
|
544
|
+
const read = kills.map(listedKill);
|
|
545
|
+
const usable = read.filter((kill): kill is ListedKill => kill !== undefined);
|
|
546
|
+
const flawed = kills.filter((raw, i) => !whole(raw, read[i])).length;
|
|
547
|
+
if (flawed === 0) badKillFaults.delete(baseUrl);
|
|
548
|
+
else {
|
|
549
|
+
badKill(
|
|
550
|
+
baseUrl,
|
|
551
|
+
log,
|
|
552
|
+
`${flawed} of its memory kills are not the wire's shape — those with no id or time, or with one outside the wire's bounds or a time that does not parse, are skipped, the others are kept without the fields that were wrong`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
return usable;
|
|
556
|
+
}
|
|
557
|
+
why = "the answer is not a list of kills";
|
|
558
|
+
} else {
|
|
559
|
+
why = `HTTP ${res.status}: ${await errorDetail(res)}`;
|
|
560
|
+
again = res.status === DRAINING;
|
|
561
|
+
}
|
|
562
|
+
} catch (err) {
|
|
563
|
+
// A local abandon propagates, as in every other verb; a timeout the caller set is none.
|
|
564
|
+
if (opts?.signal?.aborted && !isTimeout(opts.signal.reason)) throw err;
|
|
565
|
+
why = transportDetail(err);
|
|
566
|
+
again = !opts?.signal?.aborted && neverDelivered(err);
|
|
567
|
+
}
|
|
568
|
+
if (!again || opts?.signal === undefined) break;
|
|
569
|
+
try {
|
|
570
|
+
await sleep(jittered(backoffMs), opts?.signal);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
if (opts?.signal?.aborted && !isTimeout(opts.signal.reason)) throw err;
|
|
573
|
+
why = "the read's time ran out";
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
|
|
577
|
+
}
|
|
578
|
+
// One line for each fault: said when it starts. A read that works says nothing, and the next
|
|
579
|
+
// fault after it is said again.
|
|
580
|
+
if (!killReadFaults.has(baseUrl)) {
|
|
581
|
+
killReadFaults.add(baseUrl);
|
|
582
|
+
log(
|
|
583
|
+
`harness ${baseUrl}: cannot read its memory kills (${why}) — no notice from them now; the Harness keeps them, and the next admission reads again (ADR-0062)`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return undefined;
|
|
587
|
+
},
|
|
588
|
+
|
|
287
589
|
async abort(agentName, instanceId, opts) {
|
|
590
|
+
// No cause, no body: the request an Orchestrator from before `cause` sent, byte for byte.
|
|
591
|
+
const body: AbortRequest | undefined = opts?.cause
|
|
592
|
+
? { cause: opts.cause, ...(opts.submissionId ? { submissionId: opts.submissionId } : {}) }
|
|
593
|
+
: undefined;
|
|
288
594
|
const res = await fetchImpl(`${conversationUrl(agentName, instanceId)}/abort`, {
|
|
289
595
|
method: "POST",
|
|
290
|
-
headers: authorization,
|
|
596
|
+
headers: body ? { ...authorization, "content-type": "application/json" } : authorization,
|
|
597
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
291
598
|
signal: opts?.signal,
|
|
292
599
|
});
|
|
293
600
|
if (!res.ok) {
|
|
@@ -324,19 +631,37 @@ export function harnessAgentRunPort(client: HarnessClient): AgentRunPort {
|
|
|
324
631
|
definition: opts.definition,
|
|
325
632
|
model: input.model,
|
|
326
633
|
thinkingLevel: input.thinkingLevel,
|
|
634
|
+
...(opts.notices?.length ? { notices: opts.notices } : {}),
|
|
635
|
+
...(opts.kept ? { kept: true } : {}),
|
|
327
636
|
signal: opts.signal,
|
|
328
637
|
});
|
|
329
638
|
},
|
|
330
639
|
|
|
331
|
-
async settle(admission: AgentAdmission, opts?:
|
|
332
|
-
await client.wait(admission,
|
|
640
|
+
async settle(admission: AgentAdmission, opts?: AgentSettleOptions): Promise<void> {
|
|
641
|
+
await client.wait(admission, opts);
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
async holds(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<boolean> {
|
|
645
|
+
return await client.holds(agentName, instanceId, opts);
|
|
333
646
|
},
|
|
334
647
|
|
|
335
|
-
async
|
|
648
|
+
async memoryKills(opts?: { signal?: AbortSignal }): Promise<ListedKill[] | undefined> {
|
|
649
|
+
return await client.memoryKills(opts);
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
async abort(
|
|
653
|
+
agentName: string,
|
|
654
|
+
instanceId: string,
|
|
655
|
+
opts?: { signal?: AbortSignal; cause?: AbortCause; submissionId?: string },
|
|
656
|
+
): Promise<void> {
|
|
336
657
|
// The result (`{ aborted }` — whether there was work to end) is dropped: by the time this
|
|
337
658
|
// runs the state has already moved on, and an idle instance is exactly as fine as a
|
|
338
659
|
// stopped one. Settlement is asynchronous and nothing here is listening (ADR-0024).
|
|
339
|
-
await client.abort(agentName, instanceId, {
|
|
660
|
+
await client.abort(agentName, instanceId, {
|
|
661
|
+
signal: opts?.signal,
|
|
662
|
+
...(opts?.cause ? { cause: opts.cause } : {}),
|
|
663
|
+
...(opts?.submissionId ? { submissionId: opts.submissionId } : {}),
|
|
664
|
+
});
|
|
340
665
|
},
|
|
341
666
|
};
|
|
342
667
|
}
|
|
@@ -387,28 +712,419 @@ export function createEchoPush(options: {
|
|
|
387
712
|
}
|
|
388
713
|
|
|
389
714
|
/**
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
|
|
715
|
+
* Build the live-set statement for the Instance Harness (ADR-0031): `PUT /agents
|
|
716
|
+
* { live: [{ agent, instanceId }] }` → `{ freed }`, and the Harness frees memory and files of every
|
|
717
|
+
* conversation not named. The whole set every time — level-triggered, so a missed statement costs
|
|
718
|
+
* nothing and the next one says it all again. Un-retried, like the echo: the caller is a timer.
|
|
719
|
+
*/
|
|
720
|
+
export function createLiveSetPush(options: {
|
|
721
|
+
baseUrl: string;
|
|
722
|
+
/** The Instance Harness's bearer (ADR-0058). Absent only against the stub Harness. */
|
|
723
|
+
token?: string;
|
|
724
|
+
/** Injectable for socket-free tests. Default: global `fetch`. */
|
|
725
|
+
fetch?: typeof fetch;
|
|
726
|
+
}): (live: LiveConversation[]) => Promise<LiveSetResponse> {
|
|
727
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
728
|
+
return async (live) => {
|
|
729
|
+
const url = new URL("/agents", options.baseUrl).toString();
|
|
730
|
+
const res = await fetchImpl(url, {
|
|
731
|
+
method: "PUT",
|
|
732
|
+
headers: {
|
|
733
|
+
"content-type": "application/json",
|
|
734
|
+
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
735
|
+
},
|
|
736
|
+
body: JSON.stringify({ live } satisfies LiveSetRequest),
|
|
737
|
+
}).catch((err: unknown) => {
|
|
738
|
+
throw new Error(`live-set statement to ${url} failed: ${transportDetail(err)}`, { cause: err });
|
|
739
|
+
});
|
|
740
|
+
if (!res.ok) {
|
|
741
|
+
throw new Error(`live-set statement failed (${res.status}): ${await errorDetail(res)}`);
|
|
742
|
+
}
|
|
743
|
+
const { freed } = (await res.json().catch(() => ({}))) as Partial<Record<keyof LiveSetResponse, unknown>>;
|
|
744
|
+
return { freed: typeof freed === "number" ? freed : 0 };
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Make an Agent: an actor slot that runs one Turn of a model each time a state invokes it. Put it
|
|
750
|
+
* under `actors` in `jr2Setup`, and invoke it by its key: `actors: { coder: agent(…) }`, then
|
|
751
|
+
* `invoke: { src: "coder", input: { prompt } }`. The key is the Agent's name.
|
|
752
|
+
*
|
|
753
|
+
* The invoke's `input` is the Turn: `prompt` (required), `cwd`, `model`, `thinkingLevel` and
|
|
754
|
+
* `continue`. `cwd` is the absolute directory where the Working tools work. Absent, it is the
|
|
755
|
+
* Worktree of the Workspace's one Repo Slot. `continue: true` continues the Agent's conversation
|
|
756
|
+
* in this Machine. Absent, each Turn starts a new conversation. `continue` is the literal `true`,
|
|
757
|
+
* not a `boolean`. A Turn can join only a conversation that an earlier Turn started with
|
|
758
|
+
* `continue: true`. Thus in a retry loop, put `continue: true` on each Turn, also on the first, so
|
|
759
|
+
* that attempt 2 sees attempt 1. Then a Runaway on the first Turn gets no second start, because a
|
|
760
|
+
* continued Turn gets none (ADR-0035). Spread it only when a state must start a clean conversation
|
|
761
|
+
* on some Turns only: `...(context.clean ? {} : { continue: true as const })`. A clean Turn does not
|
|
762
|
+
* start the continued conversation again: the next Turn with `continue: true` joins the earlier
|
|
763
|
+
* one. The Turn ends when the state that invoked the Agent exits, usually on a pick from the
|
|
764
|
+
* Agent's Menu.
|
|
765
|
+
*
|
|
766
|
+
* The actor of an Agent never finishes, so an `onDone` on its invoke never runs. Give the Agent a
|
|
767
|
+
* pick, such as `finish`, and route on it. When a Turn fails with no pick, jr2 sends the event
|
|
768
|
+
* `agent.fault` to the Machine. It is an event, not an error of the invoke, so an `onError` does
|
|
769
|
+
* not get it. Route it on each state that invokes an Agent, or once on a state above them:
|
|
770
|
+
* `on: { "agent.fault": "failed" }`. `jr2 check` and `jr2 up` refuse a state that invokes an
|
|
771
|
+
* Agent with no route for it, and an `agent.fault` that no transition takes ends the run `error`.
|
|
772
|
+
*
|
|
773
|
+
* An Agent with `workspace: "write"` or `"read"` works in a Sandbox, so invoke it inside a
|
|
774
|
+
* `workspace()` body. An Agent with `workspace: "none"` can run anywhere. A model can lack the
|
|
775
|
+
* definition's thinking level: see {@link ThinkingLevel}.
|
|
395
776
|
*
|
|
396
|
-
*
|
|
397
|
-
* `input
|
|
398
|
-
*
|
|
399
|
-
*
|
|
777
|
+
* When a Turn starts, it refuses:
|
|
778
|
+
* - a key in `input` that a Turn does not have: ``agent "<slot>": the Turn's input carries
|
|
779
|
+
* `<key>`, which a Turn has no use for — its input is its Frame (`prompt`, `cwd`), its Dials
|
|
780
|
+
* (`model`, `thinkingLevel`), and `continue` (ADR-0057)``
|
|
781
|
+
* - no `cwd` under a Workspace with more than one Repo Slot: ``turn <id>: agent "<slot>" works
|
|
782
|
+
* under a Workspace carrying more than one Repo Slot (<slots>) and its Turn framed no `cwd` —
|
|
783
|
+
* say which Worktree it works in (`cwd: context.workspace.repos.<slot>`, one of those), because
|
|
784
|
+
* the kit gives no slot a meaning (ADR-0051) and a Turn's Frame is where it works (ADR-0057)``
|
|
785
|
+
* - a `cwd` that is not an absolute path: ``agent "<slot>": the Turn's Frame carries a `cwd` that
|
|
786
|
+
* is not an absolute path — the Frame says WHERE this Turn works (ADR-0057), and a Turn rooted
|
|
787
|
+
* anywhere else would work in the wrong directory silently``. The Harness makes this refusal
|
|
788
|
+
* when it admits the Turn, and the error starts with `harness admission failed (400): `.
|
|
789
|
+
* - an Agent with Working tools outside a `workspace()`: ``turn <id>: agent "<slot>" has
|
|
790
|
+
* workspace: "<access>" — invoke it inside a workspace() (ambient resolution), or pass an
|
|
791
|
+
* explicit `endpoint` (workspace-less stub path)``
|
|
792
|
+
* - a `model` that is still `open`: ``agent "<slot>" has an Open model — nobody bound it; bind it
|
|
793
|
+
* where the Machine is registered (`customize(<machine>, { agents: { <slot>: { model:
|
|
794
|
+
* "<provider>/<model>" } } })`), because a packaged Machine cannot pick a model on your behalf
|
|
795
|
+
* (ADR-0054)``. `jr2 up` refuses this before it builds anything.
|
|
400
796
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
797
|
+
* @param declaration - The Agent's definition (see {@link AgentDefinition}). `model` may be `open`
|
|
798
|
+
* in a Machine that you publish as a package.
|
|
799
|
+
* @returns The actor logic of the Agent's slot.
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* ```ts
|
|
803
|
+
* import { z } from "zod";
|
|
804
|
+
* import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
|
|
805
|
+
*
|
|
806
|
+
* const finish = defineEvent({
|
|
807
|
+
* name: "finish",
|
|
808
|
+
* description: "Call this when `node --test` passes.",
|
|
809
|
+
* input: z.object({}),
|
|
810
|
+
* });
|
|
811
|
+
* const runInput = z.object({ branch: z.string() });
|
|
812
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
|
|
813
|
+
*
|
|
814
|
+
* const body = jr2Setup({
|
|
815
|
+
* types: {} as { input: BodyInput; context: BodyInput },
|
|
816
|
+
* events: [finish],
|
|
817
|
+
* actors: {
|
|
818
|
+
* coder: agent({
|
|
819
|
+
* model: "anthropic/claude-haiku-5-5",
|
|
820
|
+
* instructions: "You fix failing tests. You change no test.",
|
|
821
|
+
* }),
|
|
822
|
+
* fail: fail(),
|
|
823
|
+
* },
|
|
824
|
+
* }).createMachine({
|
|
825
|
+
* id: "fix",
|
|
826
|
+
* context: ({ input }) => input,
|
|
827
|
+
* on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
|
|
828
|
+
* initial: "coding",
|
|
829
|
+
* states: {
|
|
830
|
+
* coding: {
|
|
831
|
+
* invoke: {
|
|
832
|
+
* src: "coder",
|
|
833
|
+
* input: ({ context }) => ({
|
|
834
|
+
* prompt: "Make `node --test` pass, then commit.",
|
|
835
|
+
* cwd: context.workspace.repos.app,
|
|
836
|
+
* thinkingLevel: "high",
|
|
837
|
+
* }),
|
|
838
|
+
* },
|
|
839
|
+
* on: { finish: "done" },
|
|
840
|
+
* },
|
|
841
|
+
* done: { type: "final" },
|
|
842
|
+
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
843
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
844
|
+
* },
|
|
845
|
+
* });
|
|
846
|
+
*
|
|
847
|
+
* export const machine = workspace(body, {
|
|
848
|
+
* input: runInput,
|
|
849
|
+
* repos: { app: "https://github.com/you/app.git" },
|
|
850
|
+
* spec: ({ input }) => ({ branch: input.branch }),
|
|
851
|
+
* });
|
|
852
|
+
* ```
|
|
853
|
+
*
|
|
854
|
+
* @see docs/concepts/agents-and-turns.md
|
|
855
|
+
* @see docs/guides/write-a-workflow.md
|
|
856
|
+
* @see docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md
|
|
857
|
+
* @see docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md
|
|
404
858
|
*/
|
|
405
859
|
export function agent(declaration: AgentDeclaration): AgentLogic {
|
|
860
|
+
// THE authoring surface for an Agent (ADR-0049): one definition in, one actor slot out. The
|
|
861
|
+
// slot key is the Agent's name (the Harness route, the minted iid, the markers), so a name the
|
|
862
|
+
// Machine does not carry is a compile error on `src`, and two Machines in one run may each carry
|
|
863
|
+
// their own `coder`.
|
|
864
|
+
//
|
|
865
|
+
// Everything live is constructed per-invocation from serializable input: the wire client from
|
|
866
|
+
// `input.endpoint` (which Sandbox's Harness — or the wire-compatible dev stub; either way it's
|
|
867
|
+
// only a URL, one code path). Lives here, not in actor.ts, so the actor logic and its unit
|
|
868
|
+
// tests never touch the wire.
|
|
869
|
+
//
|
|
870
|
+
// It takes a DECLARATION (ADR-0054), so a packaged Machine may write `agent({ model: open, … })`
|
|
871
|
+
// and leave the one part it cannot honestly fill to whoever registers it. What rides the Turn
|
|
872
|
+
// is still an `AgentDefinition`: the actor narrows on start and refuses an Open one there.
|
|
406
873
|
return agentActorWith(
|
|
407
874
|
(endpoint, bearer) => createHarnessAgentRunClient({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) }),
|
|
408
875
|
declaration,
|
|
409
876
|
);
|
|
410
877
|
}
|
|
411
878
|
|
|
879
|
+
/**
|
|
880
|
+
* The Harness's exec (ADR-0067), as a `command` Effect drives it: `POST /exec/:id` to start,
|
|
881
|
+
* `GET /exec/:id` long-polled by offset, `POST /exec/:id/kill` when the Effect's state exits (sent
|
|
882
|
+
* again until the Harness keeps it), `DELETE /exec/:id` once the output is ledgered. A start that
|
|
883
|
+
* never reached the wire rejects `ExecNotSent`, the one start failure sent again: a Harness runs one
|
|
884
|
+
* id once only within its own process, so a start that reached one that then restarted would run
|
|
885
|
+
* twice. A read's transport failure or 5xx rejects, and the Effect asks again; a 404 on a read is
|
|
886
|
+
* an exec the Harness does not know (it restarted); any other 4xx is a jr2 defect, never retried.
|
|
887
|
+
*/
|
|
888
|
+
export function createExecPort(options: {
|
|
889
|
+
baseUrl: string;
|
|
890
|
+
/** The Workspace's Harness bearer (ADR-0058). Absent only against a Harness that checks nothing. */
|
|
891
|
+
token?: string;
|
|
892
|
+
/** Injectable for socket-free tests. Default: global `fetch`. */
|
|
893
|
+
fetch?: typeof fetch;
|
|
894
|
+
}): ExecPort {
|
|
895
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
896
|
+
const headers = {
|
|
897
|
+
"content-type": "application/json",
|
|
898
|
+
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
899
|
+
};
|
|
900
|
+
const url = (id: string, suffix = "") => new URL(`/exec/${encodeURIComponent(id)}${suffix}`, options.baseUrl);
|
|
901
|
+
const call = async (target: URL, init: RequestInit): Promise<Response> =>
|
|
902
|
+
fetchImpl(target.toString(), { ...init, headers }).catch((err: unknown) => {
|
|
903
|
+
throw new Error(`harness exec ${init.method ?? "GET"} ${target.pathname} failed: ${transportDetail(err)}`, {
|
|
904
|
+
cause: err,
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
const refusedOrRetry = async (res: Response, what: string): Promise<never> => {
|
|
908
|
+
const detail = await errorDetail(res);
|
|
909
|
+
if (res.status >= 400 && res.status < 500)
|
|
910
|
+
throw new ExecRefused(`harness refused the ${what} (${res.status}): ${detail}`);
|
|
911
|
+
throw new Error(`harness ${what} failed (${res.status}): ${detail}`);
|
|
912
|
+
};
|
|
913
|
+
return {
|
|
914
|
+
start: async (id, req: ExecRequest, signal) => {
|
|
915
|
+
const res = await call(url(id), { method: "POST", body: JSON.stringify(req), signal }).catch((err: unknown) => {
|
|
916
|
+
// Only a request that never reached the wire is safe to send again (see `startOnce`).
|
|
917
|
+
if (!signal.aborted && neverDelivered(err)) throw new ExecNotSent((err as Error).message, { cause: err });
|
|
918
|
+
throw err;
|
|
919
|
+
});
|
|
920
|
+
if (!res.ok) return refusedOrRetry(res, "exec");
|
|
921
|
+
await res.json().catch(() => undefined);
|
|
922
|
+
},
|
|
923
|
+
read: async (id, offset, signal) => {
|
|
924
|
+
const target = url(id);
|
|
925
|
+
if (offset !== undefined) target.searchParams.set("offset", String(offset));
|
|
926
|
+
target.searchParams.set("live", LIVE_LONG_POLL);
|
|
927
|
+
const res = await call(target, { method: "GET", signal });
|
|
928
|
+
if (res.status === 404) {
|
|
929
|
+
await res.body?.cancel();
|
|
930
|
+
return undefined;
|
|
931
|
+
}
|
|
932
|
+
if (!res.ok) return refusedOrRetry(res, "exec read");
|
|
933
|
+
return (await res.json()) as ExecRead;
|
|
934
|
+
},
|
|
935
|
+
kill: async (id) => {
|
|
936
|
+
// Not kept until the Harness answers 2xx: the Effect sends it again until then.
|
|
937
|
+
const res = await call(url(id, "/kill"), { method: "POST" });
|
|
938
|
+
if (!res.ok) return refusedOrRetry(res, "exec kill");
|
|
939
|
+
await res.body?.cancel();
|
|
940
|
+
},
|
|
941
|
+
forget: async (id) => {
|
|
942
|
+
const res = await call(url(id), { method: "DELETE" });
|
|
943
|
+
await res.body?.cancel();
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Make a `command` Effect: an actor slot that runs one program in the Workspace's Sandbox when a
|
|
950
|
+
* state invokes it. Put it under `actors` and invoke it by its key: `actors: { tests:
|
|
951
|
+
* command(["node", "--test"]) }`, then `invoke: { src: "tests", onDone: … }`.
|
|
952
|
+
*
|
|
953
|
+
* `argv` is a list, never shell text: each entry is one argument. An `arg("name")` entry is filled
|
|
954
|
+
* from the invoke's `input` (`input: { name: "…" }`), as one argument whatever its text. To use a
|
|
955
|
+
* shell, write it: `["sh", "-c", "…"]`.
|
|
956
|
+
*
|
|
957
|
+
* Where it runs:
|
|
958
|
+
* - With no Effect secret in `env`, in the Harness container of the enclosing Workspace's Sandbox.
|
|
959
|
+
* It runs in the Worktree of a Repo Slot, or in `/work` in a Workspace with no Repo Slot. The
|
|
960
|
+
* Agent's Working tools also run in this container.
|
|
961
|
+
* - With an Effect secret in `env`, in the Sandbox Effector, against a commit of the branch. This
|
|
962
|
+
* is the branch tip when it starts, or with `pin: true` the commit that the invoke's `input.sha`
|
|
963
|
+
* names. The output then says `dirty: true` when the Worktree held edits that the command did
|
|
964
|
+
* not see.
|
|
965
|
+
*
|
|
966
|
+
* Every outcome is output for `onDone`, never an error: `{ outcome: "exited", code, tail }`,
|
|
967
|
+
* `{ outcome: "timeout", tail }`, `{ outcome: "killed", signal, tail }`, or `{ outcome: "unknown",
|
|
968
|
+
* reason }` when jr2 cannot know what happened. `tail` is the end of the output stream.
|
|
969
|
+
*
|
|
970
|
+
* After an Orchestrator restart:
|
|
971
|
+
* - A command with no Effect secret in `env` never runs twice. jr2 attaches to it again, or settles
|
|
972
|
+
* `unknown`.
|
|
973
|
+
* - A command with an Effect secret in `env` runs again when the Sandbox Effector no longer holds
|
|
974
|
+
* it. It runs against the same commit. If jr2 has no commit for it, it settles `unknown`.
|
|
975
|
+
*
|
|
976
|
+
* When the state exits, the command stops: jr2 kills its process group. `onError` is only for a
|
|
977
|
+
* Machine or config defect.
|
|
978
|
+
*
|
|
979
|
+
* It refuses, when you call it:
|
|
980
|
+
* - an empty `argv`: `command(argv): argv is a non-empty list — each entry one argument, never
|
|
981
|
+
* shell text`
|
|
982
|
+
* - an entry that is not a string or `arg()`: `command(argv): an argv entry is a string or
|
|
983
|
+
* arg("name"), got <entry>`
|
|
984
|
+
* - a `timeout` that is not a positive duration: `command: timeout "<timeout>" is not a positive
|
|
985
|
+
* duration (like "90s", "10m", "1h")`
|
|
986
|
+
* - an `env` name that is not a variable name, or `PATH`, `HOME` or `TMPDIR`: ``command(argv, {
|
|
987
|
+
* env }): "<name>" is not a variable the author sets — PATH, HOME and TMPDIR are the Effector's
|
|
988
|
+
* (ADR-0067)``
|
|
989
|
+
* - `pin: true` with no Effect secret in `env`: ``command(argv, { pin: true }) names no secret in
|
|
990
|
+
* its env: `pin` chooses the commit a credentialed command runs against in the Sandbox
|
|
991
|
+
* Effector, and a command that names no secret runs in the live Worktree (ADR-0067) — name its
|
|
992
|
+
* secret, command(argv, { env: { NAME: secret("x") }, pin: true }), or drop `pin` ``
|
|
993
|
+
*
|
|
994
|
+
* And when it starts, with no Effect secret in `env`:
|
|
995
|
+
* - outside a `workspace()`: `command "<slot>" runs in the Harness container of a Workspace, and
|
|
996
|
+
* its state is not inside a workspace() — invoke it from a Workspace's body (ADR-0067)`
|
|
997
|
+
* - no `slot` under a Workspace with more than one Repo Slot: `command "<slot>" runs under a
|
|
998
|
+
* Workspace carrying more than one Repo Slot (<slots>) and names none — say which Worktree it
|
|
999
|
+
* runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`
|
|
1000
|
+
* - a `slot` that the Workspace does not have: `command "<slot>" names Repo Slot "<name>", which
|
|
1001
|
+
* its Workspace does not carry (<slots>)`
|
|
1002
|
+
* - an `arg()` with no string in `input`: `command "<slot>": argv slot <<name>> has no string
|
|
1003
|
+
* value in its input (got <type>) — map it in the invoke's input`. The angle brackets around the
|
|
1004
|
+
* arg's name are in the text: `argv slot <file>`. `<type>` is `nothing` when `input` has no such
|
|
1005
|
+
* field, or the type of its value, such as `number`.
|
|
1006
|
+
*
|
|
1007
|
+
* And when it starts, with an Effect secret in `env`:
|
|
1008
|
+
* - outside a `workspace()`: `command "<slot>" runs in the Sandbox Effector of a Workspace, and its
|
|
1009
|
+
* state is not inside a workspace() — invoke it from a Workspace's body (ADR-0067)`
|
|
1010
|
+
* - no `slot` under a Workspace with more than one Repo Slot: `command "<slot>" runs under a
|
|
1011
|
+
* Workspace carrying more than one Repo Slot (<slots>) — say which: { slot: "<slot>" }
|
|
1012
|
+
* (ADR-0051)`
|
|
1013
|
+
* - a Workspace with no Repo Slot: `command "<slot>" runs under a Workspace with no Repo Slot: a
|
|
1014
|
+
* command with an Effect secret runs against one commit of a Repo Slot's branch, and there is
|
|
1015
|
+
* none — declare a Repo Slot, or name no secret in its env, and it runs in /work beside the Agent
|
|
1016
|
+
* (ADR-0051)`. `jr2 up` refuses it first, with the same reason
|
|
1017
|
+
* - a `slot` that the Workspace does not have: `command "<slot>" names Repo Slot "<name>", which
|
|
1018
|
+
* its Workspace does not carry (<slots>)`
|
|
1019
|
+
* - `pin: true` and an `input.sha` that is not a full commit id: `command "<slot>" (pin): its
|
|
1020
|
+
* input's sha is not a full commit id`
|
|
1021
|
+
* - an `arg()` with no string in `input`: the same text as above.
|
|
1022
|
+
*
|
|
1023
|
+
* @param argv - The program and its arguments: strings and `arg("name")` entries. Or `open`, in a
|
|
1024
|
+
* Machine that you publish as a package, for the Instance that registers it to bind with
|
|
1025
|
+
* `customize()`.
|
|
1026
|
+
* @param options - Optional. `slot`: the Repo Slot whose Worktree the command runs in; needed only
|
|
1027
|
+
* where the Workspace has more than one. `timeout`: `"90s"`, `"10m"` or `"1h"`, default
|
|
1028
|
+
* `"10m"`; when it expires, the output is `timeout`. `tail`: how many characters of the end of
|
|
1029
|
+
* the output stream to keep, default 4096. `env`: environment variables. Each value is
|
|
1030
|
+
* `secret("name")` for the value of an Effect secret, or `secret("name").path` for the path of a
|
|
1031
|
+
* file that holds it. `pin`: `true` to run against the commit in `input.sha`. Use `pin` only
|
|
1032
|
+
* with an Effect secret in `env`.
|
|
1033
|
+
* @returns The actor logic of the Effect's slot. Its output is a `CommandOutput`.
|
|
1034
|
+
*
|
|
1035
|
+
* @example
|
|
1036
|
+
* ```ts
|
|
1037
|
+
* import { z } from "zod";
|
|
1038
|
+
* import { arg, command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
|
|
1039
|
+
*
|
|
1040
|
+
* const runInput = z.object({ branch: z.string() });
|
|
1041
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
|
|
1042
|
+
*
|
|
1043
|
+
* const body = jr2Setup({
|
|
1044
|
+
* types: {} as { input: BodyInput },
|
|
1045
|
+
* actors: {
|
|
1046
|
+
* tests: command(["node", "--test", arg("file")], { timeout: "5m" }),
|
|
1047
|
+
* fail: fail(),
|
|
1048
|
+
* },
|
|
1049
|
+
* }).createMachine({
|
|
1050
|
+
* id: "check",
|
|
1051
|
+
* on: { "workspace.lost": ".lost" },
|
|
1052
|
+
* initial: "testing",
|
|
1053
|
+
* states: {
|
|
1054
|
+
* testing: {
|
|
1055
|
+
* invoke: {
|
|
1056
|
+
* src: "tests",
|
|
1057
|
+
* input: { file: "test/greet.test.js" },
|
|
1058
|
+
* onDone: [
|
|
1059
|
+
* {
|
|
1060
|
+
* guard: ({ event }) => event.output.outcome === "exited" && event.output.code === 0,
|
|
1061
|
+
* target: "passed",
|
|
1062
|
+
* },
|
|
1063
|
+
* { target: "failed" },
|
|
1064
|
+
* ],
|
|
1065
|
+
* },
|
|
1066
|
+
* },
|
|
1067
|
+
* passed: { type: "final" },
|
|
1068
|
+
* // Not final states: each one fails the run, and the run ends `failed`.
|
|
1069
|
+
* failed: { invoke: { src: "fail", input: { reason: "the test did not pass" } } },
|
|
1070
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
1071
|
+
* },
|
|
1072
|
+
* });
|
|
1073
|
+
*
|
|
1074
|
+
* export const machine = workspace(body, {
|
|
1075
|
+
* input: runInput,
|
|
1076
|
+
* repos: { app: "https://github.com/you/app.git" },
|
|
1077
|
+
* spec: ({ input }) => ({ branch: input.branch }),
|
|
1078
|
+
* });
|
|
1079
|
+
* ```
|
|
1080
|
+
*
|
|
1081
|
+
* @see docs/concepts/effects.md
|
|
1082
|
+
* @see docs/guides/run-a-command-or-call-a-url.md
|
|
1083
|
+
* @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
|
|
1084
|
+
* @see docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md
|
|
1085
|
+
*/
|
|
1086
|
+
export function command<const A extends readonly (string | Arg)[], const Opt extends CommandOptions = CommandOptions>(
|
|
1087
|
+
argv: A,
|
|
1088
|
+
options?: Opt,
|
|
1089
|
+
): EffectLogic<CommandInput<A, Opt["pin"]>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
|
|
1090
|
+
/**
|
|
1091
|
+
* Make a `command` Effect whose argv is Open: `command(open, options)`, in a Machine that you
|
|
1092
|
+
* publish as a package. The Instance that registers the Machine binds the argv one time:
|
|
1093
|
+
* `customize(machine, { effects: { <slot>: { argv } } })`. {@link command} tells the rest, and
|
|
1094
|
+
* what it refuses.
|
|
1095
|
+
*
|
|
1096
|
+
* @param argv - `open`.
|
|
1097
|
+
* @param options - Optional. The same options as for a written argv.
|
|
1098
|
+
* @returns The actor logic of the Effect's slot. Its output is a `CommandOutput`.
|
|
1099
|
+
*
|
|
1100
|
+
* @see docs/guides/bind-open-parts.md
|
|
1101
|
+
* @see docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md
|
|
1102
|
+
*/
|
|
1103
|
+
export function command<const Opt extends CommandOptions = CommandOptions>(
|
|
1104
|
+
argv: typeof open,
|
|
1105
|
+
options?: Opt,
|
|
1106
|
+
): EffectLogic<OpenCommandInput<Opt["pin"]>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
|
|
1107
|
+
export function command(
|
|
1108
|
+
argv: readonly (string | Arg)[] | typeof open,
|
|
1109
|
+
options: CommandOptions = {},
|
|
1110
|
+
// The implementation of the overloads above: each one states the input type.
|
|
1111
|
+
): EffectLogic<any, CommandOutput> {
|
|
1112
|
+
// THE authoring surface for a `command` Effect (ADR-0067). `argv` is never shell text — an
|
|
1113
|
+
// `arg("x")` entry is ONE argument, filled from the invoke's input — and `sh -c` is the author's
|
|
1114
|
+
// explicit choice. Bound to the real exec port here; `commandWith` in effect.ts is the seam the
|
|
1115
|
+
// unit tests share.
|
|
1116
|
+
//
|
|
1117
|
+
// An Open argv (ADR-0054) is the composer's to bind with `customize()`; see open-effect.ts.
|
|
1118
|
+
return commandWith(
|
|
1119
|
+
(endpoint, bearer) => createExecPort({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) }),
|
|
1120
|
+
argv as readonly (string | Arg)[],
|
|
1121
|
+
options,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** What a draining Harness answers an admission (ADR-0031): refused before anything is queued. */
|
|
1126
|
+
const DRAINING = 503;
|
|
1127
|
+
|
|
412
1128
|
/** A stream read worth retrying (server hiccup) — internal to the reconnect loop, never thrown out. */
|
|
413
1129
|
class ReconnectableError extends Error {}
|
|
414
1130
|
|