@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/open-body.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type UnknownActorLogic,
|
|
25
25
|
} from "xstate";
|
|
26
26
|
import { isAgent, type AgentDefinition, type WorkspaceAccess } from "./agent.ts";
|
|
27
|
+
import { ambientHandlesFor } from "./ambient.ts";
|
|
27
28
|
import {
|
|
28
29
|
admitChart,
|
|
29
30
|
chartDigest,
|
|
@@ -31,8 +32,10 @@ import {
|
|
|
31
32
|
ChartRefusal,
|
|
32
33
|
GENERIC_ACTIONS,
|
|
33
34
|
pathSegments,
|
|
35
|
+
READY_GUARD,
|
|
34
36
|
resolveSource,
|
|
35
37
|
resolveReferences,
|
|
38
|
+
slotFolderFault,
|
|
36
39
|
type AdmittedChart,
|
|
37
40
|
type ChartAction,
|
|
38
41
|
type ChartGuard,
|
|
@@ -43,11 +46,12 @@ import {
|
|
|
43
46
|
type ChartTransition,
|
|
44
47
|
type ChartTransitions,
|
|
45
48
|
} from "./chart.ts";
|
|
46
|
-
import {
|
|
49
|
+
import { inputShapeOf, isEffect, type Duration, type InputShape } from "./effect.ts";
|
|
47
50
|
import { fail, FAILURE_GUARD, failureGuard } from "./fail.ts";
|
|
48
51
|
import { agent } from "./harness-client.ts";
|
|
49
52
|
import { SENT_BY } from "./party.ts";
|
|
50
|
-
import { wrapperBodyOf } from "./parts.ts";
|
|
53
|
+
import { composesSandbox, sandboxPartsOf, wrapperBodyOf } from "./parts.ts";
|
|
54
|
+
import { open } from "./open.ts";
|
|
51
55
|
import { actorPath, boundRun } from "./registration.ts";
|
|
52
56
|
import { COMPILED_CHART, jr2Setup } from "./setup.ts";
|
|
53
57
|
import { attachBoundMachine, boundMachineFor } from "./vocabulary.ts";
|
|
@@ -573,9 +577,12 @@ export function admitBodyChart(
|
|
|
573
577
|
text: unknown,
|
|
574
578
|
decl: OpenBodyDeclaration,
|
|
575
579
|
lent: LentParts,
|
|
576
|
-
options: { inputKeys?: readonly string[] } = {},
|
|
580
|
+
options: { inputKeys?: readonly string[]; repoSlots?: readonly string[] } = {},
|
|
577
581
|
): AdmittedChart {
|
|
578
|
-
const admitted = admitChart(text, decl, lent,
|
|
582
|
+
const admitted = admitChart(text, decl, lent, {
|
|
583
|
+
...(options.inputKeys ? { inputKeys: options.inputKeys } : {}),
|
|
584
|
+
...(options.repoSlots ? { repoSlots: options.repoSlots } : {}),
|
|
585
|
+
});
|
|
579
586
|
try {
|
|
580
587
|
compileChart(admitted, lent, decl);
|
|
581
588
|
} catch (err) {
|
|
@@ -627,11 +634,23 @@ export function admitOpenBodyChart(
|
|
|
627
634
|
const logic = (inner.implementations.actors as Record<string, unknown>)[slot];
|
|
628
635
|
if (!isOpenBody(logic)) throw new Error(`machine "${machine.id}" has no Open body "${slot}"`);
|
|
629
636
|
const decl = openBodyDeclarationOf(logic);
|
|
637
|
+
const repoSlots = repoSlotsAround(machine);
|
|
630
638
|
return admitBodyChart(text, decl, lentIn(inner.implementations as Implementations, decl), {
|
|
631
639
|
...(inputKeys ? { inputKeys } : {}),
|
|
640
|
+
...(repoSlots ? { repoSlots } : {}),
|
|
632
641
|
});
|
|
633
642
|
}
|
|
634
643
|
|
|
644
|
+
/** The Repo Slots of the `workspace()` around a Chart wrapper, where the door can see one: the
|
|
645
|
+
* wrapper IS the `workspace()`. Undefined: an Open map, or a Workspace that a parent state invokes,
|
|
646
|
+
* whose slots this door cannot read. Then the Open body's own door reads them off the running
|
|
647
|
+
* Workspace, and a Turn's or an Effect's start refuses what no door could see. */
|
|
648
|
+
export function repoSlotsAround(machine: AnyStateMachine): readonly string[] | undefined {
|
|
649
|
+
if (!composesSandbox(machine)) return undefined;
|
|
650
|
+
const { repos } = sandboxPartsOf(machine);
|
|
651
|
+
return repos === open ? undefined : Object.keys(repos);
|
|
652
|
+
}
|
|
653
|
+
|
|
635
654
|
/**
|
|
636
655
|
* Admit a Chart field of a Chart wrapper's door (ADR-0070), the door a run's start runs before the
|
|
637
656
|
* run exists. The door cannot tell which Open body a field feeds, so the Chart is admitted against
|
|
@@ -648,7 +667,10 @@ export function admitWrapperChart(machine: AnyStateMachine, text: unknown): Admi
|
|
|
648
667
|
for (const { slot, logic } of bodies) {
|
|
649
668
|
const decl = openBodyDeclarationOf(logic);
|
|
650
669
|
try {
|
|
651
|
-
|
|
670
|
+
const repoSlots = repoSlotsAround(machine);
|
|
671
|
+
return admitBodyChart(text, decl, lentIn(inner.implementations as Implementations, decl), {
|
|
672
|
+
...(repoSlots ? { repoSlots } : {}),
|
|
673
|
+
});
|
|
652
674
|
} catch (err) {
|
|
653
675
|
if (!(err instanceof ChartRefusal)) throw err;
|
|
654
676
|
report.push(...err.report.map((line) => (bodies.length > 1 ? `body "${slot}": ${line}` : line)));
|
|
@@ -675,7 +697,7 @@ const scopeOf = (args: Args) => ({
|
|
|
675
697
|
input: chartInputOf((args.context?.[CHART_INPUT_KEY] as Record<string, unknown> | undefined) ?? {}),
|
|
676
698
|
});
|
|
677
699
|
|
|
678
|
-
/** An invoke's error whose message jr2 wrote for a Chart to read: a
|
|
700
|
+
/** An invoke's error whose message jr2 wrote for a Chart to read: a fault in an input the door could not see. */
|
|
679
701
|
class ChartInvokeFault extends Error {}
|
|
680
702
|
|
|
681
703
|
/**
|
|
@@ -839,6 +861,10 @@ function transitionsOf(ts: ChartTransitions): unknown {
|
|
|
839
861
|
return Array.isArray(ts) ? ts.map(transitionOf) : transitionOf(ts);
|
|
840
862
|
}
|
|
841
863
|
|
|
864
|
+
/** `jr2.ready`: is this cut's output `ready`? On an `onDone`. */
|
|
865
|
+
const readyGuard = ({ event }: { event: unknown }): boolean =>
|
|
866
|
+
(event as { output?: { outcome?: unknown } } | undefined)?.output?.outcome === "ready";
|
|
867
|
+
|
|
842
868
|
const mapValues = <T, U>(o: Record<string, T>, fn: (v: T) => U): Record<string, U> =>
|
|
843
869
|
Object.fromEntries(Object.entries(o).map(([k, v]) => [k, fn(v)]));
|
|
844
870
|
|
|
@@ -847,15 +873,17 @@ const mapValues = <T, U>(o: Record<string, T>, fn: (v: T) => U): Record<string,
|
|
|
847
873
|
* resolved object again at invoke time"), or undefined when it is. The door checked the literal
|
|
848
874
|
* parts; a reference may still resolve to a value that is no string.
|
|
849
875
|
*/
|
|
850
|
-
function effectInputFault(
|
|
876
|
+
function effectInputFault(shape: InputShape, value: unknown): string | undefined {
|
|
851
877
|
const given = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : undefined;
|
|
878
|
+
const known = [...shape.required, ...shape.optional, ...(shape.oneOf ?? [])];
|
|
852
879
|
const ok =
|
|
853
880
|
given !== undefined &&
|
|
854
|
-
|
|
855
|
-
|
|
881
|
+
shape.required.every((k) => given.includes(k)) &&
|
|
882
|
+
given.every((k) => known.includes(k) && typeof (value as Record<string, unknown>)[k] === "string") &&
|
|
883
|
+
(shape.oneOf === undefined || shape.oneOf.filter((k) => given.includes(k)).length === 1);
|
|
856
884
|
return ok
|
|
857
885
|
? undefined
|
|
858
|
-
: `its input resolved to ${JSON.stringify(value)}; it takes
|
|
886
|
+
: `its input resolved to ${JSON.stringify(value)}; it takes ${known.join(", ") || "nothing"}, each a string (ADR-0070)`;
|
|
859
887
|
}
|
|
860
888
|
|
|
861
889
|
const checkedEffects = new WeakMap<object, UnknownActorLogic>();
|
|
@@ -867,13 +895,15 @@ const checkedEffects = new WeakMap<object, UnknownActorLogic>();
|
|
|
867
895
|
*/
|
|
868
896
|
function checkedEffect(slot: string, logic: UnknownActorLogic): UnknownActorLogic {
|
|
869
897
|
if (!isEffect(logic)) return logic;
|
|
870
|
-
const
|
|
871
|
-
if (
|
|
898
|
+
const shape = inputShapeOf(logic.effect);
|
|
899
|
+
if (shape === undefined) return logic;
|
|
872
900
|
const hit = checkedEffects.get(logic);
|
|
873
901
|
if (hit) return hit;
|
|
874
902
|
const checked = Object.create(logic) as UnknownActorLogic;
|
|
875
903
|
checked.getInitialSnapshot = (scope, input) => {
|
|
876
|
-
|
|
904
|
+
// An Effect that takes nothing, invoked with no input, is a whole input: the door let it through.
|
|
905
|
+
const fault =
|
|
906
|
+
input === undefined && shape.required.length === 0 && !shape.oneOf ? undefined : effectInputFault(shape, input);
|
|
877
907
|
if (fault === undefined) return logic.getInitialSnapshot(scope, input);
|
|
878
908
|
return {
|
|
879
909
|
status: "error",
|
|
@@ -885,9 +915,38 @@ function checkedEffect(slot: string, logic: UnknownActorLogic): UnknownActorLogi
|
|
|
885
915
|
return checked;
|
|
886
916
|
}
|
|
887
917
|
|
|
888
|
-
|
|
918
|
+
const checkedAgents = new WeakMap<object, UnknownActorLogic>();
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* An Agent as a Chart invokes it: the Agent itself, whose start refuses a resolved `cwd` that is a
|
|
922
|
+
* Repo Slot's folder. A reference hides that value from the door. The check is in `start`, because
|
|
923
|
+
* the Workspace's handles are registered at a start, and a Chart's first invoke resolves before
|
|
924
|
+
* that. The refusal is THAT invoke's error, as in {@link checkedEffect}.
|
|
925
|
+
*/
|
|
926
|
+
function checkedAgent(slot: string, logic: UnknownActorLogic): UnknownActorLogic {
|
|
927
|
+
const hit = checkedAgents.get(logic);
|
|
928
|
+
if (hit) return hit;
|
|
929
|
+
const checked = Object.create(logic) as UnknownActorLogic;
|
|
930
|
+
checked.start = (snapshot, scope) => {
|
|
931
|
+
const cwd = ((snapshot as { input?: unknown }).input as { cwd?: unknown } | undefined)?.cwd;
|
|
932
|
+
const reads = (logic as { definition?: { workspace?: unknown } }).definition?.workspace !== "none";
|
|
933
|
+
const repos = reads && typeof cwd === "string" ? ambientHandlesFor(scope.self)?.repos : undefined;
|
|
934
|
+
const fault = repos ? slotFolderFault(cwd as string, Object.keys(repos)) : undefined;
|
|
935
|
+
if (fault !== undefined) throw new ChartInvokeFault(`the Agent "${slot}": ${fault}`);
|
|
936
|
+
logic.start?.(snapshot, scope);
|
|
937
|
+
};
|
|
938
|
+
checkedAgents.set(logic, checked);
|
|
939
|
+
return checked;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** The lent actors as a Chart invokes them: each Effect and Agent checked at its start. */
|
|
889
943
|
function chartActors(actors: Record<string, UnknownActorLogic>): Record<string, UnknownActorLogic> {
|
|
890
|
-
return Object.fromEntries(
|
|
944
|
+
return Object.fromEntries(
|
|
945
|
+
Object.entries(actors).map(([slot, logic]) => [
|
|
946
|
+
slot,
|
|
947
|
+
isAgent(logic) ? checkedAgent(slot, logic) : checkedEffect(slot, logic),
|
|
948
|
+
]),
|
|
949
|
+
);
|
|
891
950
|
}
|
|
892
951
|
|
|
893
952
|
/** An Agent Turn's input with `prompt` and `cwd` as strings: a whole placeholder may resolve to
|
|
@@ -964,7 +1023,7 @@ export type CompileOptions = OpenBodyOptions & {
|
|
|
964
1023
|
/**
|
|
965
1024
|
* Build the Machine a Chart describes (ADR-0070), with `jr2Setup` over: the lent actors by their
|
|
966
1025
|
* slot names, `agent(def)` for each Agent the Chart mints, the kit's `gate`, and `fail()` where the
|
|
967
|
-
* body lends it; the lent guards, `jr2.sentBy` and `jr2.
|
|
1026
|
+
* body lends it; the lent guards, `jr2.sentBy`, `jr2.failure` and `jr2.ready`; the lent actions and the four
|
|
968
1027
|
* generic ones; the lent delays. Every reference resolves at invoke time, so the Machine holds the
|
|
969
1028
|
* text's shape and no run's data. The Chart's
|
|
970
1029
|
* input is the context key {@link CHART_INPUT_KEY}; a done Chart outputs `{ outcome: "done",
|
|
@@ -984,7 +1043,7 @@ export function compileChart(
|
|
|
984
1043
|
const mint = options.mintAgent ?? ((_slot: string, definition: AgentDefinition) => agent(definition));
|
|
985
1044
|
const minted = mapValues(admitted.agents, (def) => def);
|
|
986
1045
|
const actors: Record<string, UnknownActorLogic> = chartActors(lent.actors);
|
|
987
|
-
for (const [slot, def] of Object.entries(minted)) actors[slot] = mint(slot, def);
|
|
1046
|
+
for (const [slot, def] of Object.entries(minted)) actors[slot] = checkedAgent(slot, mint(slot, def));
|
|
988
1047
|
// The kit's Part that fails the run, where the body lends it (ADR-0073): no slot of the wrapper's.
|
|
989
1048
|
if (decl.lends.fail === true) actors.fail = fail();
|
|
990
1049
|
// The slots an invoke runs an Agent Turn in: the lent Agents and the Chart's own.
|
|
@@ -1018,7 +1077,11 @@ export function compileChart(
|
|
|
1018
1077
|
events: admitted.events,
|
|
1019
1078
|
actors,
|
|
1020
1079
|
// `jr2.failure` is in every Chart, as `jr2.sentBy` is: a predicate, with no capability (ADR-0073).
|
|
1021
|
-
guards: {
|
|
1080
|
+
guards: {
|
|
1081
|
+
...(lent.guards as Record<string, unknown>),
|
|
1082
|
+
[FAILURE_GUARD]: failureGuard,
|
|
1083
|
+
[READY_GUARD]: readyGuard,
|
|
1084
|
+
} as never,
|
|
1022
1085
|
actions: { ...(lent.actions as Record<string, unknown>), ...GENERIC } as never,
|
|
1023
1086
|
delays: lent.delays as never,
|
|
1024
1087
|
}).createMachine(config as never) as unknown as AnyStateMachine;
|
|
@@ -1382,7 +1445,10 @@ export function openBodyLogic(decl: OpenBodyDeclaration, options: OpenBodyOption
|
|
|
1382
1445
|
/** Admit and compile a Chart text, and record the Machine the actor runs. Throws ChartRefusal. */
|
|
1383
1446
|
const bind = (self: AnyActorRef, text: unknown, inputKeys: readonly string[]) => {
|
|
1384
1447
|
const lent = liveLentOf(self, logic);
|
|
1385
|
-
|
|
1448
|
+
// A Chart that comes after the Workspace started finds its slots here. A Chart in the step that
|
|
1449
|
+
// starts the Workspace comes before them, and the wrapper's door or the Turn's start saw it.
|
|
1450
|
+
const repos = ambientHandlesFor(self)?.repos;
|
|
1451
|
+
const admitted = admitChart(text, decl, lent, { inputKeys, ...(repos ? { repoSlots: Object.keys(repos) } : {}) });
|
|
1386
1452
|
const src = (self as { src?: unknown }).src;
|
|
1387
1453
|
const machine = compiledFor(admitted, lent, decl, {
|
|
1388
1454
|
...options,
|
package/src/open.ts
CHANGED
|
@@ -24,7 +24,18 @@
|
|
|
24
24
|
* @example
|
|
25
25
|
* ```ts
|
|
26
26
|
* import { z } from "zod";
|
|
27
|
-
* import {
|
|
27
|
+
* import {
|
|
28
|
+
* agent,
|
|
29
|
+
* customize,
|
|
30
|
+
* defineEvent,
|
|
31
|
+
* fail,
|
|
32
|
+
* isReady,
|
|
33
|
+
* jr2Setup,
|
|
34
|
+
* open,
|
|
35
|
+
* workspace,
|
|
36
|
+
* worktree,
|
|
37
|
+
* type Workspaced,
|
|
38
|
+
* } from "@jr2/orchestrator";
|
|
28
39
|
*
|
|
29
40
|
* const finish = defineEvent({
|
|
30
41
|
* name: "finish",
|
|
@@ -32,19 +43,40 @@
|
|
|
32
43
|
* input: z.object({}),
|
|
33
44
|
* });
|
|
34
45
|
*
|
|
46
|
+
* type Input = Workspaced<{}, "app">;
|
|
47
|
+
*
|
|
48
|
+
* const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
|
|
49
|
+
*
|
|
35
50
|
* const body = jr2Setup({
|
|
51
|
+
* types: {} as { context: Input; input: Input },
|
|
36
52
|
* events: [finish],
|
|
37
|
-
* actors: {
|
|
53
|
+
* actors: {
|
|
54
|
+
* coder: agent({ model: open, instructions: "You fix the failing test." }),
|
|
55
|
+
* cut: worktree(),
|
|
56
|
+
* fail: fail(),
|
|
57
|
+
* },
|
|
38
58
|
* }).createMachine({
|
|
39
59
|
* id: "fix",
|
|
60
|
+
* context: ({ input }) => input,
|
|
40
61
|
* on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
|
|
41
|
-
* initial: "
|
|
62
|
+
* initial: "cutting",
|
|
42
63
|
* states: {
|
|
64
|
+
* cutting: {
|
|
65
|
+
* invoke: {
|
|
66
|
+
* src: "cut",
|
|
67
|
+
* input: ({ context }) => ({ path: fixTree(context), branch: "jr2/fix" }),
|
|
68
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
|
|
69
|
+
* },
|
|
70
|
+
* },
|
|
43
71
|
* coding: {
|
|
44
|
-
* invoke: {
|
|
72
|
+
* invoke: {
|
|
73
|
+
* src: "coder",
|
|
74
|
+
* input: ({ context }) => ({ prompt: "Make `node --test` pass.", cwd: fixTree(context) }),
|
|
75
|
+
* },
|
|
45
76
|
* on: { finish: "done" },
|
|
46
77
|
* },
|
|
47
78
|
* done: { type: "final" },
|
|
79
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
48
80
|
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
49
81
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
50
82
|
* },
|
|
@@ -53,7 +85,6 @@
|
|
|
53
85
|
* // What a package exports: the repository and the model are Open.
|
|
54
86
|
* export const fix = workspace(body, {
|
|
55
87
|
* repos: { app: open },
|
|
56
|
-
* spec: () => ({ branch: "jr2/fix" }),
|
|
57
88
|
* });
|
|
58
89
|
*
|
|
59
90
|
* // What an Instance's `workflows/fix.ts` exports: the same Machine, its Open parts bound.
|
package/src/parts.ts
CHANGED
|
@@ -56,7 +56,9 @@ import { isImageContext } from "./images.ts";
|
|
|
56
56
|
import { isOpenBody, openBodyDeclarationOf, type OpenBodyDeclaration } from "./open-body.ts";
|
|
57
57
|
import { open } from "./open.ts";
|
|
58
58
|
import { repoIdentity } from "./repo-identity.ts";
|
|
59
|
+
import { refuseUnroutedOutcome } from "./effect-checks.ts";
|
|
59
60
|
import { refuseUnroutedAgentFault } from "./mechanism-routes.ts";
|
|
61
|
+
import { refuseSlotActiveTwice } from "./party.ts";
|
|
60
62
|
import { refuseLostOutput } from "./run-output.ts";
|
|
61
63
|
import { userSeatOf, type Size, type UserContainer } from "./size.ts";
|
|
62
64
|
|
|
@@ -75,9 +77,9 @@ import { userSeatOf, type Size, type UserContainer } from "./size.ts";
|
|
|
75
77
|
export { open } from "./open.ts";
|
|
76
78
|
|
|
77
79
|
/**
|
|
78
|
-
* The repository of a Repo Slot: `{ url, ref }`. `url` is the Repo's git url. `ref` is the branch
|
|
79
|
-
* or commit that the branch
|
|
80
|
-
* url is needed, a string is enough.
|
|
80
|
+
* The repository of a Repo Slot: `{ url, ref }`. `url` is the Repo's git url. `ref` is the branch,
|
|
81
|
+
* the tag or the commit that the clone stays on, and that a new branch starts from; absent, the
|
|
82
|
+
* Repo's default branch. Where only a url is needed, a string is enough.
|
|
81
83
|
*
|
|
82
84
|
* `workspace()` and `customize()` refuse:
|
|
83
85
|
* - an empty `url`: ``<where>: Repo Slot "<slot>" is bound to an empty url (got <value>) — a slot
|
|
@@ -102,7 +104,6 @@ export { open } from "./open.ts";
|
|
|
102
104
|
*
|
|
103
105
|
* export const machine = workspace(body, {
|
|
104
106
|
* repos: { app },
|
|
105
|
-
* spec: () => ({ branch: "jr2/hotfix" }),
|
|
106
107
|
* });
|
|
107
108
|
* ```
|
|
108
109
|
*
|
|
@@ -113,8 +114,8 @@ export { open } from "./open.ts";
|
|
|
113
114
|
export type Binding = {
|
|
114
115
|
/** The Repo's git url. */
|
|
115
116
|
url: string;
|
|
116
|
-
/** The branch or commit that the branch
|
|
117
|
-
* branch. */
|
|
117
|
+
/** The branch, tag or commit that the clone stays on, and that a new branch starts from. Default:
|
|
118
|
+
* the Repo's default branch. */
|
|
118
119
|
ref?: string;
|
|
119
120
|
};
|
|
120
121
|
|
|
@@ -130,10 +131,11 @@ export type Binding = {
|
|
|
130
131
|
* - a function of the run input, `({ input }) => url` or a {@link Binding}: each run chooses. The
|
|
131
132
|
* url must match an entry of `git.credentials` in `jr2.config.ts`, or the run cannot use it.
|
|
132
133
|
*
|
|
133
|
-
* The key of the slot is a directory name under `/work`. It starts with a letter,
|
|
134
|
-
* `workspace()` and `customize()` refuse a key that does
|
|
135
|
-
* not a directory name — a slot becomes `/work/<slot
|
|
136
|
-
*
|
|
134
|
+
* The key of the slot is a directory name under `/work`. It starts with a letter, so never with
|
|
135
|
+
* `.`: `/work/.jr2/` is reserved for jr2. `workspace()` and `customize()` refuse a key that does
|
|
136
|
+
* not: ``<where>: Repo Slot key "<key>" is not a directory name — a slot becomes `/work/<slot>`,
|
|
137
|
+
* `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter and matches
|
|
138
|
+
* /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).`` They refuse a value of another
|
|
137
139
|
* shape: ``<where>: Repo Slot "<slot>" is <what> — a slot is `open`, a url, `{ url, ref? }`, or a
|
|
138
140
|
* mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
|
|
139
141
|
*
|
|
@@ -163,7 +165,6 @@ export type Binding = {
|
|
|
163
165
|
* docs: "https://github.com/you/docs.git",
|
|
164
166
|
* config: open,
|
|
165
167
|
* },
|
|
166
|
-
* spec: ({ input }) => ({ branch: `deploy/${input.service}` }),
|
|
167
168
|
* });
|
|
168
169
|
* ```
|
|
169
170
|
*
|
|
@@ -195,12 +196,13 @@ export function repoSlotState(value: RepoSlot<any>): RepoSlotState {
|
|
|
195
196
|
* carry — and to what a prompt can name without quoting. It starts with a LETTER: the handles
|
|
196
197
|
* keep declaration order, which a Machine may give a meaning to (`task`: the first slot is the
|
|
197
198
|
* one the coder edits), and JS puts an integer-like key such as `"1"` ahead of every other key in
|
|
198
|
-
* `Object.keys`, wherever the author wrote it.
|
|
199
|
+
* `Object.keys`, wherever the author wrote it. So a key never starts with `.`, and `/work/.jr2/`
|
|
200
|
+
* stays jr2's (ADR-0075). */
|
|
199
201
|
export const REPO_SLOT_KEY = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
200
202
|
|
|
201
203
|
/**
|
|
202
|
-
* Refuse a malformed slot value BY NAME, at build time —
|
|
203
|
-
*
|
|
204
|
+
* Refuse a malformed slot value BY NAME, at build time — a derives-from-a-typo fault, one build
|
|
205
|
+
* earlier. A bound url is also parsed here: an
|
|
204
206
|
* identity the walk cannot derive (a relative path, an unknown scheme) is refused where the
|
|
205
207
|
* author wrote it rather than at the converge that walks it. `where` names the caller
|
|
206
208
|
* (`workspace()`, `customize()`).
|
|
@@ -209,8 +211,8 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
|
|
|
209
211
|
if (!REPO_SLOT_KEY.test(slot)) {
|
|
210
212
|
throw new Error(
|
|
211
213
|
`${where}: Repo Slot key ${JSON.stringify(slot)} is not a directory name — a slot becomes ` +
|
|
212
|
-
"`/work/<slot
|
|
213
|
-
"/^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).",
|
|
214
|
+
"`/work/<slot>`, `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter " +
|
|
215
|
+
"and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).",
|
|
214
216
|
);
|
|
215
217
|
}
|
|
216
218
|
if (value === open || typeof value === "function") return;
|
|
@@ -242,9 +244,8 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
|
|
|
242
244
|
* to a concrete ref on the port's side, which is what keeps the Machine cluster-agnostic. The
|
|
243
245
|
* Repos are Slots, each bound, open, or per-run.
|
|
244
246
|
*
|
|
245
|
-
* They are options
|
|
246
|
-
*
|
|
247
|
-
* evaluate.
|
|
247
|
+
* They are static options because `jr2 up` must find them by walking the Machine, and no walk can
|
|
248
|
+
* evaluate a function of run input.
|
|
248
249
|
*/
|
|
249
250
|
export type SandboxParts = {
|
|
250
251
|
/** The Sandbox Image. Absent → the Instance's `images/default`, then the stock Harness. */
|
|
@@ -258,6 +259,8 @@ export type SandboxParts = {
|
|
|
258
259
|
/** The Size the Machine states (ADR-0060) — the `workspace()`'s, with any `customize()` layered
|
|
259
260
|
* over it. Absent → the Instance's `sandbox.resources`, then the kit default. */
|
|
260
261
|
resources?: Size;
|
|
262
|
+
/** The pod's work group (ADR-0005). Absent: the port's default. */
|
|
263
|
+
workGroup?: number;
|
|
261
264
|
/** The Held secrets this Workspace's pod gets beside its Agents' model keys (ADR-0059): names of
|
|
262
265
|
* the `secrets` list. Naming is placement — no other pod gets a name for being listed here. */
|
|
263
266
|
secrets?: string[];
|
|
@@ -561,14 +564,15 @@ export type CarriedEffect = {
|
|
|
561
564
|
(| {
|
|
562
565
|
kind: "command";
|
|
563
566
|
argv: string[];
|
|
564
|
-
slot?: string;
|
|
565
567
|
secrets?: string[];
|
|
566
568
|
pin?: true;
|
|
567
569
|
open?: Array<"argv" | "secret">;
|
|
570
|
+
/** A `worktree()`: its `expect`. A report prints it in place of the kit program's argv. */
|
|
571
|
+
cut?: { expect?: "new" | "existing" };
|
|
568
572
|
}
|
|
569
573
|
/** An http Effect (ADR-0068): its method, its fixed target, and the secrets it names — what
|
|
570
574
|
* places each secret, and what its Effector's config allows. `file`: its body is a Worktree
|
|
571
|
-
* file (ADR-0067), so it runs in its Workspace's Sandbox Effector
|
|
575
|
+
* file (ADR-0067), so it runs in its Workspace's Sandbox Effector.
|
|
572
576
|
* An Open url has no target, and names only the secrets its headers and auth name. */
|
|
573
577
|
| {
|
|
574
578
|
kind: "http";
|
|
@@ -577,10 +581,9 @@ export type CarriedEffect = {
|
|
|
577
581
|
secrets: string[];
|
|
578
582
|
idempotent: boolean;
|
|
579
583
|
file?: true;
|
|
580
|
-
slot?: string;
|
|
581
584
|
open?: Array<"url" | "secret">;
|
|
582
585
|
}
|
|
583
|
-
/** A push (ADR-0067, ADR-
|
|
586
|
+
/** A push (ADR-0067, ADR-0075): a cut Worktree's branch to its Repo Slot's Binding. It names no
|
|
584
587
|
* secret: `jr2 up` resolves its credential through `git.credentials`. */
|
|
585
588
|
| { kind: "push"; slot?: string; sign: boolean; squash: boolean; force: boolean; pin: boolean; secrets: string[] }
|
|
586
589
|
/** A dispatch (ADR-0071): the Workflow it starts a run of, as `jr2 run` names it, whether it
|
|
@@ -808,6 +811,9 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
|
|
|
808
811
|
refuseInlineParts(machine);
|
|
809
812
|
refuseLostOutput(machine);
|
|
810
813
|
refuseUnroutedAgentFault(machine);
|
|
814
|
+
refuseUnroutedOutcome(machine);
|
|
815
|
+
// Here too, for a Machine built with xstate's own setup: the Turns' identity is the runtime's.
|
|
816
|
+
refuseSlotActiveTwice(machine);
|
|
811
817
|
const parts = sandboxPartsOf(machine);
|
|
812
818
|
const user = userSeatOf(parts.user);
|
|
813
819
|
collectImage(parts.image);
|
|
@@ -968,7 +974,6 @@ export function carriedEffectOf(
|
|
|
968
974
|
idempotent: parts.idempotent,
|
|
969
975
|
timeoutMs: parts.timeoutMs,
|
|
970
976
|
...(parts.file ? { file: true as const } : {}),
|
|
971
|
-
...(parts.file?.slot !== undefined ? { slot: parts.file.slot } : {}),
|
|
972
977
|
};
|
|
973
978
|
}
|
|
974
979
|
if (parts.kind === "push") {
|
|
@@ -988,7 +993,7 @@ export function carriedEffectOf(
|
|
|
988
993
|
...(slot !== undefined ? { slot } : {}),
|
|
989
994
|
};
|
|
990
995
|
}
|
|
991
|
-
const { argv, timeoutMs,
|
|
996
|
+
const { argv, timeoutMs, secrets, pin } = parts;
|
|
992
997
|
return {
|
|
993
998
|
name,
|
|
994
999
|
path,
|
|
@@ -997,10 +1002,10 @@ export function carriedEffectOf(
|
|
|
997
1002
|
placement,
|
|
998
1003
|
argv: argvText(argv),
|
|
999
1004
|
timeoutMs,
|
|
1000
|
-
...(slot !== undefined ? { slot } : {}),
|
|
1001
1005
|
...(secrets?.length ? { secrets: [...secrets] } : {}),
|
|
1002
1006
|
...(pin ? { pin } : {}),
|
|
1003
1007
|
...(openParts.length ? { open: openParts as Array<"argv" | "secret"> } : {}),
|
|
1008
|
+
...(parts.cut ? { cut: { ...parts.cut } } : {}),
|
|
1004
1009
|
};
|
|
1005
1010
|
}
|
|
1006
1011
|
|
|
@@ -1039,6 +1044,9 @@ export type WorkspacePod = {
|
|
|
1039
1044
|
* credentialed `command` slot below the `workspace()`, down to the next `workspace()` — read off
|
|
1040
1045
|
* their plain data, statically. Empty → the pod gets no Sandbox Effector and no Secret. */
|
|
1041
1046
|
effects: CarriedEffect[];
|
|
1047
|
+
/** The slot of each `worktree()` below the `workspace()`, down to the next `workspace()`. A cut runs
|
|
1048
|
+
* in the Harness container, so `effects` does not hold it. Absent when there is none. */
|
|
1049
|
+
cuts?: string[];
|
|
1042
1050
|
/** The `workspace()`'s `effector: { image }`, as written. Absent → the Sandbox Image. */
|
|
1043
1051
|
effector?: { image: string };
|
|
1044
1052
|
/** The `workspace()`'s own `image`, as written — its Sandbox Image. Absent → `images/default`,
|
|
@@ -1067,6 +1075,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
|
|
|
1067
1075
|
models: Set<string>,
|
|
1068
1076
|
on: Set<AnyStateMachine>,
|
|
1069
1077
|
effects: Map<string, CarriedEffect>,
|
|
1078
|
+
cuts: Set<string>,
|
|
1070
1079
|
path: OpenSlot["path"],
|
|
1071
1080
|
route: PlacementRoute,
|
|
1072
1081
|
): void => {
|
|
@@ -1074,7 +1083,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
|
|
|
1074
1083
|
on.add(machine);
|
|
1075
1084
|
const body = wrapperBodyOf(machine);
|
|
1076
1085
|
const visit = (child: AnyStateMachine, at: OpenSlot["path"], to: PlacementRoute) => {
|
|
1077
|
-
if (!composesSandbox(child)) modelsIn(child, models, on, effects, at, to);
|
|
1086
|
+
if (!composesSandbox(child)) modelsIn(child, models, on, effects, cuts, at, to);
|
|
1078
1087
|
};
|
|
1079
1088
|
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
1080
1089
|
if (isAgent(logic)) {
|
|
@@ -1093,6 +1102,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
|
|
|
1093
1102
|
if (isEffect(logic)) {
|
|
1094
1103
|
const effect = carriedEffectOf(name, path, route, logic.effect);
|
|
1095
1104
|
if (effect.placement === "sandbox") effects.set(JSON.stringify([route, name]), effect);
|
|
1105
|
+
if (logic.effect.kind === "command" && logic.effect.cut) cuts.add(name);
|
|
1096
1106
|
continue;
|
|
1097
1107
|
}
|
|
1098
1108
|
const child = asMachine(logic);
|
|
@@ -1112,7 +1122,8 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
|
|
|
1112
1122
|
if (composesSandbox(machine)) {
|
|
1113
1123
|
const models = new Set<string>();
|
|
1114
1124
|
const effects = new Map<string, CarriedEffect>();
|
|
1115
|
-
|
|
1125
|
+
const cuts = new Set<string>();
|
|
1126
|
+
modelsIn(machine, models, new Set(), effects, cuts, path, route);
|
|
1116
1127
|
const parts = sandboxPartsOf(machine);
|
|
1117
1128
|
const bindings: Record<string, string | undefined> = {};
|
|
1118
1129
|
if (parts.repos !== open) {
|
|
@@ -1127,6 +1138,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
|
|
|
1127
1138
|
secrets: [...(parts.secrets ?? [])],
|
|
1128
1139
|
models: [...models].sort(),
|
|
1129
1140
|
effects: [...effects.values()],
|
|
1141
|
+
...(cuts.size ? { cuts: [...cuts] } : {}),
|
|
1130
1142
|
...(parts.effector ? { effector: { ...parts.effector } } : {}),
|
|
1131
1143
|
...(parts.image !== undefined ? { image: parts.image } : {}),
|
|
1132
1144
|
bindings,
|
package/src/party.ts
CHANGED
|
@@ -422,6 +422,39 @@ function orthogonal(a: Node, b: Node): boolean {
|
|
|
422
422
|
/** Can the two states be active at the same time? */
|
|
423
423
|
const together = (a: Node, b: Node): boolean => isAncestorOrSelf(a, b) || isAncestorOrSelf(b, a) || orthogonal(a, b);
|
|
424
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Refuse one Agent slot invoked by two states that can be active together (ADR-0015): in two
|
|
427
|
+
* parallel regions, one state inside the other, or twice by one state. A slot is one Agent of this
|
|
428
|
+
* Machine: its Turns share one sender, so `sentBy` cannot tell them apart and one pick matches both
|
|
429
|
+
* writings, and they share one conversation, so `continue` cannot hold two. Two Agents at once are
|
|
430
|
+
* two slots (one definition can fill both), a child Machine in each region, or a Pool (ADR-0075).
|
|
431
|
+
*/
|
|
432
|
+
export function refuseSlotActiveTwice(machine: AnyStateMachine): void {
|
|
433
|
+
const bySlot = new Map<string, Node[]>();
|
|
434
|
+
for (const node of allNodes(machine))
|
|
435
|
+
for (const p of partiesAt(node, machine))
|
|
436
|
+
if (p.party === "agent") bySlot.set(p.id, [...(bySlot.get(p.id) ?? []), node]);
|
|
437
|
+
for (const [slot, nodes] of bySlot) {
|
|
438
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
439
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
440
|
+
const [a, b] = [nodes[i]!, nodes[j]!];
|
|
441
|
+
if (!together(a, b)) continue;
|
|
442
|
+
const how =
|
|
443
|
+
a === b
|
|
444
|
+
? `state "${a.id}" invokes it twice`
|
|
445
|
+
: `"${a.id}" and "${b.id}" invoke it and can be active together ` +
|
|
446
|
+
(orthogonal(a, b) ? "(parallel regions)" : "(one inside the other)");
|
|
447
|
+
throw new Error(
|
|
448
|
+
`machine "${machine.id}": Agent "${slot}" would run two Turns at once: ${how}. A slot is one Agent, with ` +
|
|
449
|
+
"one sender and one conversation, so `sentBy` cannot tell its two Turns apart and `continue` cannot " +
|
|
450
|
+
"hold both. For two Agents at once, use two slots (one definition can fill both), a child Machine in " +
|
|
451
|
+
"each region, or a Pool (ADR-0075)",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
425
458
|
/** One writing: one `on` key on one state, with its candidates (an array writing has several). */
|
|
426
459
|
type Writing = { node: Node; name: string; candidates: Transition[] };
|
|
427
460
|
|