@jr2/orchestrator 0.5.1 → 0.6.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/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +56 -20
- package/docs/concepts/architecture.md +30 -17
- package/docs/concepts/charts.md +6 -3
- package/docs/concepts/converge.md +3 -0
- package/docs/concepts/effects.md +86 -31
- package/docs/concepts/machines-and-workflows.md +9 -0
- package/docs/concepts/pools-and-sources.md +6 -6
- package/docs/concepts/secrets-and-trust.md +126 -42
- package/docs/concepts/the-menu.md +3 -0
- package/docs/concepts/workspaces.md +129 -51
- package/docs/get-started/an-agent-in-a-sandbox.md +21 -19
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +17 -4
- package/docs/guides/bind-open-parts.md +5 -4
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +1 -1
- package/docs/guides/let-an-agent-write-a-chart.md +5 -1
- package/docs/guides/look-inside-a-sandbox.md +18 -15
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +19 -18
- package/docs/guides/set-an-agents-access-and-dials.md +67 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/use-a-hosted-provider.md +6 -0
- package/docs/guides/watch-a-run.md +47 -24
- package/docs/guides/write-a-workflow.md +1 -1
- package/docs/llms.txt +33 -12
- package/docs/reference/authoring-surface/agent.md +34 -11
- package/docs/reference/authoring-surface/answer.md +19 -3
- package/docs/reference/authoring-surface/arg.md +27 -5
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/command.md +36 -21
- package/docs/reference/authoring-surface/fail.md +17 -4
- package/docs/reference/authoring-surface/file.md +36 -16
- package/docs/reference/authoring-surface/http.md +108 -8
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +1 -1
- package/docs/reference/authoring-surface/open.md +36 -5
- package/docs/reference/authoring-surface/paramsof.md +22 -4
- package/docs/reference/authoring-surface/pool.md +4 -4
- package/docs/reference/authoring-surface/push.md +30 -12
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/secret.md +25 -4
- package/docs/reference/authoring-surface/source.md +3 -3
- package/docs/reference/authoring-surface/type-agentdefinition.md +2 -2
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +5 -6
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +22 -4
- package/docs/reference/authoring-surface/type-customize.md +4 -2
- package/docs/reference/authoring-surface/type-dispatchoutput.md +8 -1
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +2 -4
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +7 -1
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +2 -2
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +32 -7
- package/docs/reference/authoring-surface/type-reposlot.md +5 -5
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcespec.md +10 -10
- package/docs/reference/authoring-surface/type-workspaced.md +29 -8
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +65 -37
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +30 -10
- package/docs/reference/cli.md +1 -1
- package/docs/reference/config.md +13 -4
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +17 -10
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +29 -19
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +30 -30
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +35 -17
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +8 -2
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +6 -4
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +21 -9
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +2 -1
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +8 -9
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +62 -65
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +9 -1
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +13 -12
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +4 -1
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +18 -19
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +8 -8
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +4 -4
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +24 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +5 -3
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +110 -67
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +101 -74
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +5 -6
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +2 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +181 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +74 -63
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +31 -20
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions.md +3 -0
- package/docs/reference/glossary.md +51 -12
- package/docs/reference/http-api.md +45 -7
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +5 -4
- package/package.json +3 -3
- package/src/actor.ts +161 -35
- package/src/agent.ts +6 -6
- package/src/ambient.ts +3 -5
- package/src/chart.ts +186 -19
- package/src/charts.ts +9 -2
- package/src/config.ts +16 -6
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +251 -77
- package/src/customize.ts +11 -3
- package/src/effect-checks.ts +50 -3
- package/src/effect.ts +343 -127
- package/src/effector-wire.ts +6 -3
- package/src/fail.ts +17 -4
- package/src/harness-client.ts +74 -31
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +125 -16
- package/src/holds.ts +3 -0
- package/src/http-effect.ts +88 -32
- package/src/http.ts +56 -17
- package/src/index.ts +15 -1
- package/src/internal.ts +1 -0
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +3 -4
- package/src/model-keys.ts +24 -0
- package/src/names.ts +19 -6
- package/src/notices.ts +443 -54
- package/src/open-body.ts +85 -19
- package/src/open.ts +36 -5
- package/src/parts.ts +40 -28
- package/src/party.ts +33 -0
- package/src/pick.ts +41 -7
- package/src/pool.ts +44 -23
- package/src/registration.ts +53 -11
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +106 -20
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +63 -16
- package/src/secret-placement.ts +9 -2
- package/src/setup.ts +86 -6
- package/src/tokens.ts +107 -32
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +10 -10
- package/src/workspace.ts +264 -253
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
package/src/pick.ts
CHANGED
|
@@ -63,9 +63,11 @@ import type { PickAnswer } from "./wire.ts";
|
|
|
63
63
|
* command,
|
|
64
64
|
* defineEvent,
|
|
65
65
|
* fail,
|
|
66
|
+
* isReady,
|
|
66
67
|
* jr2Setup,
|
|
67
68
|
* paramsOf,
|
|
68
69
|
* workspace,
|
|
70
|
+
* worktree,
|
|
69
71
|
* type Workspaced,
|
|
70
72
|
* } from "@jr2/orchestrator";
|
|
71
73
|
*
|
|
@@ -83,6 +85,8 @@ import type { PickAnswer } from "./wire.ts";
|
|
|
83
85
|
*
|
|
84
86
|
* type Input = Workspaced<{}, "app">;
|
|
85
87
|
*
|
|
88
|
+
* const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
|
|
89
|
+
*
|
|
86
90
|
* const body = jr2Setup({
|
|
87
91
|
* types: {} as { context: Input; input: Input },
|
|
88
92
|
* events: [runTest, finished],
|
|
@@ -91,6 +95,7 @@ import type { PickAnswer } from "./wire.ts";
|
|
|
91
95
|
* model: "anthropic/claude-haiku-5-5",
|
|
92
96
|
* instructions: "You are a careful engineer.",
|
|
93
97
|
* }),
|
|
98
|
+
* cut: worktree(),
|
|
94
99
|
* test: command(["node", "--test", arg("file")]),
|
|
95
100
|
* fail: fail(),
|
|
96
101
|
* },
|
|
@@ -98,12 +103,22 @@ import type { PickAnswer } from "./wire.ts";
|
|
|
98
103
|
* id: "fix-tests",
|
|
99
104
|
* context: ({ input }) => input,
|
|
100
105
|
* on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
|
|
101
|
-
* initial: "
|
|
106
|
+
* initial: "cutting",
|
|
102
107
|
* states: {
|
|
108
|
+
* cutting: {
|
|
109
|
+
* invoke: {
|
|
110
|
+
* src: "cut",
|
|
111
|
+
* input: ({ context }) => ({ path: fixTree(context), branch: "fix-tests" }),
|
|
112
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
|
|
113
|
+
* },
|
|
114
|
+
* },
|
|
103
115
|
* coding: {
|
|
104
116
|
* invoke: {
|
|
105
117
|
* src: "coder",
|
|
106
|
-
* input: {
|
|
118
|
+
* input: ({ context }) => ({
|
|
119
|
+
* prompt: "Make the tests pass. Run the tests of one module at a time.",
|
|
120
|
+
* cwd: fixTree(context),
|
|
121
|
+
* }),
|
|
107
122
|
* },
|
|
108
123
|
* on: { finished: "done" },
|
|
109
124
|
* initial: "ready",
|
|
@@ -113,13 +128,17 @@ import type { PickAnswer } from "./wire.ts";
|
|
|
113
128
|
* invoke: {
|
|
114
129
|
* src: "test",
|
|
115
130
|
* // A plain mapper: the Machine makes the file name from the pick's `module`.
|
|
116
|
-
* input: ({ event }) => ({
|
|
131
|
+
* input: ({ context, event }) => ({
|
|
132
|
+
* file: `test/${paramsOf(event, runTest).module}.test.js`,
|
|
133
|
+
* worktree: fixTree(context),
|
|
134
|
+
* }),
|
|
117
135
|
* onDone: "ready",
|
|
118
136
|
* },
|
|
119
137
|
* },
|
|
120
138
|
* },
|
|
121
139
|
* },
|
|
122
140
|
* done: { type: "final" },
|
|
141
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
123
142
|
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
124
143
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
125
144
|
* },
|
|
@@ -127,7 +146,6 @@ import type { PickAnswer } from "./wire.ts";
|
|
|
127
146
|
*
|
|
128
147
|
* export const machine = workspace(body, {
|
|
129
148
|
* repos: { app: { url: "https://github.com/you/app.git" } },
|
|
130
|
-
* spec: () => ({ branch: "fix-tests" }),
|
|
131
149
|
* });
|
|
132
150
|
* ```
|
|
133
151
|
*
|
|
@@ -640,8 +658,10 @@ export const isAnswer = (action: unknown): boolean =>
|
|
|
640
658
|
* command,
|
|
641
659
|
* defineEvent,
|
|
642
660
|
* fail,
|
|
661
|
+
* isReady,
|
|
643
662
|
* jr2Setup,
|
|
644
663
|
* workspace,
|
|
664
|
+
* worktree,
|
|
645
665
|
* type Workspaced,
|
|
646
666
|
* } from "@jr2/orchestrator";
|
|
647
667
|
*
|
|
@@ -659,6 +679,8 @@ export const isAnswer = (action: unknown): boolean =>
|
|
|
659
679
|
*
|
|
660
680
|
* type Input = Workspaced<{}, "app">;
|
|
661
681
|
*
|
|
682
|
+
* const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
|
|
683
|
+
*
|
|
662
684
|
* const body = jr2Setup({
|
|
663
685
|
* types: {} as { context: Input; input: Input },
|
|
664
686
|
* events: [runTests, finished],
|
|
@@ -667,6 +689,7 @@ export const isAnswer = (action: unknown): boolean =>
|
|
|
667
689
|
* model: "anthropic/claude-haiku-5-5",
|
|
668
690
|
* instructions: "You are a careful engineer.",
|
|
669
691
|
* }),
|
|
692
|
+
* cut: worktree(),
|
|
670
693
|
* tests: command(["npm", "test"]),
|
|
671
694
|
* fail: fail(),
|
|
672
695
|
* },
|
|
@@ -674,10 +697,20 @@ export const isAnswer = (action: unknown): boolean =>
|
|
|
674
697
|
* id: "fix-tests",
|
|
675
698
|
* context: ({ input }) => input,
|
|
676
699
|
* on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
|
|
677
|
-
* initial: "
|
|
700
|
+
* initial: "cutting",
|
|
678
701
|
* states: {
|
|
702
|
+
* cutting: {
|
|
703
|
+
* invoke: {
|
|
704
|
+
* src: "cut",
|
|
705
|
+
* input: ({ context }) => ({ path: fixTree(context), branch: "fix-tests" }),
|
|
706
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
|
|
707
|
+
* },
|
|
708
|
+
* },
|
|
679
709
|
* coding: {
|
|
680
|
-
* invoke: {
|
|
710
|
+
* invoke: {
|
|
711
|
+
* src: "coder",
|
|
712
|
+
* input: ({ context }) => ({ prompt: "Make the tests pass.", cwd: fixTree(context) }),
|
|
713
|
+
* },
|
|
681
714
|
* on: { finished: "done" },
|
|
682
715
|
* initial: "ready",
|
|
683
716
|
* states: {
|
|
@@ -685,6 +718,7 @@ export const isAnswer = (action: unknown): boolean =>
|
|
|
685
718
|
* testing: {
|
|
686
719
|
* invoke: {
|
|
687
720
|
* src: "tests",
|
|
721
|
+
* input: ({ context }) => ({ worktree: fixTree(context) }),
|
|
688
722
|
* onDone: {
|
|
689
723
|
* target: "ready",
|
|
690
724
|
* // The Agent reads this string, not the whole output of the command.
|
|
@@ -699,6 +733,7 @@ export const isAnswer = (action: unknown): boolean =>
|
|
|
699
733
|
* },
|
|
700
734
|
* },
|
|
701
735
|
* done: { type: "final" },
|
|
736
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
702
737
|
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
703
738
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
704
739
|
* },
|
|
@@ -706,7 +741,6 @@ export const isAnswer = (action: unknown): boolean =>
|
|
|
706
741
|
*
|
|
707
742
|
* export const machine = workspace(body, {
|
|
708
743
|
* repos: { app: { url: "https://github.com/you/app.git" } },
|
|
709
|
-
* spec: () => ({ branch: "fix-tests" }),
|
|
710
744
|
* });
|
|
711
745
|
* ```
|
|
712
746
|
*
|
package/src/pool.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type { EventDef } from "@jr2/agent-protocol";
|
|
|
38
38
|
import { fail, failurePayload, isFailure, type FailureInput } from "./fail.ts";
|
|
39
39
|
import { gate } from "./gate.ts";
|
|
40
40
|
import { attachWrapperBody, type JR2Wrapper, type WrapperActors } from "./parts.ts";
|
|
41
|
+
import { parsedNestedDoor } from "./vocabulary.ts";
|
|
41
42
|
import { attachInputSchema, attachVocabulary } from "./vocabulary.ts";
|
|
42
43
|
|
|
43
44
|
// The rich shape's `open` is the one bit that separates DRAINED from DEADLOCKED when the pool goes
|
|
@@ -50,22 +51,27 @@ import { attachInputSchema, attachVocabulary } from "./vocabulary.ts";
|
|
|
50
51
|
*/
|
|
51
52
|
export type SourceNextResult<T> = T | null | { item: T | null; open: number };
|
|
52
53
|
|
|
54
|
+
/** The input of a Source's `next`: the ids of the items that workers have now, the ids of the items
|
|
55
|
+
* that settled, and the Pool's input. So a Source over a list that a Machine holds needs no state
|
|
56
|
+
* of its own. */
|
|
57
|
+
export type PoolNextInput<TInput = unknown> = { active: string[]; settled: string[]; input: TInput };
|
|
58
|
+
|
|
53
59
|
// The Source port (ADR-0017).
|
|
54
60
|
/** Where a Pool gets its work items: the Source. Make one with {@link source}. */
|
|
55
|
-
export type SourceSpec<T> = {
|
|
61
|
+
export type SourceSpec<T, TInput = unknown> = {
|
|
56
62
|
// A union of the accepted output shapes because xstate's actor-logic type is invariant in its
|
|
57
63
|
// output — a `fromPromise` inferred as `T | null` must still assign.
|
|
58
64
|
/** Claim the next ready item. Write it as an xstate promise actor,
|
|
59
|
-
* `fromPromise(async ({ input }) => …)`. Its input is
|
|
60
|
-
* workers have now,
|
|
61
|
-
*
|
|
62
|
-
* starts, after each claim, and when a worker settles. While workers run and no item is ready, it
|
|
65
|
+
* `fromPromise(async ({ input }) => …)`. Its input is {@link PoolNextInput}: `active`, the ids of
|
|
66
|
+
* the items that workers have now, `settled`, the ids of the items that finished or failed, and
|
|
67
|
+
* `input`, the Pool's input. It must not give again an item in `active` or `settled`. It resolves
|
|
68
|
+
* to a {@link SourceNextResult}. The Pool calls it when the run starts, after each claim, and when a worker settles. While workers run and no item is ready, it
|
|
63
69
|
* also calls it on `wake` and every `pollEvery`. */
|
|
64
70
|
next:
|
|
65
|
-
| PromiseActorLogic<SourceNextResult<T>,
|
|
66
|
-
| PromiseActorLogic<{ item: T | null; open: number },
|
|
67
|
-
| PromiseActorLogic<T | null,
|
|
68
|
-
| PromiseActorLogic<null,
|
|
71
|
+
| PromiseActorLogic<SourceNextResult<T>, PoolNextInput<TInput>>
|
|
72
|
+
| PromiseActorLogic<{ item: T | null; open: number }, PoolNextInput<TInput>>
|
|
73
|
+
| PromiseActorLogic<T | null, PoolNextInput<TInput>>
|
|
74
|
+
| PromiseActorLogic<null, PoolNextInput<TInput>>;
|
|
69
75
|
// The webhook / `jr2 send` push seam. The def IS the pool machine's vocabulary (ADR-0011).
|
|
70
76
|
/** An external event, made with `defineEvent`, that tells the Pool that new work is ready, for
|
|
71
77
|
* example from a webhook or `jr2 send`. While workers run and no item is ready, it makes the Pool
|
|
@@ -91,7 +97,7 @@ export type SourceSpec<T> = {
|
|
|
91
97
|
* @example
|
|
92
98
|
* ```ts
|
|
93
99
|
* import { fromPromise } from "xstate";
|
|
94
|
-
* import { source } from "@jr2/orchestrator";
|
|
100
|
+
* import { source, type PoolNextInput } from "@jr2/orchestrator";
|
|
95
101
|
*
|
|
96
102
|
* type Issue = { id: string; title: string };
|
|
97
103
|
*
|
|
@@ -101,7 +107,7 @@ export type SourceSpec<T> = {
|
|
|
101
107
|
* ];
|
|
102
108
|
*
|
|
103
109
|
* export const issues = source<Issue>({
|
|
104
|
-
* next: fromPromise(async ({ input }: { input:
|
|
110
|
+
* next: fromPromise(async ({ input }: { input: PoolNextInput }) => {
|
|
105
111
|
* const issue = backlog.find((i) => !input.active.includes(i.id)) ?? null;
|
|
106
112
|
* if (issue) backlog.splice(backlog.indexOf(issue), 1);
|
|
107
113
|
* return issue;
|
|
@@ -114,7 +120,7 @@ export type SourceSpec<T> = {
|
|
|
114
120
|
* @see docs/concepts/pools-and-sources.md
|
|
115
121
|
* @see docs/reference/decisions/0017-pool-over-a-source-port.md
|
|
116
122
|
*/
|
|
117
|
-
export function source<T>(spec: SourceSpec<T>): SourceSpec<T> {
|
|
123
|
+
export function source<T, TInput = unknown>(spec: SourceSpec<T, TInput>): SourceSpec<T, TInput> {
|
|
118
124
|
return spec;
|
|
119
125
|
}
|
|
120
126
|
|
|
@@ -143,8 +149,10 @@ export type PoolSpec<T, TInput = unknown> = {
|
|
|
143
149
|
* `itemInput` read it, typed by it. A worker never gets it: each worker gets its item. Default:
|
|
144
150
|
* none, and then their `input` is `unknown`. */
|
|
145
151
|
input?: z.ZodObject;
|
|
152
|
+
// `SourceSpec<T>` too: xstate's actor logic is invariant in its input, so a `next` that does not
|
|
153
|
+
// read the Pool's input must still assign under a typed door.
|
|
146
154
|
/** Where the items come from. Make it with {@link source}. */
|
|
147
|
-
source: SourceSpec<T>;
|
|
155
|
+
source: SourceSpec<T, TInput> | SourceSpec<T>;
|
|
148
156
|
// Stable child identity: the spawn id, the active-set entry, the outcome key.
|
|
149
157
|
/** The id of an item. The Pool uses it as the id of the item's worker and in the `active` list
|
|
150
158
|
* that `next` gets. It is also the key of that worker's output in `items`. Give each item an id
|
|
@@ -250,7 +258,7 @@ function normalizeClaim(output: unknown): Claim {
|
|
|
250
258
|
* @example
|
|
251
259
|
* ```ts
|
|
252
260
|
* import { fromPromise } from "xstate";
|
|
253
|
-
* import { customize, pool, source } from "@jr2/orchestrator";
|
|
261
|
+
* import { customize, pool, source, type PoolNextInput } from "@jr2/orchestrator";
|
|
254
262
|
* import { task } from "@jr2/machines";
|
|
255
263
|
*
|
|
256
264
|
* type Issue = { id: string; title: string };
|
|
@@ -261,7 +269,7 @@ function normalizeClaim(output: unknown): Claim {
|
|
|
261
269
|
* ];
|
|
262
270
|
*
|
|
263
271
|
* const issues = source<Issue>({
|
|
264
|
-
* next: fromPromise(async ({ input }: { input:
|
|
272
|
+
* next: fromPromise(async ({ input }: { input: PoolNextInput }) => {
|
|
265
273
|
* const issue = backlog.find((i) => !input.active.includes(i.id)) ?? null;
|
|
266
274
|
* if (issue) backlog.splice(backlog.indexOf(issue), 1);
|
|
267
275
|
* return issue;
|
|
@@ -325,13 +333,22 @@ export function pool(worker: AnyStateMachine, spec: PoolSpec<any, any>): AnyStat
|
|
|
325
333
|
actors: { worker, next: spec.source.next, gate, fail: fail() },
|
|
326
334
|
}).createMachine({
|
|
327
335
|
id: spec.id ?? "pool",
|
|
328
|
-
context: ({ input }): PoolCtx =>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
context: ({ input, self }): PoolCtx => {
|
|
337
|
+
// RunHost parsed a root run's door. A parent's input is parsed here, and a value that is
|
|
338
|
+
// wrong starts no worker (ADR-0033). The handles and `instanceId` are no caller's to send.
|
|
339
|
+
const sent = (input ?? {}) as Record<string, unknown>;
|
|
340
|
+
const runInput =
|
|
341
|
+
spec.input && self._parent
|
|
342
|
+
? parsedNestedDoor(spec.input, sent, ["workspace", "instanceId"], "pool input invalid, before any worker")
|
|
343
|
+
: sent;
|
|
344
|
+
return {
|
|
345
|
+
runInput,
|
|
346
|
+
cap: typeof spec.cap === "function" ? spec.cap({ input: runInput as never }) : (spec.cap ?? 1),
|
|
347
|
+
active: [],
|
|
348
|
+
items: {},
|
|
349
|
+
failures: {},
|
|
350
|
+
};
|
|
351
|
+
},
|
|
335
352
|
// The wake seam: a standing gate for the whole run's life (root invokes stop only at final).
|
|
336
353
|
// The invoke id IS the gate's name (the id derives from the actor path — ADR-0011):
|
|
337
354
|
// `source` when the pool is the root, `<path>.source` nested — so two nested
|
|
@@ -386,7 +403,11 @@ export function pool(worker: AnyStateMachine, spec: PoolSpec<any, any>): AnyStat
|
|
|
386
403
|
invoke: {
|
|
387
404
|
id: "discover",
|
|
388
405
|
src: "next",
|
|
389
|
-
input: ({ context }) => ({
|
|
406
|
+
input: ({ context }) => ({
|
|
407
|
+
active: (context as PoolCtx).active,
|
|
408
|
+
settled: [...Object.keys((context as PoolCtx).items), ...Object.keys((context as PoolCtx).failures)],
|
|
409
|
+
input: (context as PoolCtx).runInput,
|
|
410
|
+
}),
|
|
390
411
|
onDone: [
|
|
391
412
|
{
|
|
392
413
|
// Claimed: record it, spawn its worker (TOP-LEVEL spawnChild — the visualize
|
package/src/registration.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { SENDER_KEY, type EventDef, type Sender } from "@jr2/agent-protocol";
|
|
|
30
30
|
import type { AgentAdmission } from "./actor.ts";
|
|
31
31
|
import type { EffectMarker, EffectRow } from "./effect.ts";
|
|
32
32
|
import type { EffectorPlacement, EffectorTarget } from "./http-effect.ts";
|
|
33
|
-
import type { NoticeScope, TakenNotices } from "./notices.ts";
|
|
33
|
+
import type { NoticeScope, RaiseOptions, TakenNotices } from "./notices.ts";
|
|
34
34
|
import type { AnswerSeat } from "./pick.ts";
|
|
35
35
|
import type { Notice, PickAnswer } from "./wire.ts";
|
|
36
36
|
import type { SandboxPort } from "./workspace.ts";
|
|
@@ -82,6 +82,20 @@ export type Registration = {
|
|
|
82
82
|
* Harness runs on the host, in no Sandbox at all): those are the Instance token's business.
|
|
83
83
|
*/
|
|
84
84
|
sandbox?: string;
|
|
85
|
+
/**
|
|
86
|
+
* An Agent's: its SLOT (ADR-0076) — the actor path of its Machine and its agent name, which is its
|
|
87
|
+
* continued conversation's Instance ID without the run id ({@link continuedIid}). The Harness
|
|
88
|
+
* states it on every control request, from its own record of the conversation, and the
|
|
89
|
+
* Orchestrator speaks here only for that slot. Two Agents with one key in two child Machines are
|
|
90
|
+
* two slots. Absent on gates.
|
|
91
|
+
*/
|
|
92
|
+
slot?: string;
|
|
93
|
+
/** An Agent's: the step boundary's read of its conversation's pending notices (ADR-0062), each
|
|
94
|
+
* reserved for this Turn until {@link noticesHeard} or the Turn's end releases it. */
|
|
95
|
+
boundaryNotices?: () => Promise<Array<{ id: string; notice: Notice }>>;
|
|
96
|
+
/** An Agent's: the Harness appended these step-boundary notices — they are heard, ledgered in one
|
|
97
|
+
* save. Answers how many were this Turn's reservations. */
|
|
98
|
+
noticesHeard?: (ids: readonly string[]) => number;
|
|
85
99
|
/** Close over the invoking state's `sendBack`; delivery lands where the actor was invoked. */
|
|
86
100
|
deliver: (event: DeliveredEvent) => void;
|
|
87
101
|
/** An Agent's: the Harness read this surface (`GET /agents/:iid/surface`), so it has the Turn in
|
|
@@ -313,9 +327,15 @@ export function actorPath(ref: AnyActorRef): string[] {
|
|
|
313
327
|
* Turn adds a random suffix, a burned conversation's successor its epoch), and the Agent's Actor
|
|
314
328
|
* names the conversation it burns with it.
|
|
315
329
|
*/
|
|
316
|
-
|
|
330
|
+
/** An Agent's slot (ADR-0076): its continued conversation's Instance ID without the run id —
|
|
331
|
+
* `<machine actor path>/<agent>`. What the Harness states and the registration records. */
|
|
332
|
+
export function agentSlot(machine: AnyActorRef | undefined, agentName: string): string {
|
|
317
333
|
const path = (machine ? actorPath(machine).join(".") : "") || "root";
|
|
318
|
-
return `${
|
|
334
|
+
return `${path}/${agentName}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function continuedIid(machine: AnyActorRef | undefined, runId: string, agentName: string): string {
|
|
338
|
+
return `${runId}/${agentSlot(machine, agentName)}`;
|
|
319
339
|
}
|
|
320
340
|
|
|
321
341
|
/** The address of a run's gate: gate ids are run-scoped by construction (ADR-0011). */
|
|
@@ -488,9 +508,18 @@ export type RunBinding = {
|
|
|
488
508
|
* Record an Agent invocation's durable admission in the host ledger (ADR-0016): persisted
|
|
489
509
|
* beside the snapshot in the same RunBlob save, keyed by iid (globally unique, so the map is
|
|
490
510
|
* flat). Optional so a bare unit-test binding can omit it — then admissions simply are not
|
|
491
|
-
* durable.
|
|
511
|
+
* durable. `delivered` are the notices the admission carried, delivered by this save in `taker`'s
|
|
512
|
+
* conversation — the Instance ID the admission rides (ADR-0062). `member`: the Workspace the
|
|
513
|
+
* conversation was admitted in, its slot and whether it continues — so it is in the audience of
|
|
514
|
+
* every kill there from now on, in the same save.
|
|
492
515
|
*/
|
|
493
|
-
recordAdmission?: (
|
|
516
|
+
recordAdmission?: (
|
|
517
|
+
instanceId: string,
|
|
518
|
+
admission: AgentAdmission,
|
|
519
|
+
delivered?: readonly string[],
|
|
520
|
+
taker?: string,
|
|
521
|
+
member?: { workspace: string; slot: string; continued: boolean },
|
|
522
|
+
) => void;
|
|
494
523
|
/**
|
|
495
524
|
* The admission ledger's READ half (ADR-0016): the last admission ledgered under this iid. A
|
|
496
525
|
* continued Turn asks it whether its conversation was ever admitted — only then can the Harness
|
|
@@ -521,14 +550,27 @@ export type RunBinding = {
|
|
|
521
550
|
* `older` names earlier events this one tells the same as, marked raised with it (`NoticeLedger`).
|
|
522
551
|
* A burned conversation's notice is raised by `bumpEpoch` itself, in the same save.
|
|
523
552
|
*/
|
|
524
|
-
raiseNotice?: (notice: Notice, to: string, source?: string, older?: readonly string[]) => void;
|
|
553
|
+
raiseNotice?: (notice: Notice, to: string, source?: string, older?: readonly string[], opts?: RaiseOptions) => void;
|
|
525
554
|
/** Put back a notice an admission carried that no model heard, WITH its source (`NoticeLedger.restore`):
|
|
526
|
-
* a raise would drop it, its source being seen.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
555
|
+
* a raise would drop it, its source being seen. A Workspace notice is pending again for `taker`,
|
|
556
|
+
* the conversation that did not hear it. Persisted at the write. */
|
|
557
|
+
restoreNotice?: (notice: Notice, to: string, source?: string, taker?: string) => void;
|
|
558
|
+
/** Reserve the pending notices in one admission's scope, for its conversation (`scope.taker`).
|
|
559
|
+
* They are delivered by the `recordAdmission` that ledgers it (its `delivered` ids and `taker`),
|
|
560
|
+
* or released for that conversation if the admission fails. */
|
|
530
561
|
takeNotices?: (scope: NoticeScope) => TakenNotices;
|
|
531
|
-
releaseNotices?: (ids: readonly string[]) => void;
|
|
562
|
+
releaseNotices?: (ids: readonly string[], taker?: string) => void;
|
|
563
|
+
/** What `takeNotices` would reserve in this scope, reserving nothing (`NoticeLedger.peek`). */
|
|
564
|
+
peekNotices?: (scope: NoticeScope) => Array<{ id: string; notice: Notice; source?: string }>;
|
|
565
|
+
/** The Harness acknowledged a step boundary's notices (ADR-0062): each that `taker` reserved is
|
|
566
|
+
* heard there, persisted at the write. Answers how many were. */
|
|
567
|
+
acknowledgeNotices?: (ids: readonly string[], taker: string) => number;
|
|
568
|
+
/** The conversations each kill's `bash` answer told (ADR-0062, D4 rule 3), off the pod's list:
|
|
569
|
+
* they leave that kill's audience. Persisted at the write when anything changed. */
|
|
570
|
+
noticesTold?: (told: ReadonlyArray<{ source: string; takers: readonly string[] }>) => void;
|
|
571
|
+
/** A Turn of these conversations in `workspace` ended: a fresh one leaves every audience there,
|
|
572
|
+
* a continued one stays for its next Turn (ADR-0062). Persisted at the write. */
|
|
573
|
+
noticeTurnEnded?: (workspace: string, takers: readonly string[]) => void;
|
|
532
574
|
/**
|
|
533
575
|
* The pick the Turn now at this address has outstanding, or answered and not yet read (ADR-0024):
|
|
534
576
|
* what the Agent's Actor asks when its Harness restarted under a Turn — a Turn whose pick is still
|
package/src/repo-identity.ts
CHANGED
|
@@ -18,7 +18,8 @@ export type RepoIdentity = {
|
|
|
18
18
|
* absolute path for a local repository. Path case is KEPT: hosts are case-insensitive, paths on
|
|
19
19
|
* most forges are not. */
|
|
20
20
|
identity: string;
|
|
21
|
-
/** `<slug>-<sha256(identity)[:
|
|
21
|
+
/** `<slug>-<sha256(identity)[:32]>` — a DNS-1123 label of at most 49 characters: a slug of at
|
|
22
|
+
* most 16 characters and 128 bits of the hash, so no one finds two identities with one key. */
|
|
22
23
|
key: string;
|
|
23
24
|
scheme: "https" | "http" | "ssh" | "git" | "local";
|
|
24
25
|
};
|
|
@@ -139,7 +140,10 @@ function keyOf(identity: string): string {
|
|
|
139
140
|
.toLowerCase()
|
|
140
141
|
.replace(/[^a-z0-9]+/g, "-")
|
|
141
142
|
.replace(/^-+|-+$/g, "")
|
|
142
|
-
.slice(0,
|
|
143
|
+
.slice(0, 16)
|
|
143
144
|
.replace(/-+$/, "") || "repo";
|
|
144
|
-
|
|
145
|
+
// 128 bits (ADR-0051): the key names the cache that a Sandbox mounts, and a per-run url is the
|
|
146
|
+
// Agent's text. With 32 bits a search found a url of another repository with the same key in
|
|
147
|
+
// seconds, and that url mounted the other repository's cache.
|
|
148
|
+
return `${slug}-${createHash("sha256").update(identity).digest("hex").slice(0, 32)}`;
|
|
145
149
|
}
|
package/src/repos.ts
CHANGED
|
@@ -192,10 +192,28 @@ export function kubeRepos(opts: KubeReposOptions): RepoResources {
|
|
|
192
192
|
return item;
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
+
/** The resource that stands under `repo.key`, when it is `repo`'s identity; else a throw that names
|
|
196
|
+
* both urls (ADR-0051). The key is collision-resistant, so another identity under one key is a
|
|
197
|
+
* defect or an attack, and no statement of this port writes a resource of another identity: not
|
|
198
|
+
* its url, not its `secretRef`, not its clock. */
|
|
199
|
+
const sameIdentity = async (repo: { url: string; identity: string; key: string }): Promise<RepoItem> => {
|
|
200
|
+
const item = await get(repo.key);
|
|
201
|
+
const stands = item.metadata?.annotations?.[ANNOTATION_REPO_IDENTITY];
|
|
202
|
+
if (stands !== repo.identity) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Repo "${repo.key}" stands for ${stands === undefined ? "no stated identity" : `"${stands}"`} ` +
|
|
205
|
+
`(url ${item.spec?.url ?? "unknown"}), not for "${repo.identity}" (url ${repo.url}): one key, two ` +
|
|
206
|
+
"repositories, so jr2 changes nothing on it (ADR-0051). Delete that Repo if it is stale.",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return item;
|
|
210
|
+
};
|
|
211
|
+
|
|
195
212
|
return {
|
|
196
213
|
async bind(repo) {
|
|
197
214
|
const { secretRef, cr } = await resourceFor({ ...repo, bound: true });
|
|
198
215
|
if (await create(cr)) return;
|
|
216
|
+
await sameIdentity(repo);
|
|
199
217
|
// Already there from an earlier deploy: ONE merge patch says what the Machine resolves now —
|
|
200
218
|
// url, credential, and the label `jr2 gc` honors. `secretRef: null` clears a credential the
|
|
201
219
|
// config no longer names, so a dropped entry does not linger. The clock is not touched: it
|
|
@@ -216,12 +234,12 @@ export function kubeRepos(opts: KubeReposOptions): RepoResources {
|
|
|
216
234
|
// Already there — the boot's statement, or an earlier run's. The eviction clock moves: the
|
|
217
235
|
// run attached it now. The url stays as stated, whatever spelling this run brought.
|
|
218
236
|
const clock = { metadata: { annotations: { [ANNOTATION_REPO_LAST_ATTACHED]: attached } } };
|
|
237
|
+
const standing = await sameIdentity(repo);
|
|
219
238
|
// A Machine's slot binds it: the boot is its writer, and restates the credential at the next
|
|
220
|
-
// deploy.
|
|
239
|
+
// deploy.
|
|
221
240
|
if (repo.bound) return patch(repo.key, clock);
|
|
222
241
|
// This run's slot is per-run, but another registered Machine may bind the identity — the
|
|
223
242
|
// label says so, and then the boot is still the writer.
|
|
224
|
-
const standing = await get(repo.key);
|
|
225
243
|
if (standing.metadata?.labels?.[LABEL_REPO_BOUND] === "true") return patch(repo.key, clock);
|
|
226
244
|
// Nothing binds it: no boot ever restates it, so this attach does — the credential resolved
|
|
227
245
|
// against the url that STANDS, since the scheme picks the Secret's kind (config.ts) and the
|