@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/actor.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
// pod's Custodian (`/agents/<iid>/events` — ADR-0013), are validated by the table, and land on the
|
|
9
9
|
// state that invoked the agent — at any nesting depth, no routing, no `instanceId` on
|
|
10
10
|
// domain events (the closure IS the provenance). `/agents/<iid>/surface` serves exactly
|
|
11
|
-
// this registration,
|
|
12
|
-
//
|
|
13
|
-
// Harness re-
|
|
11
|
+
// this registration: the Agent's Menu, fixed for the conversation, and the Allowed picks,
|
|
12
|
+
// state-scoped by lifecycle (ADR-0029 — with no `list_changed` needed, per ADR-0013: the
|
|
13
|
+
// Harness re-reads per Submission).
|
|
14
14
|
//
|
|
15
15
|
// 2. ADMITS the run over the Harness at `input.endpoint` — the port is constructed
|
|
16
16
|
// per-invocation from serializable input (ADR-0007/0011 doctrine), and a dev stub is just
|
|
@@ -51,8 +51,14 @@ import {
|
|
|
51
51
|
type WorkspaceAccess,
|
|
52
52
|
} from "./agent.ts";
|
|
53
53
|
import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
|
|
54
|
+
import type { HarnessRestarts, MemoryKill } from "./workspace.ts";
|
|
54
55
|
import { INSTANCE_HARNESS_SERVICE } from "./names.ts";
|
|
55
|
-
import {
|
|
56
|
+
import { SENDER_KEY } from "@jr2/agent-protocol";
|
|
57
|
+
import { agentAddress, continuedIid, allowedPicks, resolveAccepts, runBindingOf } from "./registration.ts";
|
|
58
|
+
import type { NoticeScope } from "./notices.ts";
|
|
59
|
+
import { restOf } from "./rest.ts";
|
|
60
|
+
import { SUBMISSION_HARNESS_RESTARTED, SUBMISSION_RUNAWAY, type Notice, type PodMemoryKill } from "./wire.ts";
|
|
61
|
+
import type { AbortCause } from "./wire.ts";
|
|
56
62
|
|
|
57
63
|
/**
|
|
58
64
|
* One admitted Submission — the durable re-attach handle (ADR-0016). The wire fields are
|
|
@@ -75,6 +81,47 @@ export type AgentAdmission = {
|
|
|
75
81
|
* aborts the LIVE conversation, never the dead original the ledger is keyed by.
|
|
76
82
|
*/
|
|
77
83
|
instanceId?: string;
|
|
84
|
+
/**
|
|
85
|
+
* The Turn that ledgered this admission — the input's `turnId`, stamped by the ACTOR at ledger
|
|
86
|
+
* time. The ledger is keyed by `instanceId`, which a continued Turn shares with the Turn before
|
|
87
|
+
* it, so a restore compares this with the restored input's own: a mismatch is an admission of an
|
|
88
|
+
* EARLIER Turn, and the restored Turn was not admitted yet (ADR-0016).
|
|
89
|
+
*/
|
|
90
|
+
turnId?: string;
|
|
91
|
+
/**
|
|
92
|
+
* The Harness container's restart count when this admission was answered (ADR-0021) — stamped by
|
|
93
|
+
* the ACTOR, for a Turn in a Sandbox Workspace whose watch had spoken. A count above it means
|
|
94
|
+
* the process holding the conversation died; a restore holds the re-attached Turn to the same
|
|
95
|
+
* mark.
|
|
96
|
+
*/
|
|
97
|
+
harnessRestarts?: number;
|
|
98
|
+
/**
|
|
99
|
+
* When this admission's Turn started — stamped by the ACTOR on every admission of a Turn in a
|
|
100
|
+
* Workspace, so a restore reads back the real start, not the restore's: a kernel kill is held against it
|
|
101
|
+
* (ADR-0061), and a guard kill too when the Turn's first read of the pod's kills failed.
|
|
102
|
+
*/
|
|
103
|
+
turnStarted?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The newest guard kill in the pod's list when the Turn's first read of it was answered
|
|
106
|
+
* (ADR-0062), or {@link NO_KILLS} for an empty list — stamped by the ACTOR on every admission of
|
|
107
|
+
* a Turn in a Workspace. A nudge or a wake carries only kills after it, so it never tells the Agent of a kill
|
|
108
|
+
* from before its Turn. Absent when that first read failed.
|
|
109
|
+
*/
|
|
110
|
+
killMark?: string;
|
|
111
|
+
/**
|
|
112
|
+
* The Turn's task, its Frame's prompt — stamped by the ACTOR on every admission of the Turn,
|
|
113
|
+
* because a restore drops `prompt` from the input: a re-admission onto a conversation the
|
|
114
|
+
* Harness lost must send the task again, and has no other copy of it (ADR-0062).
|
|
115
|
+
*/
|
|
116
|
+
task?: string;
|
|
117
|
+
/**
|
|
118
|
+
* The Agent and the Harness base URL this admission was answered by — stamped by the ACTOR at
|
|
119
|
+
* ledger time, like `instanceId`. Together with `instanceId` they name the conversation on its
|
|
120
|
+
* Harness, which is what the live set is built from (ADR-0031): the host states the Instance
|
|
121
|
+
* Harness's live conversations from these ledger entries alone.
|
|
122
|
+
*/
|
|
123
|
+
agent?: string;
|
|
124
|
+
endpoint?: string;
|
|
78
125
|
};
|
|
79
126
|
|
|
80
127
|
/**
|
|
@@ -117,6 +164,8 @@ export type AgentTurnInput = {
|
|
|
117
164
|
* ADR-0028's containment claim, which per-invocation escalation would void.
|
|
118
165
|
*/
|
|
119
166
|
model?: string;
|
|
167
|
+
/** Omitted → the definition's, else `off`. A model with no `off` runs at the level its provider
|
|
168
|
+
* chooses; the printed conversation names it. */
|
|
120
169
|
thinkingLevel?: ThinkingLevel;
|
|
121
170
|
/**
|
|
122
171
|
* Continue this Agent's ONE conversation in this Machine instance (ADR-0016, ADR-0057) — the
|
|
@@ -131,7 +180,8 @@ export type AgentTurnInput = {
|
|
|
131
180
|
* share a conversation (the name is in the id), and two Pool children never collide (each
|
|
132
181
|
* worker is its own Machine instance). A conversation jr2 has faulted is never continued: the
|
|
133
182
|
* next `continue` lands on a virgin one (ADR-0035, ADR-0057) — and re-briefing that Turn is the
|
|
134
|
-
* author's, because jr2 cannot write that prompt.
|
|
183
|
+
* author's, because jr2 cannot write that prompt. A Harness restart is the exception: it cut the
|
|
184
|
+
* Turn, and the rebuilt Harness holds the conversation (ADR-0031).
|
|
135
185
|
*/
|
|
136
186
|
continue?: true;
|
|
137
187
|
};
|
|
@@ -161,6 +211,14 @@ export type AgentRunInput = {
|
|
|
161
211
|
* is what the Harness route, the minted iid, the markers and the telemetry all name. */
|
|
162
212
|
agentName: string;
|
|
163
213
|
instanceId: string;
|
|
214
|
+
/**
|
|
215
|
+
* This Turn's Turn ID — minted by the input mapper, one per invocation, and persisted with the
|
|
216
|
+
* input (ADR-0016). Two Turns that continue one conversation share `instanceId`, the ledger's
|
|
217
|
+
* key, and not this: the actor stamps it on every admission it ledgers, and a restore
|
|
218
|
+
* re-attaches only an admission that this Turn ledgered. Absent (a plain `setup()` machine that
|
|
219
|
+
* passes finalized input), the key alone decides, as before.
|
|
220
|
+
*/
|
|
221
|
+
turnId?: string;
|
|
164
222
|
/**
|
|
165
223
|
* The Harness base URL, EXPLICIT (ADR-0016): only for a workspace-less run (the mechanics
|
|
166
224
|
* tier's stub Harness, a dev endpoint — just a URL, ADR-0011). Inside a `workspace()` leave it
|
|
@@ -202,17 +260,38 @@ export type AgentRunInput = {
|
|
|
202
260
|
* fresh invocation, which already mints its own conversation.
|
|
203
261
|
*/
|
|
204
262
|
continuation?: boolean;
|
|
205
|
-
/** Event names (from the invoking Machine's vocabulary) this invocation accepts
|
|
263
|
+
/** Event names (from the invoking Machine's vocabulary) this invocation accepts: the invoking
|
|
264
|
+
* state's derived set, which delivery validates against and the Allowed picks narrow. */
|
|
206
265
|
tools: readonly string[];
|
|
266
|
+
/**
|
|
267
|
+
* The Agent's MENU (ADR-0029): the union of `tools` over every state of this Machine that invokes
|
|
268
|
+
* an Agent of this name, in a fixed order — what the surface serves as the tools block, the same
|
|
269
|
+
* for every Turn of the conversation. `jr2Setup` always sets it; absent (a plain `setup()`
|
|
270
|
+
* machine passing finalized input), `tools` is the Menu.
|
|
271
|
+
*/
|
|
272
|
+
menu?: readonly string[];
|
|
207
273
|
};
|
|
208
274
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
275
|
+
// Sent when the run is out of options: the Submission settled failed/aborted (infra fault after
|
|
276
|
+
// the turn's own provider retries — ADR-0027), the no-signal nudge budget ran dry, or the runaway
|
|
277
|
+
// reroll budget did (ADR-0035). The ONE terminal event (ADR-0016) — where it routes is workflow
|
|
278
|
+
// policy.
|
|
279
|
+
/** jr2 sends `agent.fault` to the Machine when an Agent's Turn cannot go on: the Harness failed
|
|
280
|
+
* the Turn, or the Agent ended its Turns with no pick after every nudge. The Machine decides what
|
|
281
|
+
* to do next. Route it in the state that invokes the Agent, or in a state above it, with
|
|
282
|
+
* `on: { "agent.fault": … }`; one route on the root is a route for each state. `jr2 check` and
|
|
283
|
+
* `jr2 up` refuse a state that invokes an Agent, or an Open body that can run one, with no route for
|
|
284
|
+
* it. When the event comes and no transition takes it (the route's guard is false), the run ends
|
|
285
|
+
* `error`, with the event, the state and the reason in its fault. In a Chart the route is optional:
|
|
286
|
+
* when no transition of the Chart takes the event, the Open body sends it, unchanged, to the Machine
|
|
287
|
+
* that invoked the body, whose route takes it. */
|
|
213
288
|
export type FaultTelemetry = {
|
|
214
289
|
type: "agent.fault";
|
|
290
|
+
/** The Instance ID of the Agent's conversation that failed: the same id on each Turn that
|
|
291
|
+
* continues it. */
|
|
215
292
|
instanceId: string;
|
|
293
|
+
/** Why the Turn failed, as text for a person or a log: the Harness's error, or that the Agent
|
|
294
|
+
* ended its Turns without a pick after every nudge. Route on the event, not on this text. */
|
|
216
295
|
reason: string;
|
|
217
296
|
};
|
|
218
297
|
|
|
@@ -238,24 +317,104 @@ export interface AgentRunPort {
|
|
|
238
317
|
admit(input: AgentRunInput, opts: AgentAdmitOptions): Promise<AgentAdmission>;
|
|
239
318
|
/**
|
|
240
319
|
* Follow an admitted submission until it settles. Resolving means the submission completed;
|
|
241
|
-
* rejecting means it settled failed/aborted (or the conversation is gone).
|
|
320
|
+
* rejecting means it settled failed/ended/aborted (or the conversation is gone).
|
|
242
321
|
*/
|
|
243
|
-
settle(admission: AgentAdmission, opts?:
|
|
322
|
+
settle(admission: AgentAdmission, opts?: AgentSettleOptions): Promise<void>;
|
|
323
|
+
/**
|
|
324
|
+
* Does the Harness still hold this conversation (ADR-0062)? Asked before a continued Turn whose
|
|
325
|
+
* conversation was admitted before: a Harness that restarted between the Turns lost it with no
|
|
326
|
+
* fault to say so, and the Turn is told its conversation is new. Answers `true` whenever it
|
|
327
|
+
* cannot tell — unknown is never loss. A wrong `true` is caught at the admission that follows:
|
|
328
|
+
* it says `kept`, and a Harness that keeps no such conversation refuses it.
|
|
329
|
+
*/
|
|
330
|
+
holds(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<boolean>;
|
|
331
|
+
/**
|
|
332
|
+
* The pod's guard kills (ADR-0061, ADR-0062), every one with `told`: the conversations whose
|
|
333
|
+
* running `bash` call answered its verdict. A stream is read only while a Submission's wait
|
|
334
|
+
* runs, and a kill while none runs, or after the wait stopped, reaches no reader there. Asked before a Turn's admission into a
|
|
335
|
+
* Workspace. A read changes nothing on the Harness; the notice ledger keeps each kill's id.
|
|
336
|
+
* Answers `undefined` whenever it cannot tell — not `[]`, which says the list is empty — and the
|
|
337
|
+
* kill stays on the Harness for the next read.
|
|
338
|
+
*/
|
|
339
|
+
memoryKills(opts?: { signal?: AbortSignal }): Promise<ListedKill[] | undefined>;
|
|
244
340
|
/**
|
|
245
341
|
* End the instance's in-flight (and queued) work — the turn is over (ADR-0024). Resolving means
|
|
246
342
|
* the intent is RECORDED, not that the submission has settled; jr2 never observes that outcome,
|
|
247
343
|
* because the actor is already stopped by the time this is called. The actor passes no `signal`
|
|
248
344
|
* for exactly that reason — its own controller is already aborted — but the option is here for
|
|
249
|
-
* parity with the other two.
|
|
345
|
+
* parity with the other two. `cause: "pick"` says the Turn's own pick ended it, so the Harness
|
|
346
|
+
* settles the Submission `ended`; with no cause it settles `aborted`.
|
|
250
347
|
*/
|
|
251
|
-
abort(
|
|
348
|
+
abort(
|
|
349
|
+
agentName: string,
|
|
350
|
+
instanceId: string,
|
|
351
|
+
opts?: { signal?: AbortSignal; cause?: AbortCause; submissionId?: string },
|
|
352
|
+
): Promise<void>;
|
|
252
353
|
}
|
|
253
354
|
|
|
254
355
|
/** What every admission carries beside the input: the Agent definition this Turn runs (ADR-0049),
|
|
255
356
|
* plus the local abandon signal. Not part of {@link AgentRunInput} on purpose — that shape is
|
|
256
357
|
* PERSISTED as the child's input, and a definition frozen into a snapshot would outlive the
|
|
257
358
|
* Machine edit that changed it. */
|
|
258
|
-
export type AgentAdmitOptions = {
|
|
359
|
+
export type AgentAdmitOptions = {
|
|
360
|
+
definition: AgentDefinition;
|
|
361
|
+
signal?: AbortSignal;
|
|
362
|
+
/** The pending notices in this admission's scope (ADR-0062), for the Briefing's Turn part. Not
|
|
363
|
+
* on the persisted input either: they are the Orchestrator's, and delivered once. */
|
|
364
|
+
notices?: Notice[];
|
|
365
|
+
/** The admission is for a conversation that `holds` said the Harness keeps (ADR-0062): the
|
|
366
|
+
* re-admission after a cut, and a continued Turn. That "kept" can be a timeout's, so a Harness
|
|
367
|
+
* that keeps none refuses it and seats none; `admit` then rejects with an error whose `notKept`
|
|
368
|
+
* is true (the wire client's `ConversationNotKept`). */
|
|
369
|
+
kept?: boolean;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/** A memory kill by the Harness's own guard (ADR-0061, layer 4), as the Orchestrator reads it on
|
|
373
|
+
* an updates stream — the twin of {@link ListedKill}, which it reads in the pod's list:
|
|
374
|
+
* the guard's `peak` and `limit` — absent when what the stream said was not a size — and the
|
|
375
|
+
* kill's `id`, the same on every stream that carries it and in the pod's list
|
|
376
|
+
* (`AgentRunPort.memoryKills`), so every witness of one kill is one notice. `legacy`: an older
|
|
377
|
+
* Harness's event, which has no id; `id` is then the source the Orchestrator raised such an event
|
|
378
|
+
* with before, its place on the stream, so a replayed read is still the same kill. */
|
|
379
|
+
export type StreamedKill = { id: string; legacy?: true; peak?: string; limit?: string };
|
|
380
|
+
|
|
381
|
+
/** A kill read from the pod's list, as the port answers it: the wire's, but `peak` and `limit` are
|
|
382
|
+
* absent when the list's were not sizes — the kill is kept, and worded with no sizes (ADR-0062). */
|
|
383
|
+
export type ListedKill = Omit<PodMemoryKill, "peak" | "limit"> & { peak?: string; limit?: string };
|
|
384
|
+
|
|
385
|
+
/** The source a guard kill's notice is raised with: the Workspace and the kill's id. Both paths use
|
|
386
|
+
* it — the stream and the pod's list — so the ledger's `seen` makes them one notice. */
|
|
387
|
+
const guardSource = (sandbox: string, id: string): string => `${sandbox}@guard:${id}`;
|
|
388
|
+
|
|
389
|
+
/** The source a kernel kill's notice is raised with (ADR-0061, layer 5): the Workspace and when the
|
|
390
|
+
* Harness container ended, which names that one end. Both paths use it — the fault of a Turn the
|
|
391
|
+
* kill ended, and the read before an admission — so the ledger's `seen` makes them one notice. */
|
|
392
|
+
const kernelSource = (sandbox: string, at: string): string => `${sandbox}@${at}`;
|
|
393
|
+
|
|
394
|
+
/** How long the read of the pod's kills may take before it counts as none. */
|
|
395
|
+
const MEMORY_KILLS_READ_MS = 5_000;
|
|
396
|
+
|
|
397
|
+
/** How long "does the Harness still hold this conversation?" is asked again before the answer is
|
|
398
|
+
* taken as "held" — unknown is never loss, but a container that is starting is not unknown yet. */
|
|
399
|
+
const HOLDS_READ_MS = 30_000;
|
|
400
|
+
|
|
401
|
+
/** A `killMark` for a list that was empty: every kill read after it is the Turn's. Kill ids start
|
|
402
|
+
* `k-`, so it is never one. */
|
|
403
|
+
export const NO_KILLS = "none";
|
|
404
|
+
|
|
405
|
+
/** How far the pod's clock may run behind this one when a kill's `at` is held against a Turn's
|
|
406
|
+
* start — only when the Turn has no `killMark`: a kill stamped up to this much before the start
|
|
407
|
+
* still counts as in the Turn, so a slow pod clock does not hide a kill of this Turn. The two
|
|
408
|
+
* clocks are a node's and the Orchestrator's, both kept by the cluster's time service; one second
|
|
409
|
+
* is far more than they drift, and far less than a Turn. */
|
|
410
|
+
const KILL_CLOCK_SKEW_MS = 1_000;
|
|
411
|
+
|
|
412
|
+
/** What following a settlement may be told beside the settlement itself. */
|
|
413
|
+
export type AgentSettleOptions = {
|
|
414
|
+
signal?: AbortSignal;
|
|
415
|
+
/** The Harness's guard killed the Agent's processes during this Submission (ADR-0061). */
|
|
416
|
+
onMemoryLimit?: (kill: StreamedKill) => void;
|
|
417
|
+
};
|
|
259
418
|
|
|
260
419
|
/** Build a port for one invocation from its serializable input (ADR-0011 static-import doctrine),
|
|
261
420
|
* plus the Harness bearer for the placement that input resolved to (ADR-0058) — absent where no
|
|
@@ -281,30 +440,114 @@ export type AgentRunOptions = {
|
|
|
281
440
|
* pathological, which belongs with the workflow's fault routing.
|
|
282
441
|
*/
|
|
283
442
|
runawayBudget?: number;
|
|
443
|
+
/**
|
|
444
|
+
* How long the read of the pod's guard kills (ADR-0062) may take before it counts as none and
|
|
445
|
+
* the Turn is admitted without it. Default 5000 ms; a seam for tests, never an author dial.
|
|
446
|
+
*/
|
|
447
|
+
memoryKillsReadMs?: number;
|
|
448
|
+
/**
|
|
449
|
+
* How long the actor asks whether a restarted Harness still holds a conversation before it
|
|
450
|
+
* takes "held" for an answer: the new container may not listen yet (ADR-0062). Default 30 s; a
|
|
451
|
+
* seam for tests, never an author dial.
|
|
452
|
+
*/
|
|
453
|
+
holdsReadMs?: number;
|
|
284
454
|
};
|
|
285
455
|
|
|
286
456
|
/**
|
|
287
457
|
* The runaway settlement class (ADR-0035), read STRUCTURALLY off a settle rejection: the wire
|
|
288
|
-
* client's `SettlementFault` carries the Settlement, but this module
|
|
289
|
-
* so the
|
|
290
|
-
*
|
|
291
|
-
*
|
|
458
|
+
* client's `SettlementFault` carries the Settlement, but this module never loads the wire client
|
|
459
|
+
* (see header), so the shape is duck-typed. The literal is the wire's own `SUBMISSION_RUNAWAY` —
|
|
460
|
+
* `./wire.ts` is shapes and constants, nothing a test load path pays for — and wire-types.test.ts
|
|
461
|
+
* holds it to the Harness's. A lost conversation (404) carries no settlement, so it stays
|
|
462
|
+
* terminal like every other class.
|
|
292
463
|
*/
|
|
293
464
|
function runawayReason(err: unknown): string | undefined {
|
|
294
465
|
if (typeof err !== "object" || err === null) return undefined;
|
|
295
466
|
const error = (err as { settlement?: { error?: { type?: string; message?: string } } }).settlement?.error;
|
|
296
|
-
return error?.type ===
|
|
467
|
+
return error?.type === SUBMISSION_RUNAWAY ? (error.message ?? SUBMISSION_RUNAWAY) : undefined;
|
|
297
468
|
}
|
|
298
469
|
|
|
299
|
-
/** The
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
470
|
+
/** The Harness's fixed prefix for a provider that refused the load (ADR-0064), restated like the
|
|
471
|
+
* runaway literal: `PROVIDER_LIMIT` in `@jr2/harness`. */
|
|
472
|
+
const PROVIDER_LIMIT = "provider limit";
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* A provider limit (ADR-0064), read STRUCTURALLY like {@link runawayReason}: the Settlement's own
|
|
476
|
+
* message, unwrapped, so the fault reason STARTS with the fixed prefix — as a memory kill's does
|
|
477
|
+
* (ADR-0061) — and the Machine and `jr2 status` can key on it.
|
|
478
|
+
*/
|
|
479
|
+
function providerLimitReason(err: unknown): string | undefined {
|
|
480
|
+
if (typeof err !== "object" || err === null) return undefined;
|
|
481
|
+
const message = (err as { settlement?: { error?: { message?: unknown } } }).settlement?.error?.message;
|
|
482
|
+
return typeof message === "string" && message.startsWith(PROVIDER_LIMIT) ? message : undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** The kubelet's reason for a container the kernel killed at its memory limit (ADR-0061). */
|
|
486
|
+
const OOM_KILLED = "OOMKilled";
|
|
487
|
+
|
|
488
|
+
/** Why a Turn failed when its Harness restarted under it (ADR-0021), in the kubelet's words. */
|
|
489
|
+
function harnessRestartReason(seen: HarnessRestarts): string {
|
|
490
|
+
const last = seen.lastTerminated;
|
|
491
|
+
return `Turn lost (Harness restarted: ${last?.reason ?? "unknown"}, exit ${last?.exitCode ?? "?"})`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* A Submission the Harness settled `failed` because it restarted under it (ADR-0031): the rebuilt
|
|
496
|
+
* stream says so, so the Orchestrator reads a Settlement, not a 404. Read STRUCTURALLY off the
|
|
497
|
+
* Settlement's typed error, like {@link runawayReason} — the wire's `SUBMISSION_HARNESS_RESTARTED`,
|
|
498
|
+
* held to the Harness's by wire-types.test.ts — and the message is only what the fault quotes.
|
|
499
|
+
* It can reach the actor BEFORE the watch's restart count does, and it is the only restart signal
|
|
500
|
+
* an Instance Harness gives — the watch does not see that pod.
|
|
501
|
+
*/
|
|
502
|
+
function settledByRestart(err: unknown): string | undefined {
|
|
503
|
+
if (typeof err !== "object" || err === null) return undefined;
|
|
504
|
+
const settlement = (err as { settlement?: { outcome?: unknown; error?: { type?: unknown; message?: unknown } } })
|
|
505
|
+
.settlement;
|
|
506
|
+
if (settlement?.outcome !== "failed" || settlement.error?.type !== SUBMISSION_HARNESS_RESTARTED) return undefined;
|
|
507
|
+
const message = settlement.error.message;
|
|
508
|
+
return typeof message === "string" ? message : "Harness restarted";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const errorMessage = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* A lost conversation (ADR-0027), read STRUCTURALLY like {@link runawayReason}: the wire client's
|
|
515
|
+
* `SettlementFault` marks a 404 `lost`, and this module is wire-free. The one fault class a
|
|
516
|
+
* memory kill can hide behind (ADR-0061).
|
|
517
|
+
*/
|
|
518
|
+
function isLostConversation(err: unknown): boolean {
|
|
519
|
+
return typeof err === "object" && err !== null && (err as { lost?: unknown }).lost === true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** An admission for a kept conversation that the Harness refused: it keeps none (ADR-0062). Read
|
|
523
|
+
* structurally, as {@link isLostConversation} is: the wire client's `ConversationNotKept`. */
|
|
524
|
+
function isNotKept(err: unknown): boolean {
|
|
525
|
+
return typeof err === "object" && err !== null && (err as { notKept?: unknown }).notKept === true;
|
|
306
526
|
}
|
|
307
527
|
|
|
528
|
+
/**
|
|
529
|
+
* The nudge (ADR-0006, absorbed here by ADR-0016). It names no picks: the Harness writes the Turn
|
|
530
|
+
* part after it, stating the Allowed picks in the model's own tool names and narrowed by the guards
|
|
531
|
+
* (ADR-0029, ADR-0062) — a list here would be a second one, in other names, that a closed guard
|
|
532
|
+
* makes wrong. Its first sentence stays true after a pick that left the Turn open, and for the
|
|
533
|
+
* wake: the ask-again rule asks whenever picks are allowed after a settle, picked or not, at once
|
|
534
|
+
* or when they open.
|
|
535
|
+
*/
|
|
536
|
+
const NUDGE_PROMPT =
|
|
537
|
+
`Your previous turn ended, and the workflow is still waiting for your pick. ` +
|
|
538
|
+
`You MUST end your turn by calling exactly one of the workflow tools allowed below. ` +
|
|
539
|
+
`Pick the one that matches the true state of your work and call it now.`;
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The re-admission after a Harness restart cut the Turn while the workflow was busy (ADR-0031). The
|
|
543
|
+
* Harness writes a cut call's history result itself ("its result comes in the next message") and the
|
|
544
|
+
* answer rides this admission as a `pick-answer` notice in the Briefing's Turn part, with the Allowed
|
|
545
|
+
* picks after it; this prompt says only what happened to the Turn.
|
|
546
|
+
*/
|
|
547
|
+
const READMIT_PROMPT =
|
|
548
|
+
`Your previous turn was cut: the process that ran you restarted while the workflow was busy. ` +
|
|
549
|
+
`The workflow did not stop. Continue your task from here.`;
|
|
550
|
+
|
|
308
551
|
/**
|
|
309
552
|
* Where this Turn works — the Frame's other half (ADR-0057), resolved before admission.
|
|
310
553
|
*
|
|
@@ -316,8 +559,8 @@ function nudgePrompt(tools: readonly string[]): string {
|
|
|
316
559
|
* slot's Worktree, which privileges nothing because there is nothing to choose between (ADR-0051);
|
|
317
560
|
* a Workspace with more REFUSES the Turn, naming the slots in declaration order and the line that
|
|
318
561
|
* ends it, because the kit gives no slot a meaning and a wrong guess is the silent failure
|
|
319
|
-
* ADR-0057 was written for; and a
|
|
320
|
-
* `/work
|
|
562
|
+
* ADR-0057 was written for; and a Workspace with no Repo Slot, or a run outside any Workspace,
|
|
563
|
+
* frames nothing, so the Harness keeps `/work`, the folder a slotless Workspace's steps share.
|
|
321
564
|
*
|
|
322
565
|
* Resolution counts SLOTS, and a slot's Worktree is the writable branch checkout. The detached
|
|
323
566
|
* review Worktree (`AmbientHandles.review`, ADR-0028's reviewer seat) is never resolved to: it is
|
|
@@ -356,7 +599,10 @@ function frameCwd(
|
|
|
356
599
|
*
|
|
357
600
|
* The brand is a RUNTIME property, read back through `isAgent`, and deliberately NOT part of this
|
|
358
601
|
* type: the unit-test seam is `provide({ actors: { coder: fake } })` (ADR-0049), and a required
|
|
359
|
-
* `definition` here would make every fake carry a definition it never uses.
|
|
602
|
+
* `definition` here would make every fake carry a definition it never uses.
|
|
603
|
+
*
|
|
604
|
+
* @internal
|
|
605
|
+
*/
|
|
360
606
|
export type AgentLogic = CallbackActorLogic<
|
|
361
607
|
AgentRunReceiveEvent,
|
|
362
608
|
AgentTurnInput | (AgentTurnInput & AgentTurnPlacement) | AgentRunInput
|
|
@@ -385,6 +631,8 @@ export function agentActorWith(
|
|
|
385
631
|
): AgentLogic {
|
|
386
632
|
const nudgeBudget = options.nudgeBudget ?? 2;
|
|
387
633
|
const runawayBudget = options.runawayBudget ?? 1;
|
|
634
|
+
const memoryKillsReadMs = options.memoryKillsReadMs ?? MEMORY_KILLS_READ_MS;
|
|
635
|
+
const holdsReadMs = options.holdsReadMs ?? HOLDS_READ_MS;
|
|
388
636
|
// Typed as the union so BOTH shapes typecheck on an invoke: jr2Setup machines write
|
|
389
637
|
// AgentTurnInput (and the config wrapper finalizes it before the actor ever runs); plain
|
|
390
638
|
// setup() machines must pass the finalized shape themselves — checked loudly below.
|
|
@@ -460,8 +708,10 @@ export function agentActorWith(
|
|
|
460
708
|
// Register this invocation's event surface (throws on a name outside the INVOKING MACHINE's
|
|
461
709
|
// vocabulary — ADR-0011's invoke-time check, scoped to `self._parent.logic` because names are
|
|
462
710
|
// per-Machine — which errors the run loudly at the invoking state).
|
|
463
|
-
// `signaled
|
|
464
|
-
//
|
|
711
|
+
// `signaled`: a Menu pick was delivered in this invocation. It closes the runaway REROLL gate
|
|
712
|
+
// (below): replaying the identical prompt would deliver it again. It does not decide the nudge
|
|
713
|
+
// — the ask-again rule does, from the Allowed picks after the settle, because a delivered pick
|
|
714
|
+
// may leave the Turn open (a mode change, a pick with no target). ONE invocation can hold
|
|
465
715
|
// more than one surface: a runaway reroll (ADR-0035) is a fresh conversation whose menu
|
|
466
716
|
// dials `/mcp/<derived iid>`, so its address must be live too — same defs, same deliver,
|
|
467
717
|
// because whichever conversation answers, it answers THIS invocation.
|
|
@@ -473,7 +723,17 @@ export function agentActorWith(
|
|
|
473
723
|
// already ended.
|
|
474
724
|
let currentIid = input.attach?.instanceId ?? instanceId;
|
|
475
725
|
const defs = resolveAccepts(self, input.tools);
|
|
726
|
+
// The Menu (ADR-0029): resolved against the same vocabulary, and served as the tools block.
|
|
727
|
+
const menu = resolveAccepts(self, input.menu ?? input.tools);
|
|
476
728
|
const disposers: Array<() => void> = [];
|
|
729
|
+
// Who this Turn's picks are from (ADR-0015): the slot, written on every event it delivers; and
|
|
730
|
+
// the invoke id that finds its state in the Machine. Both static, so a restore recomputes them.
|
|
731
|
+
const seat = { sender: { party: "agent", id: input.agentName } as const, invokeId: self.id };
|
|
732
|
+
// This Agent's continued conversation in this Machine (ADR-0057) — the base id the epoch ledger
|
|
733
|
+
// burns. This Turn is PART of it only when it continues it: a fresh Turn is new by definition.
|
|
734
|
+
const conversation = continuedIid(self._parent, binding.runId, input.agentName);
|
|
735
|
+
// The iids whose surface the Harness has read (see `carried` below).
|
|
736
|
+
const served = new Set<string>();
|
|
477
737
|
const registerSurface = (iid: string) =>
|
|
478
738
|
disposers.push(
|
|
479
739
|
binding.table.register({
|
|
@@ -482,13 +742,15 @@ export function agentActorWith(
|
|
|
482
742
|
kind: "agent",
|
|
483
743
|
id: iid,
|
|
484
744
|
defs,
|
|
745
|
+
menu,
|
|
485
746
|
sandbox,
|
|
486
747
|
deliver: (event) => {
|
|
487
748
|
signaled = true;
|
|
488
749
|
// The settlement-pick marker (ADR-0023), BEFORE the delivery moves the Machine: the pick
|
|
489
750
|
// must land on the feed ahead of the status delta it causes, or the narrative reads
|
|
490
|
-
// effect-then-cause.
|
|
491
|
-
|
|
751
|
+
// effect-then-cause. The sender is the Orchestrator's field, not the pick's payload: the
|
|
752
|
+
// printed conversation shows what the Agent picked, nothing more.
|
|
753
|
+
const { type, [SENDER_KEY]: _sender, ...payload } = event;
|
|
492
754
|
binding.marker?.({
|
|
493
755
|
kind: "pick",
|
|
494
756
|
agent: input.agentName,
|
|
@@ -498,10 +760,16 @@ export function agentActorWith(
|
|
|
498
760
|
});
|
|
499
761
|
sendBack(event);
|
|
500
762
|
},
|
|
763
|
+
// The Harness read this Turn's surface: it has the Turn in hand, and prompts it.
|
|
764
|
+
served: () => served.add(iid),
|
|
501
765
|
// The state that invoked us — the machine the menu derived from, so the only one whose
|
|
502
766
|
// guards can say whether a pick would move anything (ADR-0029). Same `_parent` the ambient
|
|
503
767
|
// walk above uses; structural, so a sibling's snapshot is unreachable.
|
|
504
768
|
invoker: self._parent,
|
|
769
|
+
...seat,
|
|
770
|
+
// Where a pick's answer goes when its route leaves the Agent's state (ADR-0024): the
|
|
771
|
+
// conversation this Turn continues, and none for a fresh Turn — no later Turn heard it.
|
|
772
|
+
...(input.continuation ? { conversation } : {}),
|
|
505
773
|
}),
|
|
506
774
|
);
|
|
507
775
|
registerSurface(currentIid);
|
|
@@ -510,19 +778,342 @@ export function agentActorWith(
|
|
|
510
778
|
// delivery scope: the pod hosting this Turn. One name, both directions — the Custodian there may
|
|
511
779
|
// speak for these picks, and only this bearer may drive the conversation there.
|
|
512
780
|
const client = portFactory(endpoint, sandbox === undefined ? undefined : binding.harnessBearer?.(sandbox));
|
|
513
|
-
|
|
781
|
+
// When this Turn started: a memory kill that ended the Harness before it is not this Turn's
|
|
782
|
+
// (ADR-0061). Undefined from a backend that cannot see the container — and never throws:
|
|
783
|
+
// naming the fault is a courtesy, the fault itself lands either way.
|
|
784
|
+
// Read back on a restore: the start is the Turn's, not this process's.
|
|
785
|
+
const turnStarted = input.attach?.turnStarted ? new Date(input.attach.turnStarted) : new Date();
|
|
786
|
+
const memoryFault = (name: string): Promise<MemoryKill | undefined> =>
|
|
787
|
+
binding.sandbox?.memoryFault(name, turnStarted).catch(() => undefined) ?? Promise.resolve(undefined);
|
|
788
|
+
// Replaced after a Harness restart that cut the Turn while its state was busy: the cut aborted it.
|
|
789
|
+
let controller = new AbortController();
|
|
790
|
+
|
|
514
791
|
// Shared per run, created on demand so the ordering guarantee holds for any binding.
|
|
515
792
|
const pendingAborts = (binding.pendingAborts ??= new Map<string, Promise<void>>());
|
|
516
793
|
let stopped = false;
|
|
794
|
+
|
|
795
|
+
// A Harness restart ends its Turns (ADR-0021): the Submission it held is cut (ADR-0031 keeps
|
|
796
|
+
// the conversation, not the Turn), and a Harness that crash-loops only ever refuses
|
|
797
|
+
// connections, so `wait` would reconnect forever. The watch sees the restart count pass its value at this Turn's
|
|
798
|
+
// admission, and the Turn fails at once — not its Workspace: the pod and `/work` survive.
|
|
799
|
+
// Sandbox Workspaces only: the Instance Harness is a StatefulSet the watch does not see, and
|
|
800
|
+
// its rebuilt stream settles the Turn `failed` instead (ADR-0031, `settledByRestart`).
|
|
801
|
+
let heardRestarts: HarnessRestarts | undefined;
|
|
802
|
+
let restartMark = input.attach?.harnessRestarts;
|
|
803
|
+
let restarted: HarnessRestarts | undefined;
|
|
804
|
+
// Whether a Harness has answered an admission of this Turn — before that there is nothing to
|
|
805
|
+
// lose, and nothing to hold a restart count against.
|
|
806
|
+
let admitted = input.attach !== undefined;
|
|
807
|
+
if (workspace !== "none" && sandbox !== undefined && binding.sandbox !== undefined) {
|
|
808
|
+
disposers.push(
|
|
809
|
+
binding.sandbox.harnessRestarts(sandbox, (seen) => {
|
|
810
|
+
heardRestarts = seen;
|
|
811
|
+
if (stopped || restarted !== undefined || !admitted) return;
|
|
812
|
+
if (restartMark === undefined) {
|
|
813
|
+
restartMark = seen.restartCount;
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (seen.restartCount > restartMark) {
|
|
817
|
+
restarted = seen;
|
|
818
|
+
controller.abort(new Error("the Harness restarted"));
|
|
819
|
+
}
|
|
820
|
+
}),
|
|
821
|
+
);
|
|
822
|
+
}
|
|
517
823
|
// Ledgered under the ORIGINAL iid — the persisted input's key, like a nudge's — with the
|
|
518
824
|
// LIVE conversation stamped on the record, so a restore settle-follows the live submission
|
|
519
|
-
// AND re-addresses it (see `currentIid` above).
|
|
520
|
-
|
|
521
|
-
|
|
825
|
+
// AND re-addresses it (see `currentIid` above). The notices the admission carried are
|
|
826
|
+
// delivered by this same write (ADR-0062).
|
|
827
|
+
const ledger = (admission: AgentAdmission, delivered: readonly string[]) =>
|
|
828
|
+
binding.recordAdmission?.(
|
|
829
|
+
instanceId,
|
|
830
|
+
{
|
|
831
|
+
...admission,
|
|
832
|
+
instanceId: currentIid,
|
|
833
|
+
agent: input.agentName,
|
|
834
|
+
endpoint,
|
|
835
|
+
...(input.turnId === undefined ? {} : { turnId: input.turnId }),
|
|
836
|
+
},
|
|
837
|
+
delivered,
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// Where this Turn hears notices (ADR-0062). Its continued conversation's — the base id the
|
|
841
|
+
// epoch ledger burns — only when it CONTINUES one: a fresh Turn is new by definition, and the
|
|
842
|
+
// notice waits for the `continue` whose context is gone. Its Workspace's only when it works in
|
|
843
|
+
// one: a Menu-only Agent hears conversation notices alone.
|
|
844
|
+
const noticeWorkspace = workspace !== "none" ? sandbox : undefined;
|
|
845
|
+
const noticeScope: NoticeScope = {
|
|
846
|
+
...(input.continuation ? { conversation } : {}),
|
|
847
|
+
...(noticeWorkspace !== undefined ? { workspace: noticeWorkspace } : {}),
|
|
848
|
+
};
|
|
849
|
+
// No `agent`: the notice names no Turn — what the pod's list says (below).
|
|
850
|
+
// No `limit`: the sizes read were not sizes, and the notice says none.
|
|
851
|
+
const memoryLimit = ({ limit, peak, agent }: { limit?: string; peak?: string; agent?: string }): Notice => ({
|
|
852
|
+
kind: "memory-limit",
|
|
853
|
+
scope: "workspace",
|
|
854
|
+
...(agent === undefined ? {} : { agent }),
|
|
855
|
+
...(limit === undefined ? {} : { limit, ...(peak === undefined ? {} : { peak }) }),
|
|
856
|
+
});
|
|
857
|
+
// The Harness's guard killed this Workspace's processes while this Turn's Submission ran
|
|
858
|
+
// (ADR-0061, layer 4), read on its stream while the wait is live — so the notice can name this
|
|
859
|
+
// Turn's Agent. A stopped actor's last poll names nothing: the state that asked for the Turn has
|
|
860
|
+
// exited, and the pod's list carries the kill. Keyed by the kill's id, so a second running
|
|
861
|
+
// conversation's stream, or the pod's list, adds nothing.
|
|
862
|
+
const onMemoryLimit = (kill: StreamedKill): void => {
|
|
863
|
+
if (stopped || noticeWorkspace === undefined) return;
|
|
864
|
+
binding.raiseNotice?.(
|
|
865
|
+
memoryLimit({ limit: kill.limit, peak: kill.peak, agent: input.agentName }),
|
|
866
|
+
noticeWorkspace,
|
|
867
|
+
kill.legacy ? kill.id : guardSource(noticeWorkspace, kill.id),
|
|
868
|
+
);
|
|
869
|
+
};
|
|
870
|
+
// …but a stream is not every kill's path. A kill while no Submission ran — a process an earlier
|
|
871
|
+
// Turn left running, killed while the workflow waited at a Gate — is on no stream, and a kill
|
|
872
|
+
// after a wait stopped (the Agent's state exited, its abort not yet landed) is on a stream that
|
|
873
|
+
// no one reads. The Harness keeps every kill, so the pod's list is read before notices are
|
|
874
|
+
// taken, and this admission carries what it finds. Keyed as on the stream: the ledger's `seen`,
|
|
875
|
+
// persisted with the run, makes a kill read on both paths, or read again after a restart of
|
|
876
|
+
// either side, one notice. The notice names no Turn: the list cannot say whose Turn a kill
|
|
877
|
+
// ended. One read raises at most one notice, the newest kill's; the older kills in it are only
|
|
878
|
+
// marked seen, so they never give a notice. A read that fails or times out is none: the list
|
|
879
|
+
// keeps the kill, and the next admission reads again.
|
|
880
|
+
//
|
|
881
|
+
// The Turn's first read sets its `killMark`, the newest kill it saw, ledgered with every
|
|
882
|
+
// admission of the Turn. Answers the sources a nudge or a wake of THIS conversation may carry:
|
|
883
|
+
// the kills after that mark — none from before the Turn, which would tell the Agent that
|
|
884
|
+
// processes it started in this Turn died — and after the newest kill its own `bash` call
|
|
885
|
+
// answered, whose answer said all its processes died. Those stay pending for the next Turn.
|
|
886
|
+
// Only when the first read failed is the Turn's start time the mark, held against `at`.
|
|
887
|
+
// A kernel kill (ADR-0061, layer 5) ends the Harness container, so no list keeps it and no
|
|
888
|
+
// stream carries it. A Turn it ended faults on it, and the fault raises its notice (below). A
|
|
889
|
+
// kill while no Turn ran faults nothing: the restarted Harness rebuilds its conversations. But
|
|
890
|
+
// the operator publishes the container's last end on the Sandbox, and the watch keeps it until
|
|
891
|
+
// the next end. So before each admission into the Workspace, a last end `OOMKilled` raises the
|
|
892
|
+
// notice, keyed by that end as the fault keys it: one notice for one kill, whichever path saw
|
|
893
|
+
// it first, and none again after a restart of either side. It names no Turn: the end cannot say
|
|
894
|
+
// whose Turn it was. A watch that has not listed yet says nothing; the next admission asks again.
|
|
895
|
+
// Not after a restart this Turn's watch saw: the Turn faults on it, or is re-admitted, and the
|
|
896
|
+
// fault's notice names this Turn's Agent — a raise here would take its source first.
|
|
897
|
+
const hearKernelKill = async (): Promise<void> => {
|
|
898
|
+
if (restarted !== undefined) return;
|
|
899
|
+
const last = heardRestarts?.lastTerminated;
|
|
900
|
+
if (noticeWorkspace === undefined || last?.reason !== OOM_KILLED || last.finishedAt === undefined) return;
|
|
901
|
+
const kill = await binding.sandbox
|
|
902
|
+
?.memoryFault(noticeWorkspace, new Date(last.finishedAt))
|
|
903
|
+
.catch(() => undefined);
|
|
904
|
+
if (kill?.at === undefined) return;
|
|
905
|
+
binding.raiseNotice?.(
|
|
906
|
+
memoryLimit({ limit: kill.limit }),
|
|
907
|
+
noticeWorkspace,
|
|
908
|
+
kernelSource(noticeWorkspace, kill.at),
|
|
909
|
+
);
|
|
910
|
+
};
|
|
911
|
+
let killMark = input.attach?.killMark;
|
|
912
|
+
let firstRead = input.attach === undefined;
|
|
913
|
+
// `known: false`: the read could not tell, so which kills this conversation was told is not
|
|
914
|
+
// known either — an admission of the same Turn then takes no guard kill at all.
|
|
915
|
+
const hearPodKills = async (): Promise<{ known: boolean; unsaid: string[]; said: string[] }> => {
|
|
916
|
+
const none = { known: true, unsaid: [], said: [] };
|
|
917
|
+
if (noticeWorkspace === undefined) return none;
|
|
918
|
+
const first = firstRead;
|
|
919
|
+
firstRead = false;
|
|
920
|
+
let kills: ListedKill[] | undefined;
|
|
921
|
+
try {
|
|
922
|
+
kills = await client.memoryKills({
|
|
923
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(memoryKillsReadMs)]),
|
|
924
|
+
});
|
|
925
|
+
} catch (err) {
|
|
926
|
+
// Stopped while it read: nothing more is admitted, and nothing is taken.
|
|
927
|
+
if (controller.signal.aborted) throw err;
|
|
928
|
+
return { ...none, known: false };
|
|
929
|
+
}
|
|
930
|
+
// Could not tell: no mark — an empty list is not what was seen — so the clock is the mark.
|
|
931
|
+
if (kills === undefined) return { ...none, known: false };
|
|
932
|
+
if (first) killMark = kills.at(-1)?.id ?? NO_KILLS;
|
|
933
|
+
const newest = kills.at(-1);
|
|
934
|
+
if (newest === undefined) return none;
|
|
935
|
+
binding.raiseNotice?.(
|
|
936
|
+
memoryLimit({ limit: newest.limit, peak: newest.peak }),
|
|
937
|
+
noticeWorkspace,
|
|
938
|
+
guardSource(noticeWorkspace, newest.id),
|
|
939
|
+
kills.slice(0, -1).map((kill) => guardSource(noticeWorkspace, kill.id)),
|
|
940
|
+
);
|
|
941
|
+
const toldHere = (kill: ListedKill): boolean =>
|
|
942
|
+
kill.told.some((t) => t.agent === input.agentName && t.instanceId === currentIid);
|
|
943
|
+
// The first kill after the Turn's mark. A mark no longer in the bounded list is older than
|
|
944
|
+
// every kill in it.
|
|
945
|
+
let from = 0;
|
|
946
|
+
kills.forEach((kill, i) => {
|
|
947
|
+
if (kill.id === killMark || toldHere(kill)) from = i + 1;
|
|
948
|
+
});
|
|
949
|
+
const inThisTurn = (kill: ListedKill): boolean =>
|
|
950
|
+
killMark !== undefined || Date.parse(kill.at) >= turnStarted.getTime() - KILL_CLOCK_SKEW_MS;
|
|
951
|
+
const unsaid = kills.slice(from).filter(inThisTurn);
|
|
952
|
+
return {
|
|
953
|
+
known: true,
|
|
954
|
+
unsaid: unsaid.map((kill) => guardSource(noticeWorkspace, kill.id)),
|
|
955
|
+
said: kills.filter((kill) => !unsaid.includes(kill)).map((kill) => guardSource(noticeWorkspace, kill.id)),
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
// The Turn's admission and a reroll's carry the pending notices in scope — a reroll's
|
|
960
|
+
// conversation is fresh, and has heard nothing. A nudge's and a wake's carry only the guard
|
|
961
|
+
// kills of this Turn that came after its own `bash` call's last kill verdict (`hearPodKills`):
|
|
962
|
+
// they re-prompt the Turn within itself (ADR-0016), so a kill that verdict covers is already in
|
|
963
|
+
// its context, and is for the next Turn in the Workspace (ADR-0062). A later kill — while its
|
|
964
|
+
// model wrote text, while it waited for its wake, or answered in another conversation — this
|
|
965
|
+
// Agent was never told. Reserved
|
|
966
|
+
// first, so no concurrent admission carries them too; delivered by the ledger write; released
|
|
967
|
+
// when the admission fails, for the next one to carry.
|
|
968
|
+
//
|
|
969
|
+
// What the last carrying admission took, and under which iid: delivered is not HEARD. If this
|
|
970
|
+
// actor ends the Turn before the Harness reads that iid's surface, the Harness finds the Turn
|
|
971
|
+
// over and settles it with no model asked (ADR-0026) — so `abandon` raises them again.
|
|
972
|
+
// Per conversation the Turn admitted on: the notices its admissions carried — `unheard` since
|
|
973
|
+
// the Harness last read its surface, `heard` before that — and whether an unheard one carried
|
|
974
|
+
// the Turn's task. Collected across the Turn, so a re-admission onto a lost conversation tells
|
|
975
|
+
// the Agent all of them again.
|
|
976
|
+
type Raised = { notice: Notice; source?: string };
|
|
977
|
+
// `fresh`: a fresh Turn's "this conversation is new", which rides an admission as `extra` and so
|
|
978
|
+
// is kept in no ledger — an unread one is carried again by the next re-admission.
|
|
979
|
+
const carriedBy = new Map<string, { heard: Raised[]; unheard: Raised[]; task: boolean; fresh: Notice[] }>();
|
|
980
|
+
// `notices`: "turn" takes every notice in scope; "same-turn" too, but the guard kills the Turn's own
|
|
981
|
+
// conversation was told (a re-admission after a cut continues the same Turn); "nudge" takes the
|
|
982
|
+
// guard kills this conversation was not told alone.
|
|
983
|
+
// `include`: sources restored for this admission, taken whatever the scope leaves out. `task`:
|
|
984
|
+
// this admission's prompt holds the Turn's task. `kept`: the admission is for a conversation
|
|
985
|
+
// that `holds` said the Harness keeps; a Harness that keeps none refuses it (`isNotKept`), and
|
|
986
|
+
// the refusal is thrown as any failed admission is.
|
|
987
|
+
const admitTurn = async (
|
|
988
|
+
turn: AgentRunInput,
|
|
989
|
+
opts: {
|
|
990
|
+
notices: "turn" | "same-turn" | "nudge";
|
|
991
|
+
extra?: Notice[];
|
|
992
|
+
include?: readonly string[];
|
|
993
|
+
task?: boolean;
|
|
994
|
+
kept?: boolean;
|
|
995
|
+
} = { notices: "turn", task: true },
|
|
996
|
+
) => {
|
|
997
|
+
await hearKernelKill();
|
|
998
|
+
const { known, unsaid, said } = await hearPodKills();
|
|
999
|
+
// A read that could not tell leaves the same Turn no way to know which kills its own `bash`
|
|
1000
|
+
// reported: it takes none, as a nudge takes none then, and they stay for the next Turn.
|
|
1001
|
+
const sameTurn: NoticeScope = known
|
|
1002
|
+
? { ...noticeScope, except: said }
|
|
1003
|
+
: {
|
|
1004
|
+
...noticeScope,
|
|
1005
|
+
...(noticeWorkspace === undefined ? {} : { exceptPrefix: guardSource(noticeWorkspace, "") }),
|
|
1006
|
+
};
|
|
1007
|
+
const scope: NoticeScope | undefined =
|
|
1008
|
+
opts.notices === "turn"
|
|
1009
|
+
? noticeScope
|
|
1010
|
+
: opts.notices === "same-turn"
|
|
1011
|
+
? sameTurn
|
|
1012
|
+
: unsaid.length > 0
|
|
1013
|
+
? { workspace: noticeWorkspace, only: unsaid }
|
|
1014
|
+
: undefined;
|
|
1015
|
+
const none = { ids: [] as string[], notices: [] as Notice[], raised: [] as Raised[] };
|
|
1016
|
+
const restored =
|
|
1017
|
+
opts.include && opts.include.length > 0
|
|
1018
|
+
? (binding.takeNotices?.({ ...noticeScope, only: opts.include }) ?? none)
|
|
1019
|
+
: none;
|
|
1020
|
+
const main = (scope ? binding.takeNotices?.(scope) : undefined) ?? none;
|
|
1021
|
+
const taken = {
|
|
1022
|
+
ids: [...restored.ids, ...main.ids],
|
|
1023
|
+
notices: [...restored.notices, ...main.notices],
|
|
1024
|
+
raised: [
|
|
1025
|
+
...(restored.raised ?? restored.notices.map((notice) => ({ notice }))),
|
|
1026
|
+
...(main.raised ?? main.notices.map((notice) => ({ notice }))),
|
|
1027
|
+
],
|
|
1028
|
+
};
|
|
1029
|
+
// `extra`: a notice this admission carries straight from the actor, never kept — the answer a
|
|
1030
|
+
// cut pick never got (ADR-0031), whose one path to the model is this admission. One "this
|
|
1031
|
+
// conversation is new" is enough, whichever path brought it.
|
|
1032
|
+
let fresh = false;
|
|
1033
|
+
const notices = [...taken.notices, ...(opts.extra ?? [])].filter((n) => {
|
|
1034
|
+
if (n.kind !== "conversation-new") return true;
|
|
1035
|
+
if (fresh) return false;
|
|
1036
|
+
fresh = true;
|
|
1037
|
+
return true;
|
|
1038
|
+
});
|
|
1039
|
+
// `served` is per Instance ID, and a nudge or a wake reuses the Turn's: it must be read again
|
|
1040
|
+
// before this admission counts as heard (`reraiseUnheard`). An admission that fails puts the
|
|
1041
|
+
// mark back — what an earlier one carried was heard.
|
|
1042
|
+
const heard = served.has(turn.instanceId);
|
|
1043
|
+
served.delete(turn.instanceId);
|
|
1044
|
+
let admission: AgentAdmission;
|
|
1045
|
+
try {
|
|
1046
|
+
admission = await client.admit(turn, {
|
|
1047
|
+
definition,
|
|
1048
|
+
signal: controller.signal,
|
|
1049
|
+
...(notices.length ? { notices } : {}),
|
|
1050
|
+
...(opts.kept ? { kept: true } : {}),
|
|
1051
|
+
});
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
binding.releaseNotices?.(taken.ids);
|
|
1054
|
+
if (heard) served.add(turn.instanceId);
|
|
1055
|
+
throw err;
|
|
1056
|
+
}
|
|
1057
|
+
// The mark is taken once the Harness has ANSWERED: a restart before the answer took the
|
|
1058
|
+
// admission with it, and the conversation's 404 says so; a restart after it is this Turn's.
|
|
1059
|
+
// Unheard yet (the watch has not listed), the first count heard is the mark.
|
|
1060
|
+
admitted = true;
|
|
1061
|
+
restartMark = heardRestarts?.restartCount;
|
|
1062
|
+
if (restartMark !== undefined) admission = { ...admission, harnessRestarts: restartMark };
|
|
1063
|
+
// The Turn's task rides every ledgered admission: a restore drops `prompt`.
|
|
1064
|
+
const task = input.prompt ?? input.attach?.task;
|
|
1065
|
+
if (task !== undefined) admission = { ...admission, task };
|
|
1066
|
+
// A Turn in a Workspace keeps its start and its kill mark across a restore.
|
|
1067
|
+
if (noticeWorkspace !== undefined) {
|
|
1068
|
+
admission = {
|
|
1069
|
+
...admission,
|
|
1070
|
+
turnStarted: turnStarted.toISOString(),
|
|
1071
|
+
...(killMark === undefined ? {} : { killMark }),
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
ledger(admission, taken.ids);
|
|
1075
|
+
const bucket = carriedBy.get(turn.instanceId) ?? { heard: [], unheard: [], task: false, fresh: [] };
|
|
1076
|
+
if (heard) {
|
|
1077
|
+
bucket.heard.push(...bucket.unheard);
|
|
1078
|
+
bucket.unheard = [];
|
|
1079
|
+
bucket.task = false;
|
|
1080
|
+
bucket.fresh = [];
|
|
1081
|
+
}
|
|
1082
|
+
bucket.unheard.push(...taken.raised);
|
|
1083
|
+
if (opts.task) bucket.task = true;
|
|
1084
|
+
if (!input.continuation) bucket.fresh.push(...(opts.extra ?? []).filter((n) => n.kind === "conversation-new"));
|
|
1085
|
+
carriedBy.set(turn.instanceId, bucket);
|
|
1086
|
+
return admission;
|
|
1087
|
+
};
|
|
1088
|
+
// The notices unheard admissions carried, pending again for the next Turn in their scope —
|
|
1089
|
+
// each WITH the source it was raised with, so a take by source finds it and no equal notice of
|
|
1090
|
+
// another source swallows it. `lost`: the current conversation, which heard the rest, is gone,
|
|
1091
|
+
// so they are unheard too. Answers the sources restored, which a re-admission of the same Turn
|
|
1092
|
+
// takes whatever else it leaves out.
|
|
1093
|
+
const reraiseUnheard = (lost = false): string[] => {
|
|
1094
|
+
const restored: string[] = [];
|
|
1095
|
+
for (const [iid, bucket] of carriedBy) {
|
|
1096
|
+
const gone = lost && iid === currentIid;
|
|
1097
|
+
const read = served.has(iid) && !gone;
|
|
1098
|
+
const again = [...(gone ? bucket.heard : []), ...(read ? [] : bucket.unheard)];
|
|
1099
|
+
if (gone) bucket.heard = [];
|
|
1100
|
+
if (!read) bucket.unheard = [];
|
|
1101
|
+
for (const { notice, source } of again) {
|
|
1102
|
+
const to = notice.scope === "conversation" ? conversation : noticeWorkspace;
|
|
1103
|
+
if (to === undefined) continue;
|
|
1104
|
+
if (binding.restoreNotice) binding.restoreNotice(notice, to, source);
|
|
1105
|
+
else binding.raiseNotice?.(notice, to);
|
|
1106
|
+
if (source !== undefined) restored.push(source);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return restored;
|
|
1110
|
+
};
|
|
522
1111
|
|
|
523
1112
|
const abandon = () => {
|
|
524
1113
|
if (stopped) return;
|
|
525
1114
|
stopped = true;
|
|
1115
|
+
// What ended the Turn, read while its registration is still there (ADR-0024).
|
|
1116
|
+
const byPick = binding.turnEndedByPick?.(currentIid) === true;
|
|
526
1117
|
for (const dispose of disposers.splice(0)) dispose();
|
|
527
1118
|
// Stop consuming the stream. The remote end of the turn is the next paragraph.
|
|
528
1119
|
controller.abort();
|
|
@@ -531,17 +1122,227 @@ export function agentActorWith(
|
|
|
531
1122
|
// Pool cancelling a child. The one exception is the host stopping the run for its own
|
|
532
1123
|
// reasons, which ADR-0007's restore re-attaches to.
|
|
533
1124
|
if (binding.hostStopping) return;
|
|
1125
|
+
reraiseUnheard();
|
|
534
1126
|
// Fire-and-forget, and unreportable BY CONSTRUCTION: this actor is stopped, so there is no
|
|
535
1127
|
// `agent.fault` left to raise. An orphan that survives a failed abort 404s on every tool
|
|
536
1128
|
// call and settles on its own.
|
|
537
1129
|
const iid = currentIid;
|
|
538
|
-
|
|
1130
|
+
// The cause names the Submission it is about: the last one ledgered for this Turn. The ledger
|
|
1131
|
+
// is keyed by the invocation's iid, and stamps the live conversation (a reroll's).
|
|
1132
|
+
const ledgered = binding.admissionOf?.(instanceId);
|
|
1133
|
+
const submissionId = ledgered?.instanceId === iid ? ledgered.submissionId : undefined;
|
|
1134
|
+
const cause = byPick ? { cause: "pick" as const, ...(submissionId ? { submissionId } : {}) } : {};
|
|
1135
|
+
const aborting = client.abort(input.agentName, iid, cause).catch(() => {});
|
|
539
1136
|
pendingAborts.set(iid, aborting);
|
|
540
1137
|
void aborting.then(() => {
|
|
541
1138
|
if (pendingAborts.get(iid) === aborting) pendingAborts.delete(iid);
|
|
542
1139
|
});
|
|
543
1140
|
};
|
|
544
1141
|
|
|
1142
|
+
// The Allowed picks NOW (ADR-0029), read as the surface reads them: one list for the surface,
|
|
1143
|
+
// the receipts, the nudge and the wake.
|
|
1144
|
+
const allowedNow = (): string[] => allowedPicks({ defs, menu, invoker: self._parent, ...seat });
|
|
1145
|
+
// Resolves with the Allowed picks the first time they are not empty — read a microtask after
|
|
1146
|
+
// every snapshot of the invoking Machine, so a Gate's answer, an outside event, a settled child
|
|
1147
|
+
// or a timer each count — or with `[]` once this actor stops. Never inside the subscriber: a
|
|
1148
|
+
// snapshot between macrosteps (a state that sends to itself) is not Rest until the mailbox
|
|
1149
|
+
// drains (rest.ts), and a wake with an empty list spends the nudge's budget for nothing. Its own
|
|
1150
|
+
// disposer, so `abandon` ends the wait.
|
|
1151
|
+
const picksOpen = (): Promise<string[]> =>
|
|
1152
|
+
new Promise((resolve) => {
|
|
1153
|
+
let done = false;
|
|
1154
|
+
let sub: { unsubscribe(): void } | undefined;
|
|
1155
|
+
const finish = (allowed: string[]): void => {
|
|
1156
|
+
if (done) return;
|
|
1157
|
+
done = true;
|
|
1158
|
+
const at = disposers.indexOf(dispose);
|
|
1159
|
+
if (at >= 0) disposers.splice(at, 1);
|
|
1160
|
+
sub?.unsubscribe();
|
|
1161
|
+
resolve(allowed);
|
|
1162
|
+
};
|
|
1163
|
+
const dispose = (): void => finish([]);
|
|
1164
|
+
const check = (): void => {
|
|
1165
|
+
if (stopped) return finish([]);
|
|
1166
|
+
const allowed = allowedNow();
|
|
1167
|
+
if (allowed.length > 0) finish(allowed);
|
|
1168
|
+
};
|
|
1169
|
+
disposers.push(dispose);
|
|
1170
|
+
check();
|
|
1171
|
+
if (done) return;
|
|
1172
|
+
sub = self._parent?.subscribe({ next: () => queueMicrotask(check), error: dispose, complete: dispose });
|
|
1173
|
+
if (sub === undefined) finish([]);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// Resolves once the Agent's state is back at Rest — its outstanding pick answered, or nothing
|
|
1177
|
+
// running below it — or has been left, or with nothing once this actor stops. Read a microtask
|
|
1178
|
+
// after each snapshot of the invoking Machine: the mailbox has drained (rest.ts).
|
|
1179
|
+
const agentState = { invoker: self._parent, invokeId: self.id };
|
|
1180
|
+
const untilRest = (): Promise<void> =>
|
|
1181
|
+
new Promise((resolve) => {
|
|
1182
|
+
let done = false;
|
|
1183
|
+
let sub: { unsubscribe(): void } | undefined;
|
|
1184
|
+
const finish = (): void => {
|
|
1185
|
+
if (done) return;
|
|
1186
|
+
done = true;
|
|
1187
|
+
const at = disposers.indexOf(finish);
|
|
1188
|
+
if (at >= 0) disposers.splice(at, 1);
|
|
1189
|
+
sub?.unsubscribe();
|
|
1190
|
+
resolve();
|
|
1191
|
+
};
|
|
1192
|
+
const check = (): void => {
|
|
1193
|
+
if (stopped) return finish();
|
|
1194
|
+
const held = binding.outstandingOf?.(currentIid);
|
|
1195
|
+
if (held !== undefined && !held.fetched && !held.answered) return;
|
|
1196
|
+
if (restOf(agentState) !== "busy") finish();
|
|
1197
|
+
};
|
|
1198
|
+
disposers.push(finish);
|
|
1199
|
+
check();
|
|
1200
|
+
if (done) return;
|
|
1201
|
+
sub = self._parent?.subscribe({ next: () => queueMicrotask(check), error: finish, complete: finish });
|
|
1202
|
+
if (sub === undefined) finish();
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// The Harness keeps no such conversation (ADR-0062): it lost it while the Turn waited, and a
|
|
1206
|
+
// `kept` admission found none. Nothing of the Turn is left there, so the Turn is admitted as a
|
|
1207
|
+
// Turn on a new conversation: every notice in scope, `include` whatever the scope leaves out,
|
|
1208
|
+
// and `prompt`, which holds the Turn's task. A continued Turn is told its conversation is new as the
|
|
1209
|
+
// continue path tells it — the notice is kept for its conversation, which this admission takes.
|
|
1210
|
+
// A fresh Turn's conversation is its own: the notice rides this admission alone, so no later
|
|
1211
|
+
// continued Turn of another conversation is told its context is gone. `before` and `after`
|
|
1212
|
+
// ride beside it, in that order.
|
|
1213
|
+
const admitOntoLost = async (
|
|
1214
|
+
prompt: string,
|
|
1215
|
+
before: Notice[],
|
|
1216
|
+
after: Notice[],
|
|
1217
|
+
include: readonly string[],
|
|
1218
|
+
): Promise<AgentAdmission> => {
|
|
1219
|
+
const extra = [...before];
|
|
1220
|
+
const fresh: Notice = {
|
|
1221
|
+
kind: "conversation-new",
|
|
1222
|
+
scope: "conversation",
|
|
1223
|
+
reason: "the Harness no longer holds it",
|
|
1224
|
+
};
|
|
1225
|
+
if (input.continuation) binding.raiseNotice?.(fresh, conversation);
|
|
1226
|
+
else extra.push(fresh);
|
|
1227
|
+
return await admitTurn(
|
|
1228
|
+
{ ...framed, attach: undefined, instanceId: currentIid, prompt },
|
|
1229
|
+
{ notices: "turn", extra: [...extra, ...after], include, task: true },
|
|
1230
|
+
);
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
// A Harness restart cut this Turn (ADR-0031). It is NOT an `agent.fault` when the Agent's state is
|
|
1234
|
+
// busy — something it started runs below it — or its pick's answer has not reached the model:
|
|
1235
|
+
// faulting would route the Machine out of the state and stop the work the Turn did not want
|
|
1236
|
+
// stopped. jr2 waits for Rest, then re-admits the Agent on the same conversation, which the
|
|
1237
|
+
// rebuilt Harness holds, with the answer as a notice. Undefined: not this case, the caller faults.
|
|
1238
|
+
let cuts = 0;
|
|
1239
|
+
const readmitAfterCut = async (err: unknown): Promise<AgentAdmission | "stopped" | undefined> => {
|
|
1240
|
+
if (stopped) return "stopped";
|
|
1241
|
+
if (restarted === undefined && settledByRestart(err) === undefined) return undefined;
|
|
1242
|
+
const unread = (): boolean => {
|
|
1243
|
+
const held = binding.outstandingOf?.(currentIid);
|
|
1244
|
+
return held !== undefined && !held.fetched;
|
|
1245
|
+
};
|
|
1246
|
+
if (restOf(agentState) !== "busy" && !unread()) return undefined;
|
|
1247
|
+
await untilRest();
|
|
1248
|
+
if (stopped) return "stopped";
|
|
1249
|
+
const held = binding.outstandingOf?.(currentIid);
|
|
1250
|
+
const notice: Notice[] =
|
|
1251
|
+
held !== undefined && !held.fetched && held.answered && !held.turnComplete
|
|
1252
|
+
? [
|
|
1253
|
+
{
|
|
1254
|
+
kind: "pick-answer",
|
|
1255
|
+
scope: "conversation",
|
|
1256
|
+
pick: held.pick,
|
|
1257
|
+
cut: true,
|
|
1258
|
+
...(held.answer !== undefined ? { answer: held.answer } : {}),
|
|
1259
|
+
},
|
|
1260
|
+
]
|
|
1261
|
+
: [];
|
|
1262
|
+
// Kept until the re-admission is sure: a Turn that faults below still names the restart.
|
|
1263
|
+
const cutBy = restarted;
|
|
1264
|
+
// The restart that cut the Turn is the new mark at once: the watch may tell it again before
|
|
1265
|
+
// the re-admission is answered, and it must not cut the re-admission too.
|
|
1266
|
+
if (restarted !== undefined) restartMark = restarted.restartCount;
|
|
1267
|
+
restarted = undefined;
|
|
1268
|
+
controller = new AbortController();
|
|
1269
|
+
// The rebuilt Harness may not read the conversation back: then this Turn lands on a new one,
|
|
1270
|
+
// and is told (ADR-0062). Asked again while the new container may not listen yet, and taken
|
|
1271
|
+
// as "held" only when the time is over. That "held" can be wrong — a Harness in
|
|
1272
|
+
// CrashLoopBackOff answers nothing for longer — so the admission says it is for a kept
|
|
1273
|
+
// conversation, and a Harness that keeps none refuses it: then the Turn takes the path of a
|
|
1274
|
+
// lost conversation below.
|
|
1275
|
+
const kept = await client.holds(input.agentName, currentIid, {
|
|
1276
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(holdsReadMs)]),
|
|
1277
|
+
});
|
|
1278
|
+
if (stopped) return "stopped";
|
|
1279
|
+
// The task: a restore dropped `prompt`, and the ledgered admission kept it. A new
|
|
1280
|
+
// conversation with no task to send cannot be asked to "continue your task": the Turn faults.
|
|
1281
|
+
const task = input.prompt ?? input.attach?.task;
|
|
1282
|
+
// Whether the cut admission, never read, carried the task: then the model never read it.
|
|
1283
|
+
const taskUnheard = !served.has(currentIid) && carriedBy.get(currentIid)?.task === true;
|
|
1284
|
+
// A fresh Turn's "this conversation is new" that an unread re-admission carried rides this
|
|
1285
|
+
// one again: it is kept nowhere else. Read before the first admission below, which can fail.
|
|
1286
|
+
const unreadFresh: Notice[] = served.has(currentIid) ? [] : [...(carriedBy.get(currentIid)?.fresh ?? [])];
|
|
1287
|
+
// On a new conversation nothing of the Turn is left: the cut pick's answer rides after the task,
|
|
1288
|
+
// and after the unread "this conversation is new" that the cut admission carried.
|
|
1289
|
+
const readmitLost = (withTask: string, include: readonly string[]): Promise<AgentAdmission> =>
|
|
1290
|
+
admitOntoLost(`${READMIT_PROMPT}\n\n${withTask}`, unreadFresh, notice, include);
|
|
1291
|
+
// On a conversation the Harness still holds, it continues the same Turn. The task follows the
|
|
1292
|
+
// re-admit text when a cut admission carried it and was never read.
|
|
1293
|
+
const readmitKept = (include: readonly string[]): Promise<AgentAdmission> => {
|
|
1294
|
+
const withTask = taskUnheard && task !== undefined;
|
|
1295
|
+
return admitTurn(
|
|
1296
|
+
{
|
|
1297
|
+
...framed,
|
|
1298
|
+
attach: undefined,
|
|
1299
|
+
instanceId: currentIid,
|
|
1300
|
+
prompt: withTask ? `${READMIT_PROMPT}\n\n${task}` : READMIT_PROMPT,
|
|
1301
|
+
},
|
|
1302
|
+
{ notices: "same-turn", extra: [...unreadFresh, ...notice], include, task: withTask, kept: true },
|
|
1303
|
+
);
|
|
1304
|
+
};
|
|
1305
|
+
// What the cut admissions carried is pending again before this one takes, if the Harness never
|
|
1306
|
+
// read their surface — or all of it, if the conversation that heard it is gone. Those sources
|
|
1307
|
+
// ride this re-admission whatever its scope leaves out.
|
|
1308
|
+
let admission: AgentAdmission;
|
|
1309
|
+
if (!kept) {
|
|
1310
|
+
// The fault names the restart that cut the Turn, and asks whether it was a memory kill.
|
|
1311
|
+
if (task === undefined) {
|
|
1312
|
+
restarted = cutBy;
|
|
1313
|
+
return undefined;
|
|
1314
|
+
}
|
|
1315
|
+
admission = await readmitLost(task, reraiseUnheard(true));
|
|
1316
|
+
} else {
|
|
1317
|
+
const restored = reraiseUnheard(false);
|
|
1318
|
+
try {
|
|
1319
|
+
admission = await readmitKept(restored);
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
if (!isNotKept(err)) throw err;
|
|
1322
|
+
// "Held" was the timeout's: the Harness keeps no such conversation, and it seated none. The
|
|
1323
|
+
// failed admission put back what it took. Now the path of a lost conversation: what the
|
|
1324
|
+
// conversation heard is unheard, and the Turn is admitted with its task — or faults when
|
|
1325
|
+
// there is no task to send.
|
|
1326
|
+
if (stopped) return "stopped";
|
|
1327
|
+
if (task === undefined) {
|
|
1328
|
+
restarted = cutBy;
|
|
1329
|
+
return undefined;
|
|
1330
|
+
}
|
|
1331
|
+
admission = await readmitLost(task, [...restored, ...reraiseUnheard(true)]);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
// Said once an admission worked: a refused one that then faults re-admitted nothing.
|
|
1335
|
+
cuts++;
|
|
1336
|
+
binding.telemetry?.({
|
|
1337
|
+
kind: "retry",
|
|
1338
|
+
child: self._parent?.id ?? self.id,
|
|
1339
|
+
attempt: cuts,
|
|
1340
|
+
reason: "re-admitted after a Harness restart",
|
|
1341
|
+
});
|
|
1342
|
+
binding.heldAnswerDelivered?.(currentIid);
|
|
1343
|
+
return admission;
|
|
1344
|
+
};
|
|
1345
|
+
|
|
545
1346
|
// Down-channel: interrupts only (ADR-0002 split-channel model).
|
|
546
1347
|
receive((event) => {
|
|
547
1348
|
if (event.type === "CANCEL") abandon();
|
|
@@ -555,8 +1356,9 @@ export function agentActorWith(
|
|
|
555
1356
|
// - INFRA faults: provider retries run inside the turn, Harness-side, and the wire
|
|
556
1357
|
// client reconnects transparently — so a `settle` rejection means the Submission settled
|
|
557
1358
|
// failed/aborted, or the conversation is lost (ADR-0027). Terminal, no jr2 re-run.
|
|
558
|
-
// - NO-SIGNAL: the Submission settles COMPLETED
|
|
559
|
-
// calls that a normal
|
|
1359
|
+
// - NO-SIGNAL: the Submission settles COMPLETED with the Turn still open and Allowed picks
|
|
1360
|
+
// left (the ask-again rule, below). The Harness calls that a normal Submission, so jr2 owns
|
|
1361
|
+
// a budgeted re-prompt on the SAME iid (the conversation
|
|
560
1362
|
// continues; each nudge is a fresh admission, ledgered like any other).
|
|
561
1363
|
// - RUNAWAY: the Harness ended a turn that would not conclude and settled it failed with
|
|
562
1364
|
// the typed "runaway" error (ADR-0035). The degenerate context is poisoned, so the
|
|
@@ -564,13 +1366,16 @@ export function agentActorWith(
|
|
|
564
1366
|
// admitting the identical prompt — closed to continuations, which opted out of exactly that.
|
|
565
1367
|
// Any budget exhausting emits the ONE terminal `agent.fault { reason }`.
|
|
566
1368
|
void (async () => {
|
|
567
|
-
const fault = (reason: string) => {
|
|
1369
|
+
const fault = (reason: string, opts: { conversationKept?: boolean } = {}) => {
|
|
568
1370
|
if (stopped) return;
|
|
569
1371
|
// Burn the conversation before the fault lands (ADR-0057): the state the fault routes to
|
|
570
1372
|
// may invoke this Agent again in the same macrostep, and its mapper must already read the
|
|
571
1373
|
// bumped epoch. Continuations only — a fresh invocation mints its own conversation, so
|
|
572
|
-
// there is nothing to burn.
|
|
573
|
-
|
|
1374
|
+
// there is nothing to burn. Not when a Harness restart cut the Turn: the rebuilt Harness
|
|
1375
|
+
// holds the conversation (ADR-0031), so only the Turn was lost and the next `continue`
|
|
1376
|
+
// lands on it; that Turn's `holds` check tells it if the rebuild could not read it (ADR-0062).
|
|
1377
|
+
// The fault's reason is the next Turn's news: it is why that conversation starts empty.
|
|
1378
|
+
if (input.continuation && !opts.conversationKept) binding.bumpEpoch?.(conversation, reason);
|
|
574
1379
|
sendBack({ type: "agent.fault", instanceId, reason } satisfies FaultTelemetry);
|
|
575
1380
|
};
|
|
576
1381
|
try {
|
|
@@ -581,8 +1386,33 @@ export function agentActorWith(
|
|
|
581
1386
|
await pendingAborts.get(instanceId);
|
|
582
1387
|
let admission = input.attach;
|
|
583
1388
|
if (!admission) {
|
|
584
|
-
|
|
585
|
-
|
|
1389
|
+
// A continued conversation admitted before may be gone with no fault to say so: its
|
|
1390
|
+
// Harness restarted between the Turns. Then this Turn lands on a new one, and is told
|
|
1391
|
+
// (ADR-0062). A conversation never admitted has nothing to lose, so nothing is asked.
|
|
1392
|
+
// "Held" can be a timeout's, so the admission says it is for a kept conversation, and a
|
|
1393
|
+
// Harness that keeps none refuses it: then the Turn is told, and admitted again.
|
|
1394
|
+
const told = () =>
|
|
1395
|
+
binding.raiseNotice?.(
|
|
1396
|
+
{ kind: "conversation-new", scope: "conversation", reason: "the Harness no longer holds it" },
|
|
1397
|
+
conversation,
|
|
1398
|
+
);
|
|
1399
|
+
const asked = input.continuation && binding.admissionOf?.(instanceId) !== undefined;
|
|
1400
|
+
const kept =
|
|
1401
|
+
asked &&
|
|
1402
|
+
(await client.holds(input.agentName, instanceId, {
|
|
1403
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(holdsReadMs)]),
|
|
1404
|
+
}));
|
|
1405
|
+
if (asked && !kept) told();
|
|
1406
|
+
if (kept) {
|
|
1407
|
+
try {
|
|
1408
|
+
admission = await admitTurn(framed, { notices: "turn", task: true, kept: true });
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
if (!isNotKept(err)) throw err;
|
|
1411
|
+
if (stopped) return;
|
|
1412
|
+
told();
|
|
1413
|
+
admission = await admitTurn(framed);
|
|
1414
|
+
}
|
|
1415
|
+
} else admission = await admitTurn(framed);
|
|
586
1416
|
// The admission marker (ADR-0023): the Turn and its framing, once — a re-attach
|
|
587
1417
|
// continues a Turn already announced, and a nudge (below) is mechanism, not narrative
|
|
588
1418
|
// (its telemetry already rides the feed).
|
|
@@ -592,7 +1422,7 @@ export function agentActorWith(
|
|
|
592
1422
|
let rerolls = 0;
|
|
593
1423
|
for (;;) {
|
|
594
1424
|
try {
|
|
595
|
-
await client.settle(admission, { signal: controller.signal });
|
|
1425
|
+
await client.settle(admission, { signal: controller.signal, onMemoryLimit });
|
|
596
1426
|
} catch (err) {
|
|
597
1427
|
// The reroll gate closes on `signaled` like the nudge gate below: a delivered pick
|
|
598
1428
|
// means the workflow already holds this turn's signal, so replaying the identical
|
|
@@ -600,6 +1430,14 @@ export function agentActorWith(
|
|
|
600
1430
|
// settlement). It also closes when restore dropped `prompt` (`attach` rides instead):
|
|
601
1431
|
// there is nothing identical to replay, and the thrown settlement still carries the
|
|
602
1432
|
// legible runaway reason.
|
|
1433
|
+
// A Harness restart that cut the Turn while its state was busy (ADR-0031): no fault. The
|
|
1434
|
+
// workflow did not stop, so neither does the Turn — it waits for Rest, then re-admits.
|
|
1435
|
+
const readmitted = await readmitAfterCut(err);
|
|
1436
|
+
if (readmitted === "stopped") return;
|
|
1437
|
+
if (readmitted !== undefined) {
|
|
1438
|
+
admission = readmitted;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
603
1441
|
const reason = stopped || signaled ? undefined : runawayReason(err);
|
|
604
1442
|
if (reason === undefined || input.continuation || input.prompt === undefined || rerolls >= runawayBudget)
|
|
605
1443
|
throw err;
|
|
@@ -615,16 +1453,30 @@ export function agentActorWith(
|
|
|
615
1453
|
// `/mcp/<currentIid>`, so its address must be live before the Harness can run it.
|
|
616
1454
|
currentIid = `${instanceId}-r${rerolls}`;
|
|
617
1455
|
registerSurface(currentIid);
|
|
618
|
-
admission = await
|
|
619
|
-
{ ...framed, attach: undefined, instanceId: currentIid },
|
|
620
|
-
{ definition, signal: controller.signal },
|
|
621
|
-
);
|
|
622
|
-
ledger(admission);
|
|
1456
|
+
admission = await admitTurn({ ...framed, attach: undefined, instanceId: currentIid });
|
|
623
1457
|
continue;
|
|
624
1458
|
}
|
|
625
|
-
|
|
1459
|
+
// THE ASK-AGAIN RULE (ADR-0016): while the Turn is open, a model that stopped is asked again
|
|
1460
|
+
// once its Allowed picks are not empty — and never with an empty list. One rule, three
|
|
1461
|
+
// moments:
|
|
1462
|
+
// - inside the Submission, the Harness's step boundary (`between-steps.ts`) restates the
|
|
1463
|
+
// Turn part when the list changed since the model last read it; that message keeps the
|
|
1464
|
+
// engine's loop going, and costs no Submission;
|
|
1465
|
+
// - after it settles with picks LEFT, the NUDGE, here;
|
|
1466
|
+
// - after it settles with NONE left (the Turn waits on a Gate, a child's invoke, a timer),
|
|
1467
|
+
// the WAKE: this actor watches the invoking Machine and asks the first time a pick
|
|
1468
|
+
// opens. A Turn that was never meant to pick waits until its state exits.
|
|
1469
|
+
// The nudge and the wake are one Submission kind — the same text, the same conversation,
|
|
1470
|
+
// the same budget — so a Machine that opens and closes picks under a stopped model cannot
|
|
1471
|
+
// ask it without bound. A Turn whose pick left the Agent's state is over: the invoke was
|
|
1472
|
+
// stopped, and nothing is asked.
|
|
1473
|
+
if (stopped) return;
|
|
1474
|
+
let allowed = allowedNow();
|
|
1475
|
+
const woken = allowed.length === 0;
|
|
1476
|
+
if (woken) allowed = await picksOpen();
|
|
1477
|
+
if (stopped || allowed.length === 0) return;
|
|
626
1478
|
if (nudges >= nudgeBudget) {
|
|
627
|
-
fault(`agent completed its turn without
|
|
1479
|
+
fault(`agent completed its turn without a pick that ends it; still allowed: ${allowed.join(", ")}`);
|
|
628
1480
|
return;
|
|
629
1481
|
}
|
|
630
1482
|
nudges++;
|
|
@@ -632,17 +1484,80 @@ export function agentActorWith(
|
|
|
632
1484
|
kind: "retry",
|
|
633
1485
|
child: self._parent?.id ?? self.id,
|
|
634
1486
|
attempt: nudges,
|
|
635
|
-
reason: "no-signal nudge",
|
|
1487
|
+
reason: woken ? "picks allowed again" : "no-signal nudge",
|
|
636
1488
|
});
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1489
|
+
// The nudge and the wake ask the same conversation, so they say `kept`: the Harness can
|
|
1490
|
+
// lose it while the Turn waits (the Instance Harness's restart is on no watch), and a
|
|
1491
|
+
// Harness that keeps none then refuses, never seating an empty conversation whose model
|
|
1492
|
+
// reads the nudge with no task. On the refusal the Turn takes the lost-conversation path,
|
|
1493
|
+
// as the re-admission after a cut does: admitted as a Turn with its task, or a fault when
|
|
1494
|
+
// there is no task to send. Its prompt is the task alone, as a continued Turn's is after
|
|
1495
|
+
// the same refusal: nothing was cut, so the re-admit text would not be true. The ask was
|
|
1496
|
+
// spent and said; the re-admission is said too.
|
|
1497
|
+
try {
|
|
1498
|
+
admission = await admitTurn(
|
|
1499
|
+
{
|
|
1500
|
+
...framed,
|
|
1501
|
+
attach: undefined,
|
|
1502
|
+
instanceId: currentIid,
|
|
1503
|
+
prompt: NUDGE_PROMPT,
|
|
1504
|
+
},
|
|
1505
|
+
{ notices: "nudge", task: false, kept: true },
|
|
1506
|
+
);
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
const task = input.prompt ?? input.attach?.task;
|
|
1509
|
+
if (!isNotKept(err) || task === undefined) throw err;
|
|
1510
|
+
if (stopped) return;
|
|
1511
|
+
admission = await admitOntoLost(task, [], [], reraiseUnheard(true));
|
|
1512
|
+
binding.telemetry?.({
|
|
1513
|
+
kind: "retry",
|
|
1514
|
+
child: self._parent?.id ?? self.id,
|
|
1515
|
+
attempt: nudges,
|
|
1516
|
+
reason: "re-admitted on a new conversation",
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
642
1519
|
}
|
|
643
1520
|
} catch (err) {
|
|
644
1521
|
if (stopped) return;
|
|
645
|
-
|
|
1522
|
+
// The Harness restarted under this Turn (ADR-0021): whatever the stream said as it was cut,
|
|
1523
|
+
// the restart is why.
|
|
1524
|
+
// A Settlement that names the restart is the same fault in the Harness's words (ADR-0021
|
|
1525
|
+
// wording, ADR-0031's rebuilt stream), so it carries the same `Turn lost` prefix.
|
|
1526
|
+
const restartSettled = restarted === undefined ? settledByRestart(err) : undefined;
|
|
1527
|
+
const message =
|
|
1528
|
+
restarted !== undefined
|
|
1529
|
+
? harnessRestartReason(restarted)
|
|
1530
|
+
: restartSettled !== undefined
|
|
1531
|
+
? `Turn lost (${restartSettled})`
|
|
1532
|
+
: (providerLimitReason(err) ?? errorMessage(err));
|
|
1533
|
+
// A lost conversation may be a memory kill (ADR-0061): the kernel's group kill took the
|
|
1534
|
+
// Harness with the Agent's processes. The operator publishes the container's last end on
|
|
1535
|
+
// the Sandbox, so the fault is NAMED — the fixed prefix `memory limit` — instead of
|
|
1536
|
+
// "Turn lost", and the Machine's policy can tell the two apart.
|
|
1537
|
+
//
|
|
1538
|
+
// The fault reaches the Machine (ADR-0016); the next Agent in this Workspace is told
|
|
1539
|
+
// through the Briefing, never through the Frame (ADR-0057, ADR-0062) — a notice raised
|
|
1540
|
+
// BEFORE the fault, because the state it routes to may admit in the same macrostep. Keyed
|
|
1541
|
+
// by the kill itself, so every Turn the one kill ended raises one notice between them.
|
|
1542
|
+
// A restart the Settlement reported before the watch did may still be a memory kill: the
|
|
1543
|
+
// Sandbox is asked, as for a 404.
|
|
1544
|
+
const lostHarness =
|
|
1545
|
+
restarted !== undefined
|
|
1546
|
+
? restarted.lastTerminated?.reason === OOM_KILLED
|
|
1547
|
+
: isLostConversation(err) || restartSettled !== undefined;
|
|
1548
|
+
const kill = lostHarness && sandbox !== undefined ? await memoryFault(sandbox) : undefined;
|
|
1549
|
+
if (stopped) return;
|
|
1550
|
+
if (kill !== undefined && noticeWorkspace !== undefined) {
|
|
1551
|
+
binding.raiseNotice?.(
|
|
1552
|
+
memoryLimit({ limit: kill.limit, agent: input.agentName }),
|
|
1553
|
+
noticeWorkspace,
|
|
1554
|
+
kill.at === undefined ? undefined : kernelSource(noticeWorkspace, kill.at),
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
// A restart the Harness came back from keeps the conversation (ADR-0031); a 404 does not.
|
|
1558
|
+
fault(kill !== undefined ? `${kill.reason}\n ${message}` : message, {
|
|
1559
|
+
conversationKept: restarted !== undefined || restartSettled !== undefined,
|
|
1560
|
+
});
|
|
646
1561
|
}
|
|
647
1562
|
})();
|
|
648
1563
|
|