@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
package/src/customize.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// `.provide()`, nothing to learn (ADR-0015's rule, restated by ADR-0049 — a `.with()` method and
|
|
10
10
|
// a callable-machine hybrid were both rejected there).
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// Five moves, and each is the smallest one that works:
|
|
13
13
|
//
|
|
14
14
|
// - An AGENT override is xstate's own `provide`: `machine.provide({ actors: { coder:
|
|
15
15
|
// agent({ ...stock, ...override }) } })`. The slot already IS the Agent (ADR-0049), so
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// package left `open`, or whose auth, header or env names `secret(open)`, carries a binder,
|
|
19
19
|
// which builds the Effect again with the composer's parts, so the bound slot is checked as a
|
|
20
20
|
// written one is. Once — a bound part is fixed from then on.
|
|
21
|
+
// - A GATE binding is the same substitution (ADR-0011, ADR-0054): a Gate slot is built again from
|
|
22
|
+
// its parts, with the look bound and the handlers patched by kind and event key.
|
|
21
23
|
// - A CHILD override is the same call one level down, recursing. Each level is exactly the
|
|
22
24
|
// one-level reach ADR-0015 found `provide` has — held by the composer who owns the child
|
|
23
25
|
// object, never host-side injection into somebody else's Machine.
|
|
@@ -80,6 +82,18 @@ import { isEffect, type Arg } from "./effect.ts";
|
|
|
80
82
|
import { attachLent, copyOpenBody, lentIn, openBodiesOf, openBodyDeclarationOf } from "./open-body.ts";
|
|
81
83
|
import { effectChecks } from "./effect-checks.ts";
|
|
82
84
|
import type { NamedSecret, Text } from "./http-effect.ts";
|
|
85
|
+
import type { EventsOf, HookKinds, NoWildcardKeys } from "./hook-kinds/index.ts";
|
|
86
|
+
import {
|
|
87
|
+
gateSlotOf,
|
|
88
|
+
hooksTableFault,
|
|
89
|
+
isGateSlot,
|
|
90
|
+
lookFault,
|
|
91
|
+
type AnyHandler,
|
|
92
|
+
type GateIn,
|
|
93
|
+
type GateLook,
|
|
94
|
+
type GateSlot,
|
|
95
|
+
type GateSlotParts,
|
|
96
|
+
} from "./gate.ts";
|
|
83
97
|
import {
|
|
84
98
|
binderOf,
|
|
85
99
|
effectBindingShape,
|
|
@@ -141,6 +155,45 @@ type AgentSlots<M extends AnyStateMachine> = Extract<
|
|
|
141
155
|
*/
|
|
142
156
|
type OpenEffectSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: OpenEffect<OpenEffectPart> }>["src"];
|
|
143
157
|
|
|
158
|
+
/** The Gate slots (`gate({ … })`, ADR-0011) — told by their brand.
|
|
159
|
+
*
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
162
|
+
type GateSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: GateSlot<any, any> }>["src"];
|
|
163
|
+
/**
|
|
164
|
+
* The meta and the event type of the Gate slot under one key.
|
|
165
|
+
*
|
|
166
|
+
* @internal
|
|
167
|
+
*/
|
|
168
|
+
type GateTypesAt<M extends AnyStateMachine, K extends string> =
|
|
169
|
+
LogicAt<M, K> extends GateSlot<infer Meta, infer Ev> ? { meta: Meta; ev: Ev } : never;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A patch of a Gate slot's handlers (ADR-0011, ADR-0054): by Hook kind id and event key, a handler
|
|
173
|
+
* that adds or replaces the one there, or `null` to remove it. A key that the kind's payloads do not
|
|
174
|
+
* name does not compile.
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export type GateHooksPatch<Meta, Ev> = {
|
|
179
|
+
readonly [Id in keyof HookKinds as HookKinds[Id] extends { readonly id: Id } ? Id : never]?: {
|
|
180
|
+
readonly [Key in keyof EventsOf<HookKinds[Id]>]?:
|
|
181
|
+
| ((d: GateIn<Meta> & { readonly payload: EventsOf<HookKinds[Id]>[Key] }) => Ev | undefined)
|
|
182
|
+
| null;
|
|
183
|
+
} & NoWildcardKeys<HookKinds[Id]>;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What `customize()` binds on one Gate slot: its look, and a patch of its handlers.
|
|
188
|
+
*
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
export type GateBinding<Meta, Ev> = {
|
|
192
|
+
/** The look, bound once: the package left it `open`, or the composer replaces it. Or `"none"`. */
|
|
193
|
+
readonly look?: GateLook<Meta, Ev> | "none";
|
|
194
|
+
readonly hooks?: GateHooksPatch<Meta, Ev>;
|
|
195
|
+
};
|
|
196
|
+
|
|
144
197
|
/** What one Open part binds to: a url, an argv, a secret's name, a dispatch's target — another
|
|
145
198
|
* Orchestrator's origin and the secret that holds its Instance token, together (ADR-0071). */
|
|
146
199
|
type PartBinding<P> = P extends "url"
|
|
@@ -151,7 +204,11 @@ type PartBinding<P> = P extends "url"
|
|
|
151
204
|
? { secret: string }
|
|
152
205
|
: P extends "target"
|
|
153
206
|
? { target: { url: string; token: NamedSecret } }
|
|
154
|
-
:
|
|
207
|
+
: P extends "server"
|
|
208
|
+
? { server: string }
|
|
209
|
+
: P extends "tool"
|
|
210
|
+
? { tool: string }
|
|
211
|
+
: never;
|
|
155
212
|
|
|
156
213
|
/** A union of object types as their intersection: one binding object with every key. */
|
|
157
214
|
type Intersected<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
@@ -275,6 +332,19 @@ export type Customize<M extends AnyStateMachine> = {
|
|
|
275
332
|
effects?: [OpenEffectSlots<Reached<M>>] extends [never]
|
|
276
333
|
? never
|
|
277
334
|
: { [K in OpenEffectSlots<Reached<M>>]?: BindingOf<LogicAt<Reached<M>, K>> };
|
|
335
|
+
/** Bind the look of a Gate slot (`gate({ … })`) that a package left `open`, and add, replace or
|
|
336
|
+
* remove its handlers by Hook kind id and event key (`null` removes one), by slot name:
|
|
337
|
+
* `{ review: { look, hooks: { gitlab: { "note.MergeRequest": null } } } }`. At run time,
|
|
338
|
+
* `customize()` refuses a slot that holds no Gate slot: `customize(): machine "<id>" carries no
|
|
339
|
+
* Gate slot "<slot>" — its Gate slots are: <slots>`. */
|
|
340
|
+
gates?: [GateSlots<Reached<M>>] extends [never]
|
|
341
|
+
? never
|
|
342
|
+
: {
|
|
343
|
+
[K in GateSlots<Reached<M>>]?: GateBinding<
|
|
344
|
+
GateTypesAt<Reached<M>, K>["meta"],
|
|
345
|
+
GateTypesAt<Reached<M>, K>["ev"]
|
|
346
|
+
>;
|
|
347
|
+
};
|
|
278
348
|
/** Retune a Machine that this Machine composes, by slot name. The value has the same shape as
|
|
279
349
|
* these parts, one level down. At run time, `customize()` refuses a slot that holds no Machine:
|
|
280
350
|
* `customize(): machine "<id>" composes no Machine under "<slot>"`. */
|
|
@@ -326,6 +396,7 @@ export type Customize<M extends AnyStateMachine> = {
|
|
|
326
396
|
type LooseParts = {
|
|
327
397
|
agents?: Record<string, Partial<AgentDefinition> | undefined>;
|
|
328
398
|
effects?: Record<string, EffectBinding | undefined>;
|
|
399
|
+
gates?: Record<string, { look?: unknown; hooks?: Record<string, Record<string, AnyHandler | null>> } | undefined>;
|
|
329
400
|
actors?: Record<string, LooseParts | undefined>;
|
|
330
401
|
image?: string;
|
|
331
402
|
user?: string | UserContainer;
|
|
@@ -350,6 +421,9 @@ type LooseParts = {
|
|
|
350
421
|
* - `customize(): machine "<id>" carries no Agent slot "<slot>" — its Agents are: <slots>`
|
|
351
422
|
* - `customize(): machine "<id>" composes no Machine under "<slot>" — the Machines it composes are: <slots>`
|
|
352
423
|
* - `customize(): machine "<id>" carries no Effect slot "<slot>" — its Effects are: <slots>`
|
|
424
|
+
* - `customize(): machine "<id>" carries no Gate slot "<slot>" — its Gate slots are: <slots>`
|
|
425
|
+
* - `` customize(): Gate slot "<slot>"'s look is bound once, to a look or "none" — not `open` again ``, and the
|
|
426
|
+
* refusals of a look that `gate()` makes
|
|
353
427
|
* - `customize(): machine "<id>" declares no Repo Slot "<slot>" — its slots are: <slots>`
|
|
354
428
|
* - `customize(): machine "<id>" declares its Repo Slots open as a map — name at least one`
|
|
355
429
|
* - `` customize(): `repos` say what a SANDBOX is made of, how big it is and which Repos it
|
|
@@ -384,11 +458,11 @@ type LooseParts = {
|
|
|
384
458
|
* @see docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md
|
|
385
459
|
*/
|
|
386
460
|
export function customize<M extends AnyStateMachine>(machine: M, parts: Customize<M>): M {
|
|
387
|
-
const { agents, effects, actors, image, user, repos, resources } = parts as LooseParts;
|
|
461
|
+
const { agents, effects, gates, actors, image, user, repos, resources } = parts as LooseParts;
|
|
388
462
|
let out: AnyStateMachine = machine;
|
|
389
463
|
// Order matters in one direction only: the Sandbox seats and slots REBUILD the wrapper, so they
|
|
390
464
|
// go last and carry the retuned implementations with them.
|
|
391
|
-
if (agents || effects || actors) out = retune(out, { agents, effects, actors });
|
|
465
|
+
if (agents || effects || gates || actors) out = retune(out, { agents, effects, gates, actors });
|
|
392
466
|
const seats: Seats = {
|
|
393
467
|
...(image !== undefined ? { image } : {}),
|
|
394
468
|
...(user !== undefined ? { user } : {}),
|
|
@@ -458,6 +532,9 @@ function retune(machine: AnyStateMachine, parts: LooseParts): AnyStateMachine {
|
|
|
458
532
|
}
|
|
459
533
|
const bound = Object.entries(parts.effects ?? {}).filter(([, b]) => b !== undefined);
|
|
460
534
|
for (const [name, binding] of bound) actors[name] = bindEffect(machine, name, binding!);
|
|
535
|
+
for (const [name, binding] of Object.entries(parts.gates ?? {})) {
|
|
536
|
+
if (binding) actors[name] = bindGate(machine, name, binding);
|
|
537
|
+
}
|
|
461
538
|
for (const [name, childParts] of Object.entries(parts.actors ?? {})) {
|
|
462
539
|
if (!childParts) continue;
|
|
463
540
|
const child = asMachine((machine.implementations.actors as Record<string, unknown>)[name]);
|
|
@@ -547,6 +624,12 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
|
|
|
547
624
|
);
|
|
548
625
|
}
|
|
549
626
|
}
|
|
627
|
+
if ((part === "server" || part === "tool") && (typeof value !== "string" || value === "")) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`customize(): the mcp Effect "${name}"'s ${part} is bound to a name, a string: ` +
|
|
630
|
+
`${effectBindingShape(part)} (ADR-0054, ADR-0079)`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
550
633
|
if (part === "secret" && typeof value !== "string") {
|
|
551
634
|
throw new Error(
|
|
552
635
|
`customize(): the ${logic.effect.kind} Effect "${name}"'s secret is bound to a secret's name, a string — ` +
|
|
@@ -557,6 +640,62 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
|
|
|
557
640
|
return binder(binding);
|
|
558
641
|
}
|
|
559
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Bind a Gate slot's look and patch its handlers (ADR-0011, ADR-0054). The slot is built again from
|
|
645
|
+
* its parts, with the same `meta`, `delivers` and `reply`, so a run of the import and a run of the
|
|
646
|
+
* bound Machine each have their own. A look is bound to a value once: `open` again is refused, and
|
|
647
|
+
* the bound look meets the checks that `gate()` runs.
|
|
648
|
+
*/
|
|
649
|
+
function bindGate(
|
|
650
|
+
machine: AnyStateMachine,
|
|
651
|
+
name: string,
|
|
652
|
+
binding: NonNullable<NonNullable<LooseParts["gates"]>[string]>,
|
|
653
|
+
): unknown {
|
|
654
|
+
const logic = (machine.implementations.actors as Record<string, unknown>)[name];
|
|
655
|
+
if (!isGateSlot(logic)) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`customize(): machine "${machine.id}" carries no Gate slot "${name}" — its Gate slots are: ` +
|
|
658
|
+
`${listed(slotsWhere(machine, isGateSlot))} (ADR-0011). Only declared parts can be bound.`,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const parts: GateSlotParts = logic.gateSlot;
|
|
662
|
+
let look = parts.look;
|
|
663
|
+
if (binding.look !== undefined) {
|
|
664
|
+
if (binding.look === open) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`customize(): Gate slot "${name}"'s look is bound once, to a look or "none" — not \`open\` again (ADR-0054)`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const fault = lookFault(binding.look);
|
|
670
|
+
if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
|
|
671
|
+
look = binding.look as GateSlotParts["look"];
|
|
672
|
+
}
|
|
673
|
+
const hooks: Record<string, Record<string, AnyHandler>> = {};
|
|
674
|
+
for (const [kind, keys] of Object.entries(parts.hooks)) hooks[kind] = { ...keys };
|
|
675
|
+
for (const [kind, keys] of Object.entries(binding.hooks ?? {})) {
|
|
676
|
+
if (typeof keys !== "object" || keys === null) {
|
|
677
|
+
throw new Error(`customize(): Gate slot "${name}": hooks.${kind} is { <event key>: handler | null } (ADR-0011)`);
|
|
678
|
+
}
|
|
679
|
+
const into = (hooks[kind] ??= {});
|
|
680
|
+
for (const [key, handler] of Object.entries(keys)) {
|
|
681
|
+
if (handler === null) delete into[key];
|
|
682
|
+
else if (handler !== undefined) into[key] = handler;
|
|
683
|
+
}
|
|
684
|
+
if (Object.keys(into).length === 0) delete hooks[kind];
|
|
685
|
+
}
|
|
686
|
+
const fault = hooksTableFault(hooks);
|
|
687
|
+
if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
|
|
688
|
+
const frozen: Record<string, Readonly<Record<string, AnyHandler>>> = {};
|
|
689
|
+
for (const [kind, keys] of Object.entries(hooks)) frozen[kind] = Object.freeze(keys);
|
|
690
|
+
return gateSlotOf(
|
|
691
|
+
Object.freeze({
|
|
692
|
+
...parts,
|
|
693
|
+
hooks: Object.freeze(frozen),
|
|
694
|
+
...(look !== undefined ? { look } : {}),
|
|
695
|
+
}),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
560
699
|
const isSecret = (value: unknown): boolean =>
|
|
561
700
|
typeof value === "object" && value !== null && typeof (value as { secret?: unknown }).secret === "string";
|
|
562
701
|
|
package/src/dispatch-effect.ts
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
// through the binding, foreign in the `jr2-dispatched-by` header), and the door answers a
|
|
20
20
|
// repeat with the run the key already started. So a dispatch is idempotent by construction: a
|
|
21
21
|
// restore that cannot tell whether its start arrived sends it again, and gets the same run.
|
|
22
|
+
// The key is asked before the slot's schema reads the body: in-process through the run host's
|
|
23
|
+
// lookup, foreign as a key-only ask (no body) when the slot's schema refuses the body. With no
|
|
24
|
+
// run under the key, the schema's refusal is the invoke's error, and nothing starts.
|
|
22
25
|
// - THE RUN ON THE ROW. Once the start's answer names the run, the intent row is written again
|
|
23
26
|
// with it, saved at the write: a restore with it settles `started` at once, or resumes the
|
|
24
27
|
// wait on that run, and never starts another.
|
|
@@ -78,9 +81,10 @@ export type DispatchDefinition<S extends z.ZodObject | undefined = undefined> =
|
|
|
78
81
|
wait?: boolean;
|
|
79
82
|
/** How long the Effect waits: by default 30s for the start, and 10m with `wait`. */
|
|
80
83
|
timeout?: Duration;
|
|
81
|
-
/** A different Orchestrator: `url` is its origin, and `token` is the secret that holds
|
|
82
|
-
*
|
|
83
|
-
* <slot>: { target: { url, token
|
|
84
|
+
/** A different Orchestrator: `url` is its origin, and `token` is the secret that holds the token
|
|
85
|
+
* of a Caller of it with `start` for the Workflow, and `read` too when `wait` is true. Or `open`,
|
|
86
|
+
* which the composer binds with `customize(<import>, { effects: { <slot>: { target: { url, token
|
|
87
|
+
* } } } })`. Without it, the run starts in this Orchestrator. */
|
|
84
88
|
target?: { url: string; token: NamedSecret } | typeof open;
|
|
85
89
|
};
|
|
86
90
|
|
|
@@ -208,7 +212,9 @@ function targetOf(workflow: string, target: unknown): { url: string; token: stri
|
|
|
208
212
|
);
|
|
209
213
|
}
|
|
210
214
|
if (typeof token !== "object" || token === null || typeof (token as { secret?: unknown }).secret !== "string") {
|
|
211
|
-
throw new Error(
|
|
215
|
+
throw new Error(
|
|
216
|
+
`${where}: the token is a secret("<name>") holding a Caller token of the target with start for the Workflow (ADR-0071)`,
|
|
217
|
+
);
|
|
212
218
|
}
|
|
213
219
|
const name = (token as { secret: string }).secret;
|
|
214
220
|
if (name === OPEN_SECRET) {
|
|
@@ -255,23 +261,75 @@ export function openDispatchOf<S extends z.ZodObject | undefined>(
|
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
/** The wire request for one foreign start (ADR-0071): the target's origin, the Workflow, the body,
|
|
258
|
-
* the token by name,
|
|
264
|
+
* the token by name, who dispatched it, and the party it acts for. With no body (`undefined`), the
|
|
265
|
+
* start is a key-only ask: the door answers the run under the key, and starts nothing. */
|
|
259
266
|
export function dispatchStartOf(
|
|
260
267
|
parts: DispatchParts,
|
|
261
|
-
input: Record<string, unknown
|
|
268
|
+
input: Record<string, unknown> | undefined,
|
|
262
269
|
dispatchedBy: DispatchedBy & { instance: string },
|
|
270
|
+
onBehalfOf?: string,
|
|
263
271
|
): DispatchStartRequest {
|
|
264
272
|
if (parts.target === undefined) throw new Error("dispatchStartOf: an in-process dispatch has no wire request");
|
|
265
273
|
return {
|
|
266
274
|
kind: "dispatch",
|
|
267
275
|
url: [{ text: parts.target.url }],
|
|
268
276
|
workflow: parts.workflow,
|
|
269
|
-
input,
|
|
277
|
+
...(input !== undefined ? { input } : { keyOnly: true as const }),
|
|
270
278
|
auth: { bearer: { secret: parts.target.token } },
|
|
271
279
|
dispatchedBy,
|
|
272
280
|
wait: parts.wait,
|
|
273
281
|
timeoutMs: parts.timeoutMs,
|
|
282
|
+
...(onBehalfOf !== undefined ? { onBehalfOf } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* What a foreign start sends as `onBehalfOf` for the first Caller of the calling run's chain
|
|
288
|
+
* (ADR-0071, ADR-0078): the party that Caller says it acts for, else the name that jr2 shows, else
|
|
289
|
+
* `<via>/<name>`. Undefined when jr2 started the chain, or the text is not printable ASCII of at
|
|
290
|
+
* most 256 characters, which a header cannot carry: the start then goes with no claim.
|
|
291
|
+
*/
|
|
292
|
+
export function onBehalfOfFor(
|
|
293
|
+
caller: { via: string; name: string; shown?: string; onBehalfOf?: string } | undefined,
|
|
294
|
+
): string | undefined {
|
|
295
|
+
if (!caller) return undefined;
|
|
296
|
+
const text = caller.onBehalfOf ?? caller.shown ?? `${caller.via}/${caller.name}`;
|
|
297
|
+
return /^[\x20-\x7e]{1,256}$/.test(text) ? text : undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- A Pool's worker ------------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/** What a Pool of runs gives its `dispatch` worker (ADR-0017): the run's body, and the Hook delivery
|
|
303
|
+
* that carried the item, when one did. The key is the worker's own spawn id: the item's id. */
|
|
304
|
+
export type PoolWorkerInput = { body: unknown; hook?: DispatchedBy["hook"] };
|
|
305
|
+
|
|
306
|
+
/** A dispatch's snapshot as a Pool's worker holds it: the key and the Hook delivery ride the
|
|
307
|
+
* persisted snapshot, so a restore sends the same key again. */
|
|
308
|
+
type KeyedSnapshot<O, I> = EffectSnapshot<O, I> & { key?: string; hook?: DispatchedBy["hook"] };
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The `dispatch` slot of a Pool of runs, as the Pool spawns it (ADR-0017): the same Effect, whose
|
|
312
|
+
* idempotency key is the item's id — the spawn id — and not its ledger row id. So every Pool, every
|
|
313
|
+
* hand-over and every restore that gives the same item to the callee Workflow gets the same run
|
|
314
|
+
* (ADR-0071). The input is {@link PoolWorkerInput}.
|
|
315
|
+
*
|
|
316
|
+
* @internal
|
|
317
|
+
*/
|
|
318
|
+
export function poolWorker(logic: EffectLogic<any, DispatchOutput>): EffectLogic<PoolWorkerInput, DispatchOutput> {
|
|
319
|
+
const keyed = Object.create(
|
|
320
|
+
Object.getPrototypeOf(logic) as object,
|
|
321
|
+
Object.getOwnPropertyDescriptors(logic),
|
|
322
|
+
) as EffectLogic<PoolWorkerInput, DispatchOutput>;
|
|
323
|
+
const initial = logic.getInitialSnapshot.bind(logic);
|
|
324
|
+
keyed.getInitialSnapshot = (scope, input) => {
|
|
325
|
+
const given = (input ?? { body: undefined }) as PoolWorkerInput;
|
|
326
|
+
return {
|
|
327
|
+
...initial(scope, given.body),
|
|
328
|
+
key: scope.self.id,
|
|
329
|
+
...(given.hook !== undefined ? { hook: given.hook } : {}),
|
|
330
|
+
} as KeyedSnapshot<DispatchOutput, PoolWorkerInput> as never;
|
|
274
331
|
};
|
|
332
|
+
return keyed;
|
|
275
333
|
}
|
|
276
334
|
|
|
277
335
|
// --- The actor logic -----------------------------------------------------------------------------
|
|
@@ -418,6 +476,9 @@ async function drive<I, O>(
|
|
|
418
476
|
mark({ stage: "output", text: `${named}: run ${runId}${existing ? " (the key found it: started before)" : ""}` });
|
|
419
477
|
};
|
|
420
478
|
const deadline = Date.parse(at) + parts.timeoutMs;
|
|
479
|
+
// A Pool's worker carries the item's id as its key, and the delivery that carried the item
|
|
480
|
+
// (ADR-0017). Any other dispatch's key is its ledger row id.
|
|
481
|
+
const { key, hook } = snapshot as KeyedSnapshot<O, I>;
|
|
421
482
|
|
|
422
483
|
if (prior?.run !== undefined && !parts.wait) {
|
|
423
484
|
// Started before the restart, and the answer is the run: nothing to send.
|
|
@@ -440,22 +501,58 @@ async function drive<I, O>(
|
|
|
440
501
|
await waitIn(prior.run);
|
|
441
502
|
return;
|
|
442
503
|
}
|
|
443
|
-
//
|
|
444
|
-
|
|
504
|
+
// The key first, as the door asks it (ADR-0071): a run of the callee that carries it is
|
|
505
|
+
// answered before the input is read, so a re-send across a change of the slot's schema answers
|
|
506
|
+
// `existing`, never an error. The slot's bound holds the lookup as it holds the start.
|
|
507
|
+
const startBound = prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline;
|
|
508
|
+
const asked = host.lookup
|
|
509
|
+
? await beforeDeadline(
|
|
510
|
+
host.lookup(parts.workflow, key ?? id).then((run) => ({ run })),
|
|
511
|
+
startBound,
|
|
512
|
+
signal,
|
|
513
|
+
)
|
|
514
|
+
: { run: undefined };
|
|
515
|
+
if (signal.aborted) return;
|
|
516
|
+
if (asked === undefined) {
|
|
517
|
+
settle(
|
|
518
|
+
unknown(
|
|
519
|
+
`the run host did not answer the start of ${parts.workflow} within the slot's timeout; a run may have started`,
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const found = asked.run;
|
|
525
|
+
if (found) {
|
|
526
|
+
if (!prior) binding.recordEffect?.(id, intent());
|
|
527
|
+
pin(found.runId, true);
|
|
528
|
+
if (!parts.wait) {
|
|
529
|
+
settle({ outcome: "started", run: found.runId });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
await waitIn(found.runId);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Fresh, or in flight with no run on the row: the key makes a second start the same run. With
|
|
536
|
+
// the key asked above and no run under it, the body is a first send's, and the slot's schema
|
|
537
|
+
// refuses it as the invoke's error (ADR-0071). A host with no lookup asks the key at its start.
|
|
538
|
+
const input = inputOf(prior !== undefined && !host.lookup);
|
|
445
539
|
if (!prior) binding.recordEffect?.(id, intent());
|
|
446
540
|
mark({
|
|
447
541
|
stage: "started",
|
|
448
542
|
text: prior ? `${named} (sent again under its key: a run it started is answered)` : named,
|
|
449
543
|
});
|
|
450
544
|
// The depth is the callee's: one below this run, which the host records as sent (ADR-0071).
|
|
451
|
-
const by: DispatchedBy = {
|
|
545
|
+
const by: DispatchedBy = {
|
|
546
|
+
run: binding.runId,
|
|
547
|
+
slot,
|
|
548
|
+
effect: id,
|
|
549
|
+
depth: (binding.depth ?? 0) + 1,
|
|
550
|
+
...(key !== undefined ? { key } : {}),
|
|
551
|
+
...(hook !== undefined ? { hook } : {}),
|
|
552
|
+
};
|
|
452
553
|
// The slot's bound holds the start too (ADR-0071): a run host that does not answer in it is
|
|
453
554
|
// `unknown` — the start may have been taken, and a restore's re-send under the key would find it.
|
|
454
|
-
const answer = await beforeDeadline(
|
|
455
|
-
host.start(parts.workflow, input, by),
|
|
456
|
-
prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline,
|
|
457
|
-
signal,
|
|
458
|
-
);
|
|
555
|
+
const answer = await beforeDeadline(host.start(parts.workflow, input, by), startBound, signal);
|
|
459
556
|
if (signal.aborted) return;
|
|
460
557
|
if (answer === undefined) {
|
|
461
558
|
settle(
|
|
@@ -489,8 +586,13 @@ async function drive<I, O>(
|
|
|
489
586
|
const port = portFactory(target);
|
|
490
587
|
// The wait's stop goes through this port when its state exits (ADR-0071).
|
|
491
588
|
run.port = port;
|
|
589
|
+
// The party the start acts for: the first Caller of this run's chain, read once (ADR-0071).
|
|
590
|
+
const onBehalfOf = onBehalfOfFor(await binding?.firstCaller?.());
|
|
591
|
+
if (signal.aborted) return;
|
|
492
592
|
// Fresh: nothing was sent. The body is checked before the intent row, so a defect in the input is
|
|
493
|
-
// the Machine's error and sends nothing.
|
|
593
|
+
// the Machine's error and sends nothing. A body the slot's schema refuses is sent as a key-only
|
|
594
|
+
// ask (ADR-0071): the door answers a run under the key, and with none the schema's refusal is the
|
|
595
|
+
// invoke's error.
|
|
494
596
|
const req = prior ? undefined : requestFor(false);
|
|
495
597
|
const big = req && tooLarge(req);
|
|
496
598
|
if (big) {
|
|
@@ -567,18 +669,27 @@ async function drive<I, O>(
|
|
|
567
669
|
expired();
|
|
568
670
|
}
|
|
569
671
|
|
|
570
|
-
/**
|
|
571
|
-
|
|
672
|
+
/** Why the slot's schema refuses the invoke's input, or undefined when it takes it (or the slot
|
|
673
|
+
* has no schema). */
|
|
674
|
+
function refusedBody(): string | undefined {
|
|
675
|
+
if (def.input === undefined) return undefined;
|
|
676
|
+
const parsed = def.input.safeParse(snapshot.input ?? {});
|
|
677
|
+
if (parsed.success) return undefined;
|
|
678
|
+
return (
|
|
679
|
+
`dispatch "${slot}": its input does not match the slot's input schema — ` +
|
|
680
|
+
parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** The run's body: the invoke's input, parsed by the slot's schema when it has one. `again`: an
|
|
685
|
+
* in-process re-send to a run host that asks the key only at its start; a body the slot's schema
|
|
686
|
+
* refuses then goes as it is, and the run host answers the run under the key (ADR-0071). */
|
|
687
|
+
function inputOf(again = false): Record<string, unknown> {
|
|
572
688
|
let input: unknown = snapshot.input ?? {};
|
|
573
689
|
if (def.input !== undefined) {
|
|
574
690
|
const parsed = def.input.safeParse(input);
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
`dispatch "${slot}": its input does not match the slot's input schema — ` +
|
|
578
|
-
parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
input = parsed.data;
|
|
691
|
+
if (parsed.success) input = parsed.data;
|
|
692
|
+
else if (!again) throw new Error(refusedBody());
|
|
582
693
|
}
|
|
583
694
|
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
584
695
|
throw new Error(`dispatch "${slot}": its input is the run's body, a JSON object (ADR-0071)`);
|
|
@@ -600,7 +711,9 @@ async function drive<I, O>(
|
|
|
600
711
|
instance: binding?.orchestratorName ?? "an Orchestrator with no name",
|
|
601
712
|
};
|
|
602
713
|
const timeoutMs = again ? Math.max(1, Math.floor(deadline - Date.now())) : parts.timeoutMs;
|
|
603
|
-
|
|
714
|
+
// A body the slot's schema refuses is never sent: the start asks the key alone (ADR-0071).
|
|
715
|
+
const body = refusedBody() === undefined ? inputOf() : undefined;
|
|
716
|
+
return dispatchStartOf({ ...parts, timeoutMs }, body, by, onBehalfOf);
|
|
604
717
|
}
|
|
605
718
|
|
|
606
719
|
/** The in-process wait (ADR-0071): the run host tells when the run leaves `live`; the slot's
|
|
@@ -677,6 +790,9 @@ async function drive<I, O>(
|
|
|
677
790
|
if (state.state === "settled" && state.output) {
|
|
678
791
|
const parsed = DispatchOutputShape.safeParse(state.output);
|
|
679
792
|
if (!parsed.success) throw new EffectorMalformed("the Effector's output for a dispatch is not a dispatch's");
|
|
793
|
+
// A key-only ask, and no run has the key: nothing was started, and the slot's schema refusal
|
|
794
|
+
// is the invoke's error (ADR-0071).
|
|
795
|
+
if (parsed.data.outcome === "absent") throw new Error(refusedBody() ?? "the door found no run under the key");
|
|
680
796
|
const output = parsed.data as DispatchOutput;
|
|
681
797
|
if (output.outcome === "started" || output.outcome === "settled") pin(output.run, false);
|
|
682
798
|
settle(output);
|
package/src/durability.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type StoredSnapshot = {
|
|
|
18
18
|
status: string;
|
|
19
19
|
snapshot: unknown;
|
|
20
20
|
reason?: string;
|
|
21
|
+
/** When the run started (ADR-0009): the row's `started_at`. */
|
|
22
|
+
startedAt?: string;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
type ChildEntry = { snapshot?: { input?: unknown; [k: string]: unknown }; [k: string]: unknown };
|
package/src/duration.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A duration as a person writes it in config or a query: a number and one unit, such as `"90m"`,
|
|
2
|
+
// `"36h"`, `"30d"` or `"2w"`. `runs.retain` (ADR-0065) and the `since` of `GET /runs` (ADR-0009)
|
|
3
|
+
// read it.
|
|
4
|
+
|
|
5
|
+
const DURATION = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/;
|
|
6
|
+
const UNIT_MS: Record<string, number> = {
|
|
7
|
+
s: 1000,
|
|
8
|
+
m: 60_000,
|
|
9
|
+
h: 3_600_000,
|
|
10
|
+
d: 86_400_000,
|
|
11
|
+
w: 7 * 86_400_000,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The duration in milliseconds, or undefined when `text` is not a positive duration. */
|
|
15
|
+
export function periodMs(text: string): number | undefined {
|
|
16
|
+
const m = DURATION.exec(text.trim());
|
|
17
|
+
if (!m) return undefined;
|
|
18
|
+
const ms = Number(m[1]) * UNIT_MS[m[2]!]!;
|
|
19
|
+
return ms > 0 && Number.isFinite(ms) ? ms : undefined;
|
|
20
|
+
}
|
package/src/effect-checks.ts
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import type { AnyStateMachine, UnknownActorLogic } from "xstate";
|
|
40
40
|
import type { EventDef } from "@jr2/agent-protocol";
|
|
41
41
|
import { isAgent } from "./agent.ts";
|
|
42
|
+
import { isGateInvoke } from "./gate-brand.ts";
|
|
42
43
|
import { inputKeysOf, inputShapeOf, isEffect } from "./effect.ts";
|
|
43
44
|
import { mayTake, partiesAt } from "./party.ts";
|
|
44
45
|
import { isAnswer, isPickMapper } from "./pick.ts";
|
|
@@ -389,7 +390,7 @@ export function effectChecks(
|
|
|
389
390
|
const below: Node[] = [];
|
|
390
391
|
const descend = (p: Node): void => {
|
|
391
392
|
for (const child of Object.values(p.states)) {
|
|
392
|
-
if (invokesAgent(child) !== undefined || child.invoke.some((inv) => inv
|
|
393
|
+
if (invokesAgent(child) !== undefined || child.invoke.some((inv) => isGateInvoke(inv, actors))) continue;
|
|
393
394
|
below.push(child);
|
|
394
395
|
descend(child);
|
|
395
396
|
}
|