@jr2/orchestrator 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/bin/server.ts +7 -4
  2. package/console/canvas.ts +27 -14
  3. package/console/components/app.ts +10 -1
  4. package/console/components/fleet.ts +39 -6
  5. package/console/components/machine-pane.ts +89 -2
  6. package/console/components/nav.ts +1 -1
  7. package/console/components/schema-form.ts +12 -4
  8. package/console/json-field.ts +23 -0
  9. package/console/main.ts +110 -39
  10. package/console/store.ts +221 -17
  11. package/console/style.css +90 -0
  12. package/package.json +4 -4
  13. package/src/actor.ts +502 -49
  14. package/src/ambient.ts +2 -0
  15. package/src/assembly-checks.ts +165 -0
  16. package/src/assembly-tar.ts +494 -0
  17. package/src/assembly.ts +721 -0
  18. package/src/chart.ts +2050 -0
  19. package/src/charts.ts +720 -0
  20. package/src/config.ts +152 -46
  21. package/src/content-hash.ts +105 -0
  22. package/src/custodian.lua +6 -5
  23. package/src/custodian.ts +71 -19
  24. package/src/customize.ts +216 -25
  25. package/src/effect-checks.ts +557 -0
  26. package/src/effect.ts +905 -0
  27. package/src/effector-wire.ts +525 -0
  28. package/src/fingerprint.ts +4 -3
  29. package/src/gate.ts +8 -0
  30. package/src/git-clone.ts +65 -0
  31. package/src/git-push.ts +336 -0
  32. package/src/harness-client.ts +245 -23
  33. package/src/held-secrets.ts +82 -26
  34. package/src/holds.ts +156 -0
  35. package/src/http-effect.ts +981 -0
  36. package/src/http.ts +496 -104
  37. package/src/images.ts +47 -21
  38. package/src/index.ts +43 -2
  39. package/src/instance.ts +444 -58
  40. package/src/kube-client.ts +333 -0
  41. package/src/machine-doc.ts +88 -6
  42. package/src/mount-digest.ts +53 -0
  43. package/src/names.ts +66 -8
  44. package/src/notices.ts +88 -0
  45. package/src/open-body.ts +1444 -0
  46. package/src/open-effect.ts +144 -0
  47. package/src/open.ts +4 -4
  48. package/src/parts.ts +530 -33
  49. package/src/party.ts +468 -0
  50. package/src/pick.ts +309 -0
  51. package/src/pool.ts +1 -1
  52. package/src/registration.ts +301 -17
  53. package/src/repo-fetch.ts +60 -85
  54. package/src/repos.ts +63 -74
  55. package/src/rest.ts +133 -0
  56. package/src/run-host.ts +1832 -98
  57. package/src/sandbox-effect.ts +891 -0
  58. package/src/sandbox-effector.ts +111 -0
  59. package/src/sandbox-kube.ts +1719 -0
  60. package/src/sandbox-watch.ts +260 -0
  61. package/src/secret-placement.ts +494 -0
  62. package/src/server.ts +281 -84
  63. package/src/setup.ts +589 -70
  64. package/src/size.ts +342 -0
  65. package/src/snapshot-store.ts +64 -13
  66. package/src/stub-harness.ts +5 -0
  67. package/src/sync-client.ts +54 -0
  68. package/src/timers.ts +178 -0
  69. package/src/tokens.ts +35 -4
  70. package/src/vocabulary.ts +66 -1
  71. package/src/wire.ts +156 -2
  72. package/src/workspace.ts +529 -194
  73. package/src/sandbox-kubectl.ts +0 -1108
package/src/holds.ts ADDED
@@ -0,0 +1,156 @@
1
+ // The held answer (ADR-0024, ADR-0013): a pick that moves the workflow to a state inside the Agent's
2
+ // state is answered when the workflow is back at Rest there (rest.ts). Until then the pick is
3
+ // OUTSTANDING: its POST answered at once with a delivery id, and the Harness long-polls
4
+ // `GET /agents/:iid/deliveries/:id` for the answer, inside the one tool call.
5
+ //
6
+ // A hold is plain data on the run (`RunBlob.holds`), saved with the snapshot, so a park or an
7
+ // Orchestrator restart is a longer wait in the same Turn: the restored host reads the same Rest off
8
+ // the restored Machine, and answers the same poll. What a hold keeps beside the delivery id is EVERY
9
+ // reply from below the Agent's state since the pick (`chain`) — the last is the answer's default —
10
+ // because the events that carried them are gone after a restart.
11
+ //
12
+ // The answer is the last reply since the pick, rendered by the Harness: a party's event (a Gate's
13
+ // answer, a nested Agent's pick) with its payload, or a child's output (a child Machine, an Effect
14
+ // stand-in). A chain link is a reply from below the Agent's state — from the set of active states
15
+ // under it, never from a sibling region or the root; the events the Machine sent itself (`raise`,
16
+ // `sendTo(self)`, a timer) are skipped, and so is a child's error: every outcome of work is its
17
+ // output, and an error is a jr2 or config defect, never an answer. With no link, the answer carries
18
+ // no reply — it does not say whether work ran.
19
+
20
+ import type { AnyActorRef, AnyEventObject, AnyMachineSnapshot, AnyStateMachine } from "xstate";
21
+ import { SENDER_KEY, type Sender } from "@jr2/agent-protocol";
22
+ import { activeBelow } from "./rest.ts";
23
+ import type { PickAnswer } from "./wire.ts";
24
+
25
+ type Node = AnyStateMachine["root"];
26
+
27
+ /** One outstanding pick, or its answer not yet superseded. JSON-safe: it rides the run blob. */
28
+ export type Hold = {
29
+ /** The Agent address the pick was posted to. */
30
+ instanceId: string;
31
+ deliveryId: string;
32
+ /** The pick's event name. */
33
+ pick: string;
34
+ /** The Agent slot that picked — the Console's "held since" names it. */
35
+ agent: string;
36
+ /** ISO time the pick was delivered: the Console's "held since". */
37
+ since: string;
38
+ /** The Sandbox whose token may read the answer (ADR-0013) — the registration's, kept because the
39
+ * answer outlives a registration that a route out of the Agent's state ends. */
40
+ sandbox?: string;
41
+ /** The invoke id of the Agent's state when the pick was made: a re-registration of the same
42
+ * address by another state is another Turn. */
43
+ invokeId?: string;
44
+ /** The continued conversation (ADR-0057) the picking Turn is part of: where a route out of its
45
+ * state leaves the answer as a notice for its next Turn. Absent for a fresh Turn: no notice. */
46
+ conversation?: string;
47
+ /** Every reply since the pick, oldest first (ADR-0024): the last is the answer's default, and
48
+ * the whole is the `chain` an `answer` action reads (ADR-0067). Each link whole, a command's tail
49
+ * included, because the `answer` action may read it after a restore; dropped once `answer` is
50
+ * set. So a loop below the Agent's state grows the blob by one tail per pass while the pick is
51
+ * outstanding. */
52
+ chain?: PickAnswer[];
53
+ /** What the Machine's `answer` action set since the pick (ADR-0067): it replaces the default.
54
+ * Wrapped, so an answer of `null` is still one. */
55
+ shaped?: { value: unknown };
56
+ /** The last event the Machine took while the Agent's state was the hold's, with work still running
57
+ * below it, was no reply from below and no event a Machine sent itself at once: a timer the Machine
58
+ * set, an event from outside the states below, a party's event, the Agent's own end. If the Machine
59
+ * then left the Agent's state, the pick's route did not take it out: the exit stopped the work
60
+ * below (`HeldAnswer.stopped`). An event a Machine sends itself at once (`sendTo(self)`; a `raise`
61
+ * is never relayed) is part of the route of the event whose actions sent it and changes nothing
62
+ * here; a reply clears it. */
63
+ cutShort?: true;
64
+ /** The answer, once the Machine is back at Rest or has left the Agent's state. */
65
+ answer?: HeldAnswer;
66
+ /** The answer reached the model: the poll read it, or a re-admission carried it (ADR-0031). */
67
+ fetched?: boolean;
68
+ };
69
+
70
+ /** A pick's answer from what the Machine did since it: the author's `answer` when one ran, else the
71
+ * last reply; undefined when neither — the answer then carries no reply. */
72
+ export function answerOf(record: {
73
+ chain?: readonly PickAnswer[];
74
+ shaped?: { value: unknown };
75
+ }): PickAnswer | undefined {
76
+ if (record.shaped !== undefined) return { kind: "answer", value: record.shaped.value };
77
+ return record.chain?.at(-1);
78
+ }
79
+
80
+ /** The answer to an outstanding pick: the receipt's facts at the moment of Rest (ADR-0024). */
81
+ export type HeldAnswer = {
82
+ /** The Machine left the Agent's state: the Turn is over. */
83
+ turnComplete: boolean;
84
+ /** The Allowed picks read AFTER the route (ADR-0029); empty once the Turn is over. */
85
+ allowed: string[];
86
+ /** The last reply since the pick — kept when the work was stopped after an earlier link replied. */
87
+ answer?: PickAnswer;
88
+ /** With `turnComplete`: something other than the pick's route left the Agent's state while the
89
+ * pick's work ran, and that exit stopped the work before it finished (`Hold.cutShort`). */
90
+ stopped?: true;
91
+ };
92
+
93
+ /**
94
+ * The reply one event is, when the invoking Machine receives it during a hold — or undefined when it
95
+ * is no reply. Rest, the answer and the ledger are computed over the SET of active states below the
96
+ * Agent's state (`seat`), so a reply comes from below it:
97
+ *
98
+ * - from a child invoked on an active state below the seat (`source` is that child: its end, or an
99
+ * event it sends its parent), or
100
+ * - from a party seated below it: a Gate whose invoke id, or an Agent whose slot, is invoked on an
101
+ * active state below the seat (the event's `sender`, ADR-0015).
102
+ *
103
+ * Everything else is skipped: the Machine's own events (`source` is the Machine itself: a `raise`, a
104
+ * `sendTo(self)`, a timer), an xstate event other than a child's output — a child's error
105
+ * (`xstate.error.actor.*`) is a defect, never a reply, so its message never reaches the model — and
106
+ * an event from outside the set: a Gate in a sibling region, a root Gate, an outside `jr2 send`.
107
+ * Read it as the event is relayed, before the Machine processes it: the states the reply came from are still active. A
108
+ * child is named by its slot (`src`) where it has one, else by its invoke id.
109
+ */
110
+ export function replyOf(
111
+ event: AnyEventObject,
112
+ source: AnyActorRef | undefined,
113
+ machine: AnyActorRef,
114
+ seat: Node,
115
+ ): PickAnswer | undefined {
116
+ if (source === machine) return undefined;
117
+ const below = activeBelow(machine.getSnapshot() as AnyMachineSnapshot, seat).flatMap((node) => node.invoke);
118
+ const sender = (event as { [SENDER_KEY]?: Sender })[SENDER_KEY];
119
+ const fromChild = source !== undefined && below.some((inv) => inv.id === source.id);
120
+ const fromParty =
121
+ sender !== undefined &&
122
+ below.some((inv) => (sender.party === "gate" ? inv.id === sender.id : inv.src === sender.id));
123
+ if (!fromChild && !fromParty) return undefined;
124
+ const done = /^xstate\.done\.actor\.(.+)$/.exec(event.type);
125
+ if (done) {
126
+ const src = (source as { src?: unknown } | undefined)?.src;
127
+ const actor = typeof src === "string" ? src : done[1]!;
128
+ return { kind: "output", actor, output: (event as { output?: unknown }).output };
129
+ }
130
+ if (event.type.startsWith("xstate.")) return undefined;
131
+ const payload = Object.fromEntries(Object.entries(event).filter(([key]) => key !== "type" && key !== SENDER_KEY));
132
+ return { kind: "event", event: event.type, ...(sender ? { sender: { ...sender } } : {}), payload };
133
+ }
134
+
135
+ /**
136
+ * The error one event is, when it is a child's error from below the Agent's state (`seat`) — or
137
+ * undefined. It is no reply (`replyOf`): the Agent is not told by default, and the operator reads it
138
+ * on the run's feed (ADR-0024). The child is named as `replyOf` names it.
139
+ */
140
+ export function childErrorOf(
141
+ event: AnyEventObject,
142
+ source: AnyActorRef | undefined,
143
+ machine: AnyActorRef,
144
+ seat: Node,
145
+ ): { child: string; error: string } | undefined {
146
+ const failed = /^xstate\.error\.actor\.(.+)$/.exec(event.type);
147
+ if (!failed || source === undefined || source === machine) return undefined;
148
+ const below = activeBelow(machine.getSnapshot() as AnyMachineSnapshot, seat).flatMap((node) => node.invoke);
149
+ if (!below.some((inv) => inv.id === source.id)) return undefined;
150
+ const src = (source as { src?: unknown }).src;
151
+ const error = (event as { error?: unknown }).error;
152
+ return {
153
+ child: typeof src === "string" ? src : failed[1]!,
154
+ error: error instanceof Error ? error.message : String(error),
155
+ };
156
+ }