@jr2/orchestrator 0.6.0 → 0.7.0
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/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +86 -9
- package/docs/concepts/architecture.md +41 -28
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +7 -5
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +4 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +50 -22
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +5 -4
- package/docs/concepts/pools-and-sources.md +187 -11
- package/docs/concepts/secrets-and-trust.md +73 -33
- package/docs/concepts/the-menu.md +11 -0
- package/docs/concepts/workspaces.md +6 -5
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/guides/add-tools-to-the-sandbox.md +55 -11
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +24 -8
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/fix-a-failed-converge.md +18 -6
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +9 -6
- package/docs/guides/look-inside-a-sandbox.md +101 -31
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-one-worker-per-item.md +15 -9
- package/docs/guides/set-an-agents-access-and-dials.md +2 -0
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +1 -1
- package/docs/guides/watch-a-run.md +127 -121
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +17 -7
- package/docs/llms.txt +81 -15
- package/docs/reference/authoring-surface/agent.md +1 -1
- package/docs/reference/authoring-surface/answer.md +1 -1
- package/docs/reference/authoring-surface/arg.md +1 -1
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +1 -1
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +1 -1
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +1 -1
- package/docs/reference/authoring-surface/file.md +1 -1
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +1 -1
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +1 -1
- package/docs/reference/authoring-surface/jr2setup.md +13 -3
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +5 -3
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +1 -1
- package/docs/reference/authoring-surface/pool.md +77 -9
- package/docs/reference/authoring-surface/push.md +1 -1
- package/docs/reference/authoring-surface/readyof.md +1 -1
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +1 -1
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +56 -1
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
- package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-binding.md +1 -1
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
- package/docs/reference/authoring-surface/type-customize.md +16 -1
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-framepath.md +1 -1
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
- package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +1 -1
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +1 -1
- package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
- package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
- package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
- package/docs/reference/authoring-surface/workspace.md +1 -1
- package/docs/reference/authoring-surface/worktree.md +1 -1
- package/docs/reference/authoring-surface.md +88 -167
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +235 -2
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
- package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +6 -4
- package/docs/reference/glossary.md +96 -42
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +976 -149
- package/docs/why.md +2 -1
- package/package.json +10 -3
- package/src/actor.ts +36 -10
- package/src/agent.ts +50 -0
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +52 -7
- package/src/config.ts +550 -1
- package/src/conversations.ts +453 -0
- package/src/custodian.ts +8 -2
- package/src/customize.ts +143 -4
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +2 -1
- package/src/effect.ts +60 -7
- package/src/effector-wire.ts +272 -27
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +10 -4
- package/src/held-secrets.ts +72 -23
- package/src/holds.ts +2 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +76 -38
- package/src/http.ts +889 -94
- package/src/index.ts +16 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +22 -3
- package/src/machine-doc.ts +26 -1
- package/src/mcp-effect.ts +424 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +64 -2
- package/src/notices.ts +138 -10
- package/src/open-body.ts +25 -11
- package/src/open-effect.ts +17 -4
- package/src/open.ts +4 -2
- package/src/parts.ts +87 -9
- package/src/party.ts +2 -1
- package/src/pick.ts +2 -2
- package/src/pool.ts +1142 -163
- package/src/registration.ts +203 -13
- package/src/run-host.ts +1509 -171
- package/src/run-list.ts +191 -0
- package/src/sandbox-kube.ts +53 -5
- package/src/secret-placement.ts +239 -33
- package/src/server.ts +53 -4
- package/src/setup.ts +72 -32
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +263 -22
- package/src/wire.ts +17 -0
- package/src/workspace.ts +50 -6
- package/src/worktree.ts +3 -3
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
|
@@ -15,11 +15,13 @@ else.
|
|
|
15
15
|
|
|
16
16
|
- **The rule.** A name is on the surface when an author writes it in a workflow module or in `jr2.config.ts`. No other
|
|
17
17
|
name is. A name that an author only gets by inference stays off the surface: the event union that `jr2Setup` derives,
|
|
18
|
-
a Gate's input, an Agent's Frame. The `gate` actor is off the surface too. `jr2Setup` registers it, and a state
|
|
19
|
-
invokes it as `src: "gate"`, so no author writes the value.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
a Gate's input, an Agent's Frame. The plain `gate` actor is off the surface too. `jr2Setup` registers it, and a state
|
|
19
|
+
invokes it as `src: "gate"`, so no author writes the value. The `gate()` factory of a Gate slot that hears a Hook is
|
|
20
|
+
on the surface, because an author writes it in `actors` ([ADR-0011](0011-workflow-defined-events.md)); the walk knows
|
|
21
|
+
such a slot by identity, not only by `src === "gate"`. A type is on the surface when an author must write its name,
|
|
22
|
+
for example `Workspaced`, `HostInjectedInput`, `AgentDefinition`, and the types of the parts that `customize()` takes.
|
|
23
|
+
`JR2Config` is on the surface too: the `jr2.config.ts` Reference documents its fields, and an author names it to type
|
|
24
|
+
a config that is built outside `defineConfig()`.
|
|
23
25
|
- **Named exports only.** `src/index.ts` names each export (`export { a } from …`, `export type { T } from …`), in
|
|
24
26
|
groups by topic, and has no `export *`. Thus an Assistant that reads the file sees the full surface, and each change
|
|
25
27
|
to the surface is a line in a diff.
|
|
@@ -73,14 +75,14 @@ another Agent slot (a nested Turn: its picks are its own) or a `gate` (its trans
|
|
|
73
75
|
leaves out everything below that child; a child that runs anything else stays in. A state that invokes `gate` gets its
|
|
74
76
|
accepted set from its own and its ancestors' transitions and those written on its plain children, by the same descent:
|
|
75
77
|
position gives a Gate the writings below it that no other party answers for, as it gives an Agent its modes (the Pool's
|
|
76
|
-
|
|
77
|
-
code. Mechanics: the **vocabulary** derivation is static, in `jr2Setup.createMachine` —
|
|
78
|
-
invoke's `input` to append the derived names, so names still ride serializable input and the
|
|
79
|
-
invoke-time validation are unchanged. The same walk unions the derived sets of every state
|
|
80
|
-
name into that Agent's **Menu** — its tools, fixed for a conversation so the provider's
|
|
81
|
-
of state. The walk reads transition _keys_ and so cannot see guards, which is why what a
|
|
82
|
-
top of it: `agentSurface` asks the invoking machine's guards, and the Turn is told its
|
|
83
|
-
set, less every event that the live snapshot, in its active mode, would not take
|
|
78
|
+
`source` Gate, on its root, takes the writing on `parked`). The consumer names neither, and the Menu's `mcp__jr2__` tool
|
|
79
|
+
names appear nowhere in workflow code. Mechanics: the **vocabulary** derivation is static, in `jr2Setup.createMachine` —
|
|
80
|
+
a config walk wraps each invoke's `input` to append the derived names, so names still ride serializable input and the
|
|
81
|
+
ADR-0007 restore path and invoke-time validation are unchanged. The same walk unions the derived sets of every state
|
|
82
|
+
that invokes an Agent of one name into that Agent's **Menu** — its tools, fixed for a conversation so the provider's
|
|
83
|
+
prompt cache survives a change of state. The walk reads transition _keys_ and so cannot see guards, which is why what a
|
|
84
|
+
Turn may pick is not static on top of it: `agentSurface` asks the invoking machine's guards, and the Turn is told its
|
|
85
|
+
**Allowed picks** — the derived set, less every event that the live snapshot, in its active mode, would not take
|
|
84
86
|
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md) owns the rationale).
|
|
85
87
|
Authoring is untouched by the split: a workflow names no tools in either leg. The same walk feeds `jr2 visualize` ("this
|
|
86
88
|
state's agent can call X, Y"). Dotted names (`agent.*`, `workspace.lost`, `xstate.*`, `after`) are mechanically
|
|
@@ -89,7 +91,9 @@ excluded.
|
|
|
89
91
|
**A transition belongs to the party that its state, or the nearest state above it, invokes.** The party is the Agent
|
|
90
92
|
slot or the `gate` that state invokes, or both when one state invokes both; root transitions, with no such state above,
|
|
91
93
|
belong to both. That is who may send an event whose def leaves `audience` unset
|
|
92
|
-
(`defineEvent({ audience?: "agent" | "external" | "any" })`, no default)
|
|
94
|
+
(`defineEvent({ audience?: "agent" | "external" | "any" })`, no default). One case reads as `external`: an event in a
|
|
95
|
+
Gate slot's `delivers` whose def leaves the audience unset, so a Hook's event is never on the Menu of an Agent below
|
|
96
|
+
that Gate ([ADR-0011](0011-workflow-defined-events.md)):
|
|
93
97
|
|
|
94
98
|
| Transition written on | Unset audience: who may send |
|
|
95
99
|
| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
@@ -9,35 +9,219 @@ done — was ~90 lines of hand-rolled bookkeeping in the jr-parity exercise (`ac
|
|
|
9
9
|
`spawnChild` top-level or visualize goes blind" footgun). No xstate-ecosystem primitive exists for this (verified: the
|
|
10
10
|
canonical v5 pattern _is_ the hand-rolled loop), so jr2 ships one.
|
|
11
11
|
|
|
12
|
+
The webhook round of 2026-11-03 added the other half: work that starts from the outside. An alert service sends one
|
|
13
|
+
alert, and each alert needs one run of a triage Workflow. A forge labels an issue, and each labeled issue needs one run
|
|
14
|
+
of a ticket Workflow. A Hook ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md))
|
|
15
|
+
never starts a run itself. It reaches two things that exist: a Gate, which answers a run
|
|
16
|
+
([ADR-0011](0011-workflow-defined-events.md)), and a Source, which brings items. So this ADR gives the Source a push
|
|
17
|
+
half, lets a Pool's worker be a run of another Workflow, and lets such a Pool stand.
|
|
18
|
+
|
|
12
19
|
## `pool(worker, spec)` — a machine factory, like `workspace()`
|
|
13
20
|
|
|
14
|
-
`pool(worker, { source, itemId, cap, itemInput, onDrained, failures })` returns a plain machine that owns
|
|
21
|
+
`pool(worker, { source, itemId, cap, itemInput, onDrained, failures, standing })` returns a plain machine that owns
|
|
15
22
|
spawn-under-cap, stable child identity, typed completion collection (`DoneActorEvent<Output, string>` + the
|
|
16
|
-
`xstate.done.actor.*` wildcard — zero casts), `stopChild` bookkeeping, the
|
|
17
|
-
top-level `spawnChild` lives in jr2's pool code, once — the visualize footgun is dissolved by construction, and
|
|
23
|
+
`xstate.done.actor.*` wildcard — zero casts), `stopChild` bookkeeping, the Source's Gate, and the re-query timer. The
|
|
24
|
+
one top-level `spawnChild` lives in jr2's pool code, once — the visualize footgun is dissolved by construction, and
|
|
18
25
|
everything durable is a registered string src (xstate cannot persist inline-src children, which independently forces
|
|
19
26
|
this shape). The pool composes: it returns a plain machine and can itself be nested as a child. The typed idioms
|
|
20
27
|
(wildcard done events, `assertEvent`, outcome-in-context root output) ship as documented patterns regardless, so
|
|
21
28
|
outgrowing the pool means hand-rolling without penalty, not fighting a framework.
|
|
22
29
|
|
|
30
|
+
**The worker is a child Machine or a `dispatch(...)` slot, never a bare Workflow name.** A child Machine runs inside the
|
|
31
|
+
Pool's run, as it always did. A `dispatch` slot ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)) makes
|
|
32
|
+
each item a run of its own: `pool(dispatch("triage", { input: triageDoor, wait: true }), { … })`. The two worker kinds
|
|
33
|
+
differ in five places — a worker's defect, a cancel of the Pool, what `customize` reaches, the life of an item's key,
|
|
34
|
+
and where the Gates are — and each difference is one that ADR-0071 states between a child and a dispatched run. So they
|
|
35
|
+
belong to the worker kind, and the word `dispatch` at the call site shows them. `pool("triage", …)` would hide all five
|
|
36
|
+
behind a string, so jr2 does not offer it. Inside, one claim loop serves both kinds with two internal policies.
|
|
37
|
+
|
|
23
38
|
## The source port is generalized; Work Source is one adapter
|
|
24
39
|
|
|
25
|
-
The pool draws from a **Source** port
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
The pool draws from a **Source** port in one of three forms:
|
|
41
|
+
|
|
42
|
+
- **Pull**: `next({ active, settled, input }) → item | null`, plus an optional `wake` event def (for `jr2 send`) and
|
|
43
|
+
`pollEvery` (the set changes under the Pool). `input` is the Pool's input and `settled` holds the ids of the finished
|
|
44
|
+
and failed items, so a Source can fan out over a plan with no state of its own.
|
|
45
|
+
- **Read**: `read`, an `http` Effect marked `idempotent: true` or a promise (`pool()` refuses another Effect,
|
|
46
|
+
[ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md), as a Gate's look does),
|
|
47
|
+
and `items`, which turns its answer into the items in order (by default the answer itself, when it is a list). The
|
|
48
|
+
read's input is the Pool's input. It is the re-queried set, read through an Effect, so the read can spend a secret
|
|
49
|
+
that the Instance Effector holds ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which
|
|
50
|
+
a `next` in the Orchestrator cannot. A promise read holds no secret, and the default tier reads with one. A failed
|
|
51
|
+
read gives no items.
|
|
52
|
+
- **Push**: `item`, a zod schema, and `hooks` whose handlers carry each item. Nothing can be read again: each item comes
|
|
53
|
+
in a delivery, and the Pool holds it (below).
|
|
54
|
+
|
|
55
|
+
A pull or a read Source can hear a Hook too. Its `hooks` handlers give `reread()`: read the set again now. **When a
|
|
56
|
+
Source can read, the read is the truth**: a delivery only says "read again", and an item that lost its label while it
|
|
57
|
+
waited is not in the next read. A handler of a Source has the shape of a Gate's (by Hook kind, then event key) and reads
|
|
58
|
+
`{ payload, caller, delivery, input }`. It gives `reread()`, `carry(item)` or `skip(reason)`, and never a bare
|
|
59
|
+
`undefined`: a delivery that brings nothing has a reason that a person can read in the Hook's answer, the hook log and
|
|
60
|
+
`jr2 hooks`. A throw is a skip with the error as its reason. `skip` is for a Source's handlers only; a Gate's handler
|
|
61
|
+
gives an event or `undefined`. A verdict that the Source's form cannot take (`carry` from a pull or a read Source,
|
|
62
|
+
`reread` from a push Source) and an item that the `item` schema refuses are each a skip, with the reason. **An item
|
|
63
|
+
whose `itemId` throws or gives no id gets no worker, in each form of Source.** In a delivery to a push Source, it is a
|
|
64
|
+
skip with the reason. In a read, the read skips that item, with a `pool` mark `skipped` on the feed that says which item
|
|
65
|
+
and why, and it claims the rest. A read at each `pollEvery` finds the item again, so jr2 marks each such item one time
|
|
66
|
+
in a run's life. Else one item with no id would make each read give no items, with no mark, and no item of that Source
|
|
67
|
+
would start. A `next` gives one item at a time, so its item with no id ends the run `error`, and the streak below pauses
|
|
68
|
+
the Pool when that does not stop. Mechanically the push half is the Pool's `source` Gate, which heard only `wake`
|
|
69
|
+
before, so the Hook's fan-in reaches it as it reaches any open Gate (ADR-0011). A Source may also give
|
|
70
|
+
`reply(verdict, { caller })`: pure, from its own verdict to plain text or `undefined`, which the person who acted on a
|
|
71
|
+
chat service reads, and nobody else; a `skipped` text shows only when no other target gave one (ADR-0011). The verdict
|
|
72
|
+
is the line of `sources`, and for `held`, `existing` and `hold-full` also the parsed `item`. A paused Pool has no run,
|
|
73
|
+
so no `reply` runs for `paused`. The reply after the work is a plain `http` Effect of the Machine.
|
|
74
|
+
|
|
75
|
+
The claim loop has three paths: `next`, the read, and the held items. A `reread`, a `wake` or a settled worker that
|
|
76
|
+
comes during a read (or during a call of `next`) sets a latch, and the loop reads once more after that read, so no
|
|
77
|
+
change is lost. Reads coalesce: one read claims every free slot, and the loop does not read after each claim and each
|
|
78
|
+
settled item one by one. The Pool drops a known id before it claims, so a read that finds an active or settled item
|
|
79
|
+
starts nothing. For a Pool of runs, an item that started a run has one record of "settled": the store's key check
|
|
80
|
+
(below). An item that ended with no run (`start-refused`, `dispatch-error`, `unreachable`) has no row in the store, so
|
|
81
|
+
the Pool counts its key as known for the rest of its run's life: else the read that its own settle starts would claim it
|
|
82
|
+
again at once, with no pause and no end, and one item that the callee's door refuses would hold the Orchestrator's event
|
|
83
|
+
loop. The next run of the Pool (a hand-over or a resume) tries such an item again, so a publish that fixes the door or
|
|
84
|
+
the `itemInput` takes it. The Pool keeps no settled keys past its own run's life: it keeps the run and the status of
|
|
85
|
+
each item that ended in that life, which the bound of a standing run limits, and which a `next` reads as `settled`. A
|
|
86
|
+
`next` that gives an item whose run ended before gets that run (`existing`), whose wait ends at once, so the item
|
|
87
|
+
settles and is not asked for again.
|
|
31
88
|
|
|
32
|
-
|
|
89
|
+
tk's claim actor is one adapter; GLOSSARY.md's **Work Source** reframes as _the Source adapter for tickets_, and the
|
|
90
|
+
Ready-set doctrine (re-query, never materialize) survives as that adapter's semantics, not the primitive's. A Work
|
|
91
|
+
Source — `tk`, GitHub issues, Jira, a task queue — is a behavior port, not a canonical data schema: verbs like
|
|
33
92
|
`claimNext`, `updateStatus` and `comment`, each gated by a capability the adapter advertises. It owns the dependency
|
|
34
93
|
graph and hands out the Ready-set, the currently-unblocked items in dependency order, so the Machine never encodes a
|
|
35
94
|
dependency edge. The Ready-set mutates during execution — items can be reopened or created mid-run — so it is not a
|
|
36
|
-
static FIFO queue.
|
|
95
|
+
static FIFO queue. An adapter can give both halves of a Source: its read and its Hook's handlers, so the mapping from
|
|
96
|
+
the service's data to an item is written once.
|
|
97
|
+
|
|
98
|
+
## A pushed item is held until the Pool claims it
|
|
99
|
+
|
|
100
|
+
- **The Pool holds each carried item in its run's blob**, in the order of arrival, until it claims it. An item that
|
|
101
|
+
leaves for a worker is no longer held: the worker, by its key, is the record. The Hook's answer leaves after the save
|
|
102
|
+
that records the item, so a `held` answer is durable.
|
|
103
|
+
- **The key check comes before the hold.** An item whose key a held item, an active worker, or a run of the worker's
|
|
104
|
+
Workflow holds is `existing`, and the answer names the run when one holds it.
|
|
105
|
+
- **`hold` bounds the held items**, 1000 by default. Above it, the Hook's answer is 429 (for a chat command, 200 with a
|
|
106
|
+
text that says why, ADR-0011) and the hook log keeps the delivery. The 1000 is not measured.
|
|
107
|
+
- **What a push loses**: a delivery while the Orchestrator is down, while no Assembly is current (503), while the Pool
|
|
108
|
+
is paused (`paused`), and above `hold`. A read Source loses nothing, because the next read finds the item.
|
|
109
|
+
- **A 429 does not claim the delivery's id.** The hook log keeps it as `hold-full`, and a repeat is found only by a
|
|
110
|
+
delivery that the door claimed (ADR-0011), so the service's send again is heard: a Gate that took it passes it over by
|
|
111
|
+
the run's answers, and a Pool that holds its item answers `existing`.
|
|
112
|
+
- **A Source's verdict on a delivery** is one of `held`, `existing`, `hold-full`, `reread`, `skipped` and `paused`, and
|
|
113
|
+
it goes in the delivery's outcome as a line of `sources` (ADR-0011).
|
|
114
|
+
|
|
115
|
+
## A Pool of runs, and a Pool that stands
|
|
116
|
+
|
|
117
|
+
A Pool whose worker is a `dispatch` slot keys each item's run by `itemId`, scoped to the callee Workflow
|
|
118
|
+
([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)): every Pool, every hand-over and every `jr2 run` that
|
|
119
|
+
gives the same key for that Workflow gets the same run while its row lives, live and for `runs.retain` after it ends
|
|
120
|
+
([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)). `cap` counts the live runs that
|
|
121
|
+
this Pool dispatched, so its slot must say `wait: true`: with `started` alone, the Pool cannot know when a run ends. A
|
|
122
|
+
run that ends in any way frees its slot. A run that ends `failed`, `error`, `cancelled`, `lost` or `drifted`, or a start
|
|
123
|
+
that the door refuses, is a failure of its item, and never the Pool's `error`: the run is not the Pool's child. A cancel
|
|
124
|
+
of the Pool never cancels its runs (ADR-0071), and `customize(pool, { agents })` reaches nothing in them
|
|
125
|
+
([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)). The Pool spawns its slot with the item's id as
|
|
126
|
+
the spawn id, and the Effect sends that id as the key, not its ledger row id. The run's `dispatchedBy` gains `key`, and,
|
|
127
|
+
for a carried item, `hook: { name, delivery, onBehalfOf? }`, so `jr2 status` prints which Pool, key and delivery started
|
|
128
|
+
it. Neither is on the open band (ADR-0014): a key is the workflow's data. A run that a read claimed names no delivery:
|
|
129
|
+
the Pool's feed says why an item did or did not start, with a `pool` mark for each start (`started`), each end
|
|
130
|
+
(`settled`, with the run's status, `start-refused` or `dispatch-error`), each held item that a hand-over drops
|
|
131
|
+
(`carry-refused`) and each item of a read that has no id (`skipped`). A `wait` that the slot's timeout ends while the
|
|
132
|
+
run is still live (`status: "live"`) is sent again under its key, so the wait follows the run and `cap` still counts it.
|
|
133
|
+
|
|
134
|
+
**`standing: true` makes a Pool of runs stand.** It is two things:
|
|
135
|
+
|
|
136
|
+
- **Parking.** A standing Pool never ends by itself. It has no `drained` and no `deadlocked` end, no output and no
|
|
137
|
+
`failures` mode. When it has no work it waits for a delivery, a `reread` or its `pollEvery`. An item that ends is a
|
|
138
|
+
`settled` mark on the feed, with the run's status.
|
|
139
|
+
- **The standing run.** jr2 keeps one live run of the Workflow under the current Assembly
|
|
140
|
+
([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). Nobody starts it: jr2
|
|
141
|
+
starts it at boot and at the publish that adds it, and `jr2 run` of it is refused, except to resume it (below). A
|
|
142
|
+
dispatch to a standing Workflow is refused 409. jr2 **hands it over** to a new run at every publish, at drift, after
|
|
143
|
+
`error` (with back-off), and when its blob passes its bound. At every publish, because the Assembly is one hash and a
|
|
144
|
+
handler has no stamp of its own. The bound is 500 items taken or one day: each item taken is a dispatch, which keeps a
|
|
145
|
+
row in the Effect ledger for the run's life (ADR-0067), about 217 bytes, so 500 is about 110 KB a save. The bound
|
|
146
|
+
counts the items that the run took, not the keys that a hand-over carried to it: each carried key is sent again, so a
|
|
147
|
+
count of the ledger's rows would hand a run with a `cap` at the bound over again at once. **A run at the bound in
|
|
148
|
+
which no item that it took itself started a run is handed over as a run that ended `error` is**: after the back-off,
|
|
149
|
+
and it counts in the streak below. An item that ends with no run (`start-refused`, `dispatch-error`, `unreachable`,
|
|
150
|
+
`unknown`) is known only for its run's life (above), so a backlog of as many such items as the bound fills the bound
|
|
151
|
+
again in each new run. A hand-over at once would then loop with no pause and no end, and would hold the event loop.
|
|
152
|
+
jr2 ends that run `done`, with no `handedOverTo`, and starts the next run after the back-off. A run in which one item
|
|
153
|
+
that it took started a run (or found its run under its key) is handed over at once, and the count starts again. A
|
|
154
|
+
carried key does not count: the hand-over sends it again and it finds a run that an earlier run started, so a callee
|
|
155
|
+
run that lives for hours would hide the loop for as long as it lives. While each item that the run took is still in
|
|
156
|
+
flight, jr2 does not yet know, so it waits for the save of the first answer. The back-off before the hand-over after
|
|
157
|
+
the `n`-th counted run in a row is 1 s, 4 s, then 16 s. The numbers are not measured. The hand-over carries the held
|
|
158
|
+
items, parsed by the new Assembly's `item` schema (an item that does not parse is dropped, with a `carry-refused`
|
|
159
|
+
mark), and the active keys: the new run sends each one again with `wait: true`, the door answers `existing`, and the
|
|
160
|
+
wait follows that run, so `cap` still counts it. A held item is mapped by the new `itemInput`. A hand-over ends the
|
|
161
|
+
old run `done`, and its record carries `handedOverTo: <new run id>`: a standing Pool has no output, so its stint is
|
|
162
|
+
done. The start of the new run and the end of the old one are one synchronous step. A delivery that a Source hears
|
|
163
|
+
across that step (its key lookup awaits the store) goes on to the new run's Source, so the door never claims a
|
|
164
|
+
delivery that no run holds. jr2 keeps the **head** of each standing Workflow in a table of the store: its newest run,
|
|
165
|
+
when that run started, how many counted runs in a row ended, and why it is paused. jr2 writes the head in the same
|
|
166
|
+
step as the start. The sweep of rows older than `runs.retain`
|
|
167
|
+
([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)) never deletes the run that a head
|
|
168
|
+
names, so a paused Pool keeps its held items for as long as it is paused. A Workflow that a publish removes, or that
|
|
169
|
+
no longer stands, gets no hand-over: its live run holds its Assembly and runs on, as any run does (ADR-0069), until a
|
|
170
|
+
person cancels it. A standing run has no Caller: its record's `startedBy` is `{ kind: "jr2", why }`, with `why` one of
|
|
171
|
+
`boot`, `publish`, `hand-over` and `resume`
|
|
172
|
+
([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)).
|
|
173
|
+
- **A cancel is a pause.** `jr2 send <run> --event CANCEL` of a standing run ends that run `cancelled`
|
|
174
|
+
([ADR-0025](0025-cancel-ends-a-run-stop-parks-it.md)) and pauses its Workflow: jr2 starts it again at no boot and no
|
|
175
|
+
publish until `jr2 run <workflow>` resumes it. `jr2 up` and `jr2 status` print the Pool as paused, with the number of
|
|
176
|
+
items it holds. They read it from `GET /assembly` and from the answer of the publish, whose `standing` list gives each
|
|
177
|
+
standing Workflow's state (`live`, `paused` or `waiting`), its run and its count of held items. It holds no key and no
|
|
178
|
+
item, so it is on the open band with the held Assemblies ([ADR-0014](0014-observation-is-open-run-state-is-not.md)). A
|
|
179
|
+
paused Pool has no live run: its held items and active keys stay in the blob of the cancelled run. A resume is a
|
|
180
|
+
hand-over from that run, so the new run takes them and claims under `cap`, and a read Source reads again. The new run
|
|
181
|
+
records the Caller of the `jr2 run` that resumed it as `resumedBy`. On the wire the resume is
|
|
182
|
+
`POST /workflows/<workflow>/runs`, which answers `201 { runId, instanceId, resumed: true }`, and 409 while the
|
|
183
|
+
Workflow is live or waits out a back-off. `jr2 run <workflow> --drop-held` (`?drop-held=true`) resumes with no held
|
|
184
|
+
item, so a person can remove a poison item or a queue that they paused on purpose; a read Source then finds again what
|
|
185
|
+
is still true. A delivery to a paused Pool gets `paused`, and nothing is held, so a push Source loses it and a read
|
|
186
|
+
Source finds it at the resume. A delivery to a standing Pool in its back-off (no live run, and not paused) makes the
|
|
187
|
+
door answer 503 (ADR-0011), so the service sends it again. **One rule counts a run in the streak**, for each end that
|
|
188
|
+
hands over after the back-off: `error`, `failed` (a `fail()` of the Machine,
|
|
189
|
+
[ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md), which jr2 treats as
|
|
190
|
+
`error`) and the bound with no run started. A run in which an item that it took itself, not a carried key, started a
|
|
191
|
+
run starts the count again, and is handed over after 1 s. Any other run counts. So a run that took only items that the
|
|
192
|
+
callee's door refused, and then ended `error`, counts. After 3 hand-overs from counted runs in a row, jr2 pauses the
|
|
193
|
+
Pool when the next counted run ends, so a poison item, or a backlog that the callee's door refuses, cannot loop
|
|
194
|
+
without end. The pause says why: `errors` when the last run ended `error` or `failed`, and `bound` when it took its
|
|
195
|
+
bound; `jr2 status` and the Orchestrator's log line name that end. The 3 is not measured. The Pool's context keeps the
|
|
196
|
+
keys that the hand-over carried, so jr2 tells them from the items that the run took.
|
|
197
|
+
|
|
198
|
+
**What the load walk refuses**, with its fix in the message:
|
|
199
|
+
|
|
200
|
+
- `standing: true` on a Pool of child Machines: a child cannot be handed over.
|
|
201
|
+
- `standing: true` on a Pool that is not the top Machine of its Workflow: jr2 keeps one run for each standing Workflow,
|
|
202
|
+
so the Workflow is the Pool. The fix is to move the Pool to its own file in `workflows/`.
|
|
203
|
+
- A Pool of runs that does not stand. A batch of runs (a Pool of runs that a Caller starts and that ends) is not offered
|
|
204
|
+
now: its `itemId` is a key that lives for `runs.retain` past the run, so a second batch in that time does no work. To
|
|
205
|
+
add it later does not break anything.
|
|
206
|
+
- A Pool of runs whose `dispatch` slot has no `wait: true`, or has a `target`: a foreign worker is not designed.
|
|
207
|
+
- A Pool of runs with no `cap`. The author must choose it; a Pool of child Machines keeps its default of 1.
|
|
208
|
+
- A push Source on a Pool that ends when it drains, which would end `drained` at once. The types refuse it too.
|
|
209
|
+
- A worker that is a bare Workflow name.
|
|
210
|
+
|
|
211
|
+
**What `jr2 up` prints and warns.** For each standing Pool, it prints the chain from each Hook that reaches it to the
|
|
212
|
+
callee: `sentry → alerts → triage`. It warns when two standing Pools dispatch to one Workflow: `cap` is per Pool, so one
|
|
213
|
+
issue with two labels can take a slot in each, and the caps add.
|
|
214
|
+
|
|
215
|
+
**The callee must end the level.** A read Source re-reads a level, such as "the open issues with the label `jr2`". When
|
|
216
|
+
the callee's run ends and the label stays, the key keeps the item from a second run while the run's row lives.
|
|
217
|
+
`runs.retain` after the run ended (30 days by default), the row is swept, and the next read starts the item again. So
|
|
218
|
+
the callee (or the person) removes the label. A drifted callee holds its key for `runs.retain`, and nothing starts that
|
|
219
|
+
item again without a person.
|
|
37
220
|
|
|
38
221
|
## Terminal triage is three-valued, surfaced as status
|
|
39
222
|
|
|
40
|
-
Forced by parking-is-retention (ADR-0012) plus workflows whose children park on gates (escalation, human review)
|
|
223
|
+
Forced by parking-is-retention (ADR-0012) plus workflows whose children park on gates (escalation, human review). It
|
|
224
|
+
holds for a Pool that does not stand:
|
|
41
225
|
|
|
42
226
|
- **drained** — source empty and all children settled → final, or the run ends `failed` when items failed (below);
|
|
43
227
|
- **deadlocked** — open-but-never-ready items with nothing progressing → surfaced distinctly (jr exit 2), not an
|
|
@@ -69,3 +253,33 @@ is a correct answer. The default is `"fail"` because `done` means that the work
|
|
|
69
253
|
not exit 0 while every item failed. The deadlock is the usual end of a dependency backlog with a failure: a Source over
|
|
70
254
|
a tracker keeps the failed item open, so its dependents never become ready. The `status` in the detail tells the two
|
|
71
255
|
ends apart, and the two details have one shape.
|
|
256
|
+
|
|
257
|
+
## Tests
|
|
258
|
+
|
|
259
|
+
The default e2e tier (`features/standing-pool.feature`) and `pool-sources.test.ts` prove the standing Pool of runs: a
|
|
260
|
+
push Source, the held items and `hold`, `cap`, `reread`, the latch and merged reads, a restart and a hand-over with no
|
|
261
|
+
item lost or started twice, a hand-over across a changed callee door, and the key per Workflow. They read with a
|
|
262
|
+
promise, because the default tier has no Instance Effector. The `@kind` tier proves a read through an `http` Effect.
|
|
263
|
+
|
|
264
|
+
## Considered options
|
|
265
|
+
|
|
266
|
+
- **An Intake**: a kit-shipped `intake({ routes })`, each `route({ start, input, hooks, look, cap })`, that jr2 keeps as
|
|
267
|
+
a **Standing Workflow**. Rejected: it needs three concepts (Intake, route, Start) and two GLOSSARY terms, and reuses
|
|
268
|
+
nothing that an author knows about a Pool, though it is a Pool inside. It writes the mapping from a delivery to a
|
|
269
|
+
start twice, once in the handler and once in the look, and its look starts an issue that lost its label while it
|
|
270
|
+
waited. One intake run serves every route, so its fault stops every route; a standing Pool for each Source keeps a
|
|
271
|
+
fault in one Source.
|
|
272
|
+
- **A direct door**: a Hook that starts a run itself (`door()`, or a Listener's `startOn(machine, kind, …)`). Rejected:
|
|
273
|
+
it is the smallest form for one alert, but it cannot grow. A cap, a read of a level, a queue and an answer after a
|
|
274
|
+
start are each state over time, which a door does not have, and each needs a standing Machine. So an Instance gets two
|
|
275
|
+
ways to start a run. It also makes a Hook more than a door that checks and decides nothing (ADR-0078).
|
|
276
|
+
- **A router that the author writes**: a hand-written standing Machine with `dispatch({ key })`, regions and a table of
|
|
277
|
+
runs in flight. Rejected: about 90 lines of mechanism for 25 lines of intent. When a judge wrote one alert's router
|
|
278
|
+
from it, the result had no hand-over and no routes for a child: its table went stale at each publish, and the first
|
|
279
|
+
refused start faulted it. A run holds its Assembly, so a router keeps its old handlers after a publish; a router that
|
|
280
|
+
ends `error` has no open Gate, so each later delivery reaches nothing; and a Kit bump drifts it and loses its backlog.
|
|
281
|
+
- **Two factories over one Source port**: `pool()` for children and batches, and a second factory for standing Pools of
|
|
282
|
+
runs. Rejected: the split removes three rows from the table of differences, and costs a GLOSSARY term for the second
|
|
283
|
+
factory, which is the Intake again.
|
|
284
|
+
- **A batch Pool of runs now.** Rejected for now, as above: no scenario needs it, and its key makes a second batch
|
|
285
|
+
within `runs.retain` do nothing. It adds later without a break.
|
package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md
CHANGED
|
@@ -7,9 +7,9 @@ hands: the [ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-
|
|
|
7
7
|
Submission — forget it and the Agent is silently mute, the same footgun class ADR-0016 removed from workflows), the
|
|
8
8
|
ADR-0004/0012 sandbox geography (working tools execute in the Harness container against `/work`), the dependency pin
|
|
9
9
|
that must match the Orchestrator's wire, and the image contracts the operator imposes (numeric uid, `:8080` = Ready, git
|
|
10
|
-
present). The only content a user genuinely owns is the
|
|
11
|
-
GLOSSARY.md already promised the split — an Agent is a definition jr2 maps into the Harness, and the Instance
|
|
12
|
-
holds only what a Machine cannot carry (ADR-0049/0050: the definition is one of the things it can).
|
|
10
|
+
present). The only content a user genuinely owns is the Agent's definition: model + instructions, and the MCP servers it
|
|
11
|
+
uses. GLOSSARY.md already promised the split — an Agent is a definition jr2 maps into the Harness, and the Instance
|
|
12
|
+
folder holds only what a Machine cannot carry (ADR-0049/0050: the definition is one of the things it can).
|
|
13
13
|
|
|
14
14
|
## Decision
|
|
15
15
|
|
|
@@ -53,6 +53,12 @@ holds only what a Machine cannot carry (ADR-0049/0050: the definition is one of
|
|
|
53
53
|
Stand-in. `harness.provider.apiKey` is one implicitly: the held secret `JR2_PROVIDER_API_KEY`, bound to `baseUrl`'s
|
|
54
54
|
host, so it leaves the `jr2-harness-env` Secret and `baseUrl` must be `https://` when a key is set. A known model key
|
|
55
55
|
in `harness.env`, or in an `envFrom` Secret, is refused.
|
|
56
|
+
- **MCP is the one extension contract for tools.** Beyond its Working tools and its Menu, an Agent gets tools only from
|
|
57
|
+
the MCP servers that its definition names (`mcp`), and jr2 has no tool API of its own
|
|
58
|
+
([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)). The
|
|
59
|
+
definition names a server by the name that the `mcp` block of `jr2.config.ts` gives it, never by its url or its
|
|
60
|
+
secret: as `harness` declares reach for models, the `mcp` block declares reach for tools, and the definition makes the
|
|
61
|
+
choice.
|
|
56
62
|
- **A catalog provider can be moved to a gateway, and stays a catalog provider.** `harness.catalog.<id>.baseUrl`
|
|
57
63
|
replaces the base URL of every model of one pi catalog provider — a LiteLLM gateway in front of Bedrock, for
|
|
58
64
|
`anthropic/…`. The catalog keeps its limits, cost and thinking levels; only the endpoint moves. It is reach, not a
|
|
@@ -99,11 +105,13 @@ Definitions keep converge-time safety; overrides pay the later check for the fle
|
|
|
99
105
|
|
|
100
106
|
## Consequences
|
|
101
107
|
|
|
102
|
-
- The definition contract is data-only: what an Agent may do to the Workspace is part of it (`access`, ADR-0028)
|
|
103
|
-
|
|
104
|
-
hatch to a foreign harness project
|
|
108
|
+
- The definition contract is data-only: what an Agent may do to the Workspace is part of it (`access`, ADR-0028), and so
|
|
109
|
+
are the MCP servers it uses (ADR-0079); an author's own tool is an MCP server. Skills and subagents have no seat yet.
|
|
110
|
+
When someone needs them, the contract grows deliberately — there is no eject hatch to a foreign harness project
|
|
111
|
+
(ADR-0027 deleted it).
|
|
105
112
|
- Because definitions are plain data, stock ones compose with no API: a Machine carries `agent(coder)`, and a consumer
|
|
106
|
-
retunes it with `customize(machine, { agents: { coder: { model: "…" } } })` (ADR-0049, ADR-0009)
|
|
113
|
+
retunes it with `customize(machine, { agents: { coder: { model: "…" } } })` (ADR-0049, ADR-0009), or rebinds its
|
|
114
|
+
servers with `{ mcp: [...] }` in the same place.
|
|
107
115
|
- An invalid definition fails at admission, loudly, naming the slot: a model no registry resolves, a malformed field. A
|
|
108
116
|
Machine with no Agent slots is valid — a `workspace()` body parking a Sandbox (ADR-0012) invokes none. Readiness is
|
|
109
117
|
binding `:8080`, with no boot build ahead of it (ADR-0027).
|
|
@@ -66,11 +66,12 @@ loudly narrated, safe to re-run:
|
|
|
66
66
|
It is an ordinary converge failure: it names each node whose agent reports none, the probe's reason, and the fix (keep
|
|
67
67
|
the node out with `sandbox.nodeSelector` or a taint, ADR-0052, or enable Landlock); a run after the fix converges
|
|
68
68
|
([ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)).
|
|
69
|
-
- **Provider preflight**: `up` checks each provider that a carried model names
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Harness takes. If it does not, a
|
|
69
|
+
- **Provider preflight**: `up` checks each provider that a carried model names, and the tool list of each remote MCP
|
|
70
|
+
server that an Agent names (below). It checks from inside the cluster, with one trivial tool-call completion for a
|
|
71
|
+
model. An endpoint that the pods cannot reach, a wrong or missing key, or a vLLM without `--enable-auto-tool-choice`
|
|
72
|
+
then fails at converge time, not as `agent.fault` in a run. Reachability is the user's concern (`localhost` never
|
|
73
|
+
works from a pod; a LAN address does on kind). The check must take the path that the Harness takes. If it does not, a
|
|
74
|
+
pass means nothing.
|
|
74
75
|
- **One path for every provider.** `up` checks the custom provider (`harness.provider`, ADR-0018) and a provider of
|
|
75
76
|
pi's catalog (for example `anthropic/…`) in the same way. A key of either is a held secret, and only a Custodian can
|
|
76
77
|
use it ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). So no key enters a pod
|
|
@@ -106,8 +107,8 @@ loudly narrated, safe to re-run:
|
|
|
106
107
|
model runs where the Instance Harness runs.
|
|
107
108
|
- The held set: a pod that holds a secret sends its HTTPS through its Custodian, and a tunnel carries a host that no
|
|
108
109
|
secret is bound to. A pod that holds no secret sends its HTTPS direct. So the check pod holds the held set of the
|
|
109
|
-
first pod of its class, and the keys that the other pods of the class hold. A class
|
|
110
|
-
check pod that holds nothing.
|
|
110
|
+
first pod of its class, and the keys and the MCP server secrets that the other pods of the class hold. A class
|
|
111
|
+
that holds nothing gets a check pod that holds nothing.
|
|
111
112
|
|
|
112
113
|
When no pod of the class holds a key of the provider, the check pod holds none. Then pi refuses the call
|
|
113
114
|
(`not configured`), and the converge fails. A signing credential in `harness.env` reaches the check pod, as it
|
|
@@ -115,6 +116,16 @@ loudly narrated, safe to re-run:
|
|
|
115
116
|
knows, and for which jr2 knows no key name, is checked with no held key, as a Turn runs it. The check pod has the
|
|
116
117
|
priority class of the Sandbox tier, as the Harness pods have (ADR-0060).
|
|
117
118
|
|
|
119
|
+
- **MCP servers.** The check pod also checks each remote MCP server that an Agent of its class names
|
|
120
|
+
([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)). It
|
|
121
|
+
reads the server's `tools/list` through its Custodian, with the Stand-in of the server's secret, as a Turn reads it.
|
|
122
|
+
An allowlisted tool name that the list does not have fails the converge, and the result names the server and the
|
|
123
|
+
tool. A server that refuses the request (a 401 or a 403: a wrong or expired token) fails the converge, as a model
|
|
124
|
+
key that the provider refuses does. A server that does not answer is a warning: the first Turn of a conversation
|
|
125
|
+
fails with a named error when the server does not answer then. A stdio server is not checked: it runs as Workspace
|
|
126
|
+
code in the Sandbox, and the check pod runs none. Each server has 60 s for its whole listing, after the models. Two
|
|
127
|
+
literals set it, `PREFLIGHT_MCP_BUDGET_SECONDS` in `preflight-line.ts` and `CHECK_MCP_SECONDS` in `up.ts`, and a
|
|
128
|
+
test holds them equal. The pod writes the list, and `up` compares it with the allowlists.
|
|
118
129
|
- **The request.** The check's request shares these with a Turn's: the model, its api, its endpoint and key, the
|
|
119
130
|
completion limit (what pi-ai sends for a Turn), the retries, no forced tool, and the level of a definition with no
|
|
120
131
|
`thinkingLevel` (`off`: no `reasoning` is sent). It does not share these: it has no system prompt, one tool, no
|
|
@@ -195,12 +206,12 @@ loudly narrated, safe to re-run:
|
|
|
195
206
|
|
|
196
207
|
It never deletes the objects of another Instance.
|
|
197
208
|
|
|
198
|
-
- **Skipping it.** `jr2 up --skip-provider-preflight` runs no check pod
|
|
199
|
-
example when a model loads for longer than the check waits.
|
|
200
|
-
fixed. The skip is never silent: `up` writes one warning line,
|
|
201
|
-
|
|
202
|
-
because no flag of `up` has one (CI gives `--yes`
|
|
203
|
-
`agent.fault`.
|
|
209
|
+
- **Skipping it.** `jr2 up --skip-provider-preflight` runs no check pod, so it checks no model and no MCP server's
|
|
210
|
+
tool list. A check can fail when the model works, for example when a model loads for longer than the check waits.
|
|
211
|
+
That fault must not stop every converge until jr2 is fixed. The skip is never silent: `up` writes one warning line,
|
|
212
|
+
which names each model and each MCP server that it did not check. `up` still deletes the leftovers of earlier runs,
|
|
213
|
+
because their Secrets can hold a key. The flag has no config key, because no flag of `up` has one (CI gives `--yes`
|
|
214
|
+
as a flag). A model that does not work then fails in a run, as `agent.fault`.
|
|
204
215
|
- **A limit.** The check pod has labels of its own. It does not get the labels of a Harness pod: the Instance Harness
|
|
205
216
|
Service selects its pods by label, and would send Turns to the check pod. So a NetworkPolicy that selects Harness
|
|
206
217
|
pods by label does not select the check pod. When such a policy closes the egress of the Harness pods, the check can
|
|
@@ -229,8 +240,9 @@ takes the operator too.
|
|
|
229
240
|
instance error with "not deployed here — right context?". This is the guardrail against silently pointing at the wrong
|
|
230
241
|
cluster, without a state file that can itself go stale.
|
|
231
242
|
- **CLI transport**: run-verbs port-forward the Orchestrator Service via the kube API for the duration of the command
|
|
232
|
-
and read the Instance token from its Secret — kube RBAC is the real gate. `--url`/`JR2_URL` (+ token env
|
|
233
|
-
|
|
243
|
+
and read the Instance token from its Secret — kube RBAC is the real gate. `--url`/`JR2_URL` (+ token env, any Caller's
|
|
244
|
+
credential, [ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)) is the escape
|
|
245
|
+
hatch for ingress-exposed instances and callers without cluster creds.
|
|
234
246
|
- **A dead ADDRESS and an absent IDENTITY are two faults, and resolution names which.** They share a verb and a context
|
|
235
247
|
but not a fix: one is the network, the other is `jr2 up`. A cluster that answered and holds no instance Secret is "not
|
|
236
248
|
deployed here — right context?"; a cluster that could not answer at all is "cannot reach the cluster — context X: …",
|
|
@@ -274,9 +286,11 @@ from env.
|
|
|
274
286
|
|
|
275
287
|
## Consequences
|
|
276
288
|
|
|
277
|
-
- The everyday story is two commands ever: `jr2 init`, `jr2 up`. The
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
- The everyday story is two commands ever: `jr2 init`, `jr2 up`. The rest of the CLI is in ADR-0009's table, and the
|
|
290
|
+
workspace verbs `workspace ls|shell|forward|rm` are among them.
|
|
291
|
+
- **The workspace verbs are not run-verbs.** They address the Sandbox CRs of the current context with its kube
|
|
292
|
+
credentials alone: no port-forward to the Orchestrator, no Instance token and no Caller scope. So they work when the
|
|
293
|
+
Orchestrator is down, and kube RBAC on the Sandbox CRs and their pods is the whole gate.
|
|
280
294
|
- **Simplification must not surprise**: `up` prompts exactly when meeting a cluster that isn't yet home, and run-verbs
|
|
281
295
|
print the context they're talking to — the ambient-context magic stays visible.
|
|
282
296
|
- The snapshot store (sqlite) moves onto a PVC in the instance's namespace; `.jr2/` shrinks to scratch.
|
|
@@ -57,6 +57,19 @@ end. And a restarted Harness answered a `continue` with no memory of the convers
|
|
|
57
57
|
attached pod. Its lifetime is the state's lifetime, which xstate already manages: it re-invokes on snapshot restore,
|
|
58
58
|
so restore-reconcile stops being a special case and becomes the first tick of the normal loop; and it stops on every
|
|
59
59
|
exit — body final, run stopped, run faulted.
|
|
60
|
+
- **The Lease marks its hold on the CR.** The CR's first write sets the annotation `jr2.dev/hold: held`, and each
|
|
61
|
+
renewal writes it again. When the Lease stops for the body's final state, a fault, a cancel or a parent that leaves
|
|
62
|
+
the Workspace's state, it sets `jr2.dev/hold: released` and renews no more. A defect that a parent catches does not
|
|
63
|
+
stop the Lease, because xstate stops no child of a Machine that ended in error: at its next renewal the Lease sees
|
|
64
|
+
that its Workspace is over, and sets `released` then. A Lease does not set `released` on a name that a live Workspace
|
|
65
|
+
of the run holds: a parent that catches the defect can invoke the same Machine again, and the new Workspace gets the
|
|
66
|
+
same Sandbox name and a Lease of its own. A step of the wrapper from one state to the next writes nothing: the next
|
|
67
|
+
state has a Lease of its own. A park (`RunHost.stop`, [ADR-0025](0025-cancel-ends-a-run-stop-parks-it.md)) and a
|
|
68
|
+
process shutdown write nothing, so a crash keeps the mark `held` and the restored run's Lease writes it again. A
|
|
69
|
+
restore that ends a run `drifted` or `lost` without hosting it starts no Lease, so the Orchestrator sets `released` on
|
|
70
|
+
each CR with that run's `jr2.dev/run` label. So `jr2 workspace rm` tells a held Sandbox from a free one with the
|
|
71
|
+
cluster alone ([ADR-0009](0009-cli-and-instance-interface.md)): the keepalive's age cannot tell it, because a renewal
|
|
72
|
+
can be late by more than one interval and a few missed renewals are survivable.
|
|
60
73
|
|
|
61
74
|
That last point deletes machinery rather than adding it. Three mechanisms with three different owners and lifetimes —
|
|
62
75
|
the process-global heartbeat map, the one-shot probe, and `release(runId)` (which label-queried the cluster to stop
|
|
@@ -98,4 +111,4 @@ behavior `release()` was written to produce is emergent.
|
|
|
98
111
|
independently. This falls out of invoking it beside the body rather than owning it at the host.
|
|
99
112
|
- `SandboxPort` has no optional method: `renew` is a write, `continuity` is a subscription to the watch, and
|
|
100
113
|
`memoryFault` (ADR-0061) answers undefined from a backend that cannot see the container. The `jr2.dev/run` label
|
|
101
|
-
survives for `jr2
|
|
114
|
+
survives for the `jr2 workspace` verbs and as the watch's selector, no longer load-bearing for lease bookkeeping.
|
|
@@ -34,8 +34,8 @@ can get away with fetching once, because a person can press a button; an API can
|
|
|
34
34
|
so there is no gap to detect and no cursor to resume from. Same reconciliation idiom as the Lease (ADR-0021) and
|
|
35
35
|
`jr2 up` (ADR-0019): say the whole truth, repeatedly, and let the reader converge.
|
|
36
36
|
- **Two granularities, one vocabulary.** The per-run feed stays — tracking exactly one run is a legitimate thing to
|
|
37
|
-
want, and it is what `jr2 run` and `jr2
|
|
38
|
-
|
|
37
|
+
want, and it is what `jr2 run` and `jr2 watch` do. Every run-scoped frame carries `runId` even where the route makes
|
|
38
|
+
it redundant, so one parser serves both.
|
|
39
39
|
- **`gone` is a fact, not an inference.** A run can leave the live set _without_ a terminal status: `stop()`
|
|
40
40
|
deliberately leaves the stored status `"live"` so a later `restore()` picks the run back up. A client watching for
|
|
41
41
|
`status === "done"` would show a stopped run as live forever, so departure is stated outright.
|
|
@@ -48,7 +48,7 @@ can get away with fetching once, because a person can press a button; an API can
|
|
|
48
48
|
which is why it is wired to the handler's exit.
|
|
49
49
|
- **A settled run is the client's own memory.** A run appears in the page's `settled` list iff _this page_ watched it
|
|
50
50
|
leave. After a hard reload they are gone, and that is correct rather than a defect: serving a finished run's state to
|
|
51
|
-
an unauthenticated reader is a read-through, which stays behind the
|
|
51
|
+
an unauthenticated reader is a read-through, which stays behind the `read` scope on `/runs/:id` (ADR-0014).
|
|
52
52
|
- **`jr2 visualize` is deleted.** Its job was to find an orchestrator, forward a port, and open a browser. With the page
|
|
53
53
|
now useful on its own, that is one `kubectl port-forward` the reader already knows how to run — and the command's
|
|
54
54
|
ephemeral-fallback path was quietly forwarding to a _different_ instance than the deployed one. Its one unique piece
|
|
@@ -61,7 +61,7 @@ can get away with fetching once, because a person can press a button; an API can
|
|
|
61
61
|
tick, and a reader that keeps up drains them before any ping judges it. Closing is safe because the feed is
|
|
62
62
|
level-triggered: the client reconnects and receives the whole current truth, having missed no state — only activity
|
|
63
63
|
(Emits, retries) that fell in the gap. A page reconnects with EventSource's own retry; the CLI (`jr2 run`,
|
|
64
|
-
`jr2
|
|
64
|
+
`jr2 watch`) re-attaches any feed that ends before a terminal status, and never reads such an end as the run's —
|
|
65
65
|
except `gone`: a run stopped before it settled (parked, ADR-0025) reads through as `active`, so its one-shot feed says
|
|
66
66
|
`gone` after the status, and the CLI ends there instead of re-attaching in a loop. The server keeps no per-client
|
|
67
67
|
queue and no slow-client accounting beyond counting the subscriber's writes (scaling review R10, 2026-09-30).
|