@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.
- package/bin/server.ts +7 -4
- package/console/canvas.ts +27 -14
- package/console/components/app.ts +10 -1
- package/console/components/fleet.ts +39 -6
- package/console/components/machine-pane.ts +89 -2
- package/console/components/nav.ts +1 -1
- package/console/components/schema-form.ts +12 -4
- package/console/json-field.ts +23 -0
- package/console/main.ts +110 -39
- package/console/store.ts +221 -17
- package/console/style.css +90 -0
- package/package.json +4 -4
- package/src/actor.ts +502 -49
- package/src/ambient.ts +2 -0
- package/src/assembly-checks.ts +165 -0
- package/src/assembly-tar.ts +494 -0
- package/src/assembly.ts +721 -0
- package/src/chart.ts +2050 -0
- package/src/charts.ts +720 -0
- package/src/config.ts +152 -46
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +6 -5
- package/src/custodian.ts +71 -19
- package/src/customize.ts +216 -25
- package/src/effect-checks.ts +557 -0
- package/src/effect.ts +905 -0
- package/src/effector-wire.ts +525 -0
- package/src/fingerprint.ts +4 -3
- package/src/gate.ts +8 -0
- package/src/git-clone.ts +65 -0
- package/src/git-push.ts +336 -0
- package/src/harness-client.ts +245 -23
- package/src/held-secrets.ts +82 -26
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +981 -0
- package/src/http.ts +496 -104
- package/src/images.ts +47 -21
- package/src/index.ts +43 -2
- package/src/instance.ts +444 -58
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +88 -6
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +66 -8
- package/src/notices.ts +88 -0
- package/src/open-body.ts +1444 -0
- package/src/open-effect.ts +144 -0
- package/src/open.ts +4 -4
- package/src/parts.ts +530 -33
- package/src/party.ts +468 -0
- package/src/pick.ts +309 -0
- package/src/pool.ts +1 -1
- package/src/registration.ts +301 -17
- package/src/repo-fetch.ts +60 -85
- package/src/repos.ts +63 -74
- package/src/rest.ts +133 -0
- package/src/run-host.ts +1832 -98
- package/src/sandbox-effect.ts +891 -0
- package/src/sandbox-effector.ts +111 -0
- package/src/sandbox-kube.ts +1719 -0
- package/src/sandbox-watch.ts +260 -0
- package/src/secret-placement.ts +494 -0
- package/src/server.ts +281 -84
- package/src/setup.ts +589 -70
- package/src/size.ts +342 -0
- package/src/snapshot-store.ts +64 -13
- package/src/stub-harness.ts +5 -0
- package/src/sync-client.ts +54 -0
- package/src/timers.ts +178 -0
- package/src/tokens.ts +35 -4
- package/src/vocabulary.ts +66 -1
- package/src/wire.ts +156 -2
- package/src/workspace.ts +529 -194
- package/src/sandbox-kubectl.ts +0 -1108
package/src/registration.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The internal registration table (ADR-0011): one structure behind both delivery dialects.
|
|
2
|
-
// An Agent slot and `gate` register `address → { accepted event defs, deliver closure, meta
|
|
3
|
-
// the
|
|
4
|
-
//
|
|
2
|
+
// An Agent slot and `gate` register `address → { accepted event defs, deliver closure, meta,
|
|
3
|
+
// sender }` — the sender written on every event it delivers (ADR-0015); the two HTTP surfaces
|
|
4
|
+
// (`/agents/:iid/*` for the Agent's Harness, `/runs/:id/gates/*` for humans and webhooks) are
|
|
5
|
+
// adapters over it — lookup, schema validation, delivery, discovery, and lifecycle
|
|
5
6
|
// are implemented ONCE. The table is implementation structure, not vocabulary: workflows speak
|
|
6
7
|
// only `defineEvent` / `agent` / `gate`.
|
|
7
8
|
//
|
|
@@ -23,11 +24,20 @@
|
|
|
23
24
|
// `resolveAccepts` reads them off the invoking Machine below and a nested Machine's names are
|
|
24
25
|
// never the root's problem.
|
|
25
26
|
|
|
26
|
-
import
|
|
27
|
-
import type {
|
|
27
|
+
import { getStateNodes } from "xstate";
|
|
28
|
+
import type { ActorSystem, AnyActorRef, AnyEventObject, AnyMachineSnapshot, AnyStateMachine } from "xstate";
|
|
29
|
+
import { SENDER_KEY, type EventDef, type Sender } from "@jr2/agent-protocol";
|
|
28
30
|
import type { AgentAdmission } from "./actor.ts";
|
|
31
|
+
import type { EffectMarker, EffectRow } from "./effect.ts";
|
|
32
|
+
import type { EffectorPlacement, EffectorTarget } from "./http-effect.ts";
|
|
33
|
+
import type { NoticeScope, TakenNotices } from "./notices.ts";
|
|
34
|
+
import type { AnswerSeat } from "./pick.ts";
|
|
35
|
+
import type { Notice, PickAnswer } from "./wire.ts";
|
|
29
36
|
import type { SandboxPort } from "./workspace.ts";
|
|
30
37
|
import { invokingMachine, vocabularyOf } from "./vocabulary.ts";
|
|
38
|
+
import type { ChartText } from "./chart.ts";
|
|
39
|
+
import { notTaken, senderText, takesNow, type NotTakenWriting } from "./party.ts";
|
|
40
|
+
import { restOf } from "./rest.ts";
|
|
31
41
|
|
|
32
42
|
/** xstate doesn't export its internal AnyActorSystem; this matches what actors receive. */
|
|
33
43
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -45,8 +55,17 @@ export type Registration = {
|
|
|
45
55
|
kind: "gate" | "agent";
|
|
46
56
|
/** The caller-facing id within its dialect (the gate id, or the agent's instance id). */
|
|
47
57
|
id: string;
|
|
48
|
-
/** Accepted events, name→def — the validation scope AND the discovery listing.
|
|
58
|
+
/** Accepted events, name→def — the validation scope AND the discovery listing. For an Agent,
|
|
59
|
+
* the invoking state's derived set: what its Allowed picks are narrowed from (ADR-0029). */
|
|
49
60
|
defs: Map<string, EventDef>;
|
|
61
|
+
/**
|
|
62
|
+
* An Agent's MENU (ADR-0029), in its fixed order: the union of the derived sets of every state
|
|
63
|
+
* that invokes an Agent of this name, and the tools block the Harness presents — byte-stable for
|
|
64
|
+
* the conversation, so it never breaks the provider's prompt cache. A superset of {@link defs};
|
|
65
|
+
* a pick on it that {@link defs} lacks is refused at delivery, naming the Allowed picks. Absent
|
|
66
|
+
* on gates, and on an Agent registered with no Menu, which serves its `defs` as the Menu.
|
|
67
|
+
*/
|
|
68
|
+
menu?: Map<string, EventDef>;
|
|
50
69
|
/** Serializable caller/integration context (PR URL, title …) — rides the discovery listing. */
|
|
51
70
|
meta?: Record<string, unknown>;
|
|
52
71
|
/**
|
|
@@ -64,6 +83,9 @@ export type Registration = {
|
|
|
64
83
|
sandbox?: string;
|
|
65
84
|
/** Close over the invoking state's `sendBack`; delivery lands where the actor was invoked. */
|
|
66
85
|
deliver: (event: DeliveredEvent) => void;
|
|
86
|
+
/** An Agent's: the Harness read this surface (`GET /agents/:iid/surface`), so it has the Turn in
|
|
87
|
+
* hand and prompts it. Until then the Turn may end with no model ever asked (ADR-0026). */
|
|
88
|
+
served?: () => void;
|
|
67
89
|
/**
|
|
68
90
|
* The Machine that invoked this actor — `self._parent`, captured at registration. The menu was
|
|
69
91
|
* derived from THIS machine's transitions (ADR-0015), so it is the only snapshot whose guards
|
|
@@ -71,6 +93,22 @@ export type Registration = {
|
|
|
71
93
|
* that have no parent to name, which is why every read of it fails open.
|
|
72
94
|
*/
|
|
73
95
|
invoker?: AnyActorRef;
|
|
96
|
+
/**
|
|
97
|
+
* Who this registration delivers as (ADR-0015): an Agent by its slot, a Gate by its invoke id.
|
|
98
|
+
* The table writes it on every event it delivers (`event.sender`), never taken from the caller.
|
|
99
|
+
* Absent on a registration no party made (a bare unit-test entry): its events carry none.
|
|
100
|
+
*/
|
|
101
|
+
sender?: Sender;
|
|
102
|
+
/** The invoke id of the actor that registered: what finds its state — its seat — in the
|
|
103
|
+
* Machine, for the party questions (`party.ts`). */
|
|
104
|
+
invokeId?: string;
|
|
105
|
+
/**
|
|
106
|
+
* An Agent's: the continued conversation ({@link continuedIid}) this Turn is part of — set only
|
|
107
|
+
* when the Turn continues one (`continue: true`). A pick whose route leaves the Agent's state is
|
|
108
|
+
* answered by a notice to it (ADR-0024); a fresh Turn's conversation is never heard again, so its
|
|
109
|
+
* answer has nowhere to go.
|
|
110
|
+
*/
|
|
111
|
+
conversation?: string;
|
|
74
112
|
};
|
|
75
113
|
|
|
76
114
|
/**
|
|
@@ -98,6 +136,41 @@ export function wouldMove(invoker: AnyActorRef | undefined, event: AnyEventObjec
|
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The states this exact event's transitions name as targets, by id — empty for a pick with no
|
|
141
|
+
* target. Read before the send and checked after it with `leftAgain`: what tells a pick that entered
|
|
142
|
+
* a state and left it again inside its own send (an `always` back to where it was) from one that
|
|
143
|
+
* stayed where it went or went nowhere (ADR-0024). Fails CLOSED: a read that cannot be made reads
|
|
144
|
+
* as no target, the plain receipt.
|
|
145
|
+
*/
|
|
146
|
+
export function targetsOf(invoker: AnyActorRef | undefined, event: AnyEventObject): string[] {
|
|
147
|
+
if (!invoker) return [];
|
|
148
|
+
try {
|
|
149
|
+
const snapshot = invoker.getSnapshot() as AnyMachineSnapshot;
|
|
150
|
+
return (snapshot.machine as AnyStateMachine)
|
|
151
|
+
.getTransitionData(snapshot, event)
|
|
152
|
+
.flatMap((t) => (t.target ?? []).map((node) => node.id));
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Is some state of `targets` no longer active — entered by the send and left again inside it? A
|
|
160
|
+
* target to the state the pick was taken in (`drafting: { on: { note: "drafting" } }`) is still
|
|
161
|
+
* active, so it is not. Fails CLOSED: false, the plain receipt.
|
|
162
|
+
*/
|
|
163
|
+
export function leftAgain(invoker: AnyActorRef | undefined, targets: readonly string[]): boolean {
|
|
164
|
+
if (!invoker || targets.length === 0) return false;
|
|
165
|
+
try {
|
|
166
|
+
const snapshot = invoker.getSnapshot() as AnyMachineSnapshot;
|
|
167
|
+
const active = new Set(getStateNodes(snapshot.machine.root, snapshot.value).map((node) => node.id));
|
|
168
|
+
return targets.some((id) => !active.has(id));
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
101
174
|
/**
|
|
102
175
|
* COULD this event move the invoking Machine, judged before the Agent has picked its arguments?
|
|
103
176
|
* What the surface build can ask (ADR-0029) — there is no payload yet, so a guard reading one
|
|
@@ -138,10 +211,82 @@ export function mayMove(invoker: AnyActorRef | undefined, type: string): boolean
|
|
|
138
211
|
return readPayload;
|
|
139
212
|
}
|
|
140
213
|
|
|
214
|
+
/** Would the run take `type` from this registration now, payload-blind? Asked AS its sender
|
|
215
|
+
* ({@link takesNow}); a registration with no seat to ask from (a plain `setup()` actor tree, a bare
|
|
216
|
+
* test entry) falls back to {@link mayMove}. */
|
|
217
|
+
function takenNow(reg: Pick<Registration, "invoker" | "sender" | "invokeId">, type: string): boolean {
|
|
218
|
+
return takesNow(reg, type) ?? mayMove(reg.invoker, type);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* An Agent's Allowed picks now (ADR-0029): the invoking state's derived set, narrowed to what the
|
|
223
|
+
* invoking Machine would take from this Agent ({@link takenNow}), in Menu order — so the list reads
|
|
224
|
+
* in the order the tools block does. The ONE reading of the list: the surface, every receipt, the
|
|
225
|
+
* nudge and the wake all ask here, so no two of them can disagree about what the Turn may pick
|
|
226
|
+
* (ADR-0016).
|
|
227
|
+
*
|
|
228
|
+
* EMPTY while the Agent's state is not at Rest (rest.ts, ADR-0024): something below it runs, or a
|
|
229
|
+
* timer holds, and every pick is refused until it is back at Rest — so the Rest rule and the
|
|
230
|
+
* payload-blind selection give one list, and a Turn that starts busy is not asked into a refusal.
|
|
231
|
+
*/
|
|
232
|
+
export function allowedPicks(reg: Pick<Registration, "defs" | "menu" | "invoker" | "sender" | "invokeId">): string[] {
|
|
233
|
+
if (restOf(reg) === "busy") return [];
|
|
234
|
+
const ordered = new Set([...(reg.menu ?? reg.defs).keys()].filter((name) => reg.defs.has(name)));
|
|
235
|
+
for (const name of reg.defs.keys()) ordered.add(name);
|
|
236
|
+
return [...ordered].filter((name) => takenNow(reg, name));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A Gate's `acceptsNow` (ADR-0011): its accepted set, narrowed the same way as an Agent's Allowed
|
|
240
|
+
* picks — the names the run would take from this Gate now. */
|
|
241
|
+
export function acceptsNow(reg: Pick<Registration, "defs" | "invoker" | "sender" | "invokeId">): string[] {
|
|
242
|
+
return [...reg.defs.keys()].filter((name) => takenNow(reg, name));
|
|
243
|
+
}
|
|
244
|
+
|
|
141
245
|
/** Delivery target absent (unknown address, settled run, exited state) — the one catch point. */
|
|
142
246
|
export class UnknownAddressError extends Error {}
|
|
143
|
-
/** Delivery body rejected (unaccepted name, or payload failing the named schema).
|
|
144
|
-
|
|
247
|
+
/** Delivery body rejected (unaccepted name, or payload failing the named schema). An Agent's
|
|
248
|
+
* refusal carries its Allowed picks, read after the refusal (ADR-0029), so the Harness can say
|
|
249
|
+
* what the Turn may pick instead. */
|
|
250
|
+
export class EventValidationError extends Error {
|
|
251
|
+
allowed?: string[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A pick that arrived while the Agent's state is not at Rest (ADR-0024): refused, `finish` included,
|
|
256
|
+
* and nothing is delivered — xstate would take it, and a pick its state handles would exit the child
|
|
257
|
+
* state and stop the work running there. `outstanding` names this Turn's pick still out, when there
|
|
258
|
+
* is one; without it, the state is busy with work the Turn did not pick (it started busy).
|
|
259
|
+
*/
|
|
260
|
+
export class OutstandingError extends Error {
|
|
261
|
+
readonly outstanding?: string;
|
|
262
|
+
constructor(message: string, outstanding?: string) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.outstanding = outstanding;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Refuse a caller's body that carries the sender (ADR-0013, ADR-0015): only the Orchestrator writes
|
|
269
|
+
* it, from the registration, so neither the Agent's wire nor the gates route takes it. */
|
|
270
|
+
export function refuseSenderField(payload: unknown): void {
|
|
271
|
+
if (payload && typeof payload === "object" && SENDER_KEY in payload) {
|
|
272
|
+
throw new EventValidationError(
|
|
273
|
+
`\`${SENDER_KEY}\` is written by the Orchestrator on every delivered event; a caller cannot send it`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The run would not take this event from this sender (ADR-0015, ADR-0029): xstate's selection holds
|
|
280
|
+
* a writing that is another party's, or — at a Gate — nothing would move. Refused BEFORE the Machine
|
|
281
|
+
* sees it. `writings` names the other party's writings, empty when nothing would move.
|
|
282
|
+
*/
|
|
283
|
+
export class NotTakenError extends Error {
|
|
284
|
+
readonly writings: readonly NotTakenWriting[];
|
|
285
|
+
constructor(message: string, writings: readonly NotTakenWriting[] = []) {
|
|
286
|
+
super(message);
|
|
287
|
+
this.writings = writings;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
145
290
|
|
|
146
291
|
/**
|
|
147
292
|
* The actor path below the run's root: every id from the root's children down to `ref` itself,
|
|
@@ -164,7 +309,7 @@ export function actorPath(ref: AnyActorRef): string[] {
|
|
|
164
309
|
* instances (two Pool children, whose ids are the items') can never meet.
|
|
165
310
|
*
|
|
166
311
|
* One derivation, two readers, so they cannot drift: `mintIid` builds every iid on it (a fresh
|
|
167
|
-
* Turn adds a random suffix, a burned conversation's successor its epoch), and the Agent
|
|
312
|
+
* Turn adds a random suffix, a burned conversation's successor its epoch), and the Agent's Actor
|
|
168
313
|
* names the conversation it burns with it.
|
|
169
314
|
*/
|
|
170
315
|
export function continuedIid(machine: AnyActorRef | undefined, runId: string, agentName: string): string {
|
|
@@ -213,11 +358,14 @@ export class RegistrationTable {
|
|
|
213
358
|
* Validate and deliver one event to an address: the shared behavior both dialect adapters
|
|
214
359
|
* call. Unknown address / unaccepted name / bad payload throw typed errors the adapters map
|
|
215
360
|
* to their wire (404 / 400, MCP tool errors). The payload is parsed by the named def's
|
|
216
|
-
* schema, so what lands in the Machine is exactly the validated shape
|
|
361
|
+
* schema, so what lands in the Machine is exactly the validated shape — plus the registration's
|
|
362
|
+
* sender, written here and nowhere else (ADR-0015). A delivery that would run another party's
|
|
363
|
+
* writing throws {@link NotTakenError}.
|
|
217
364
|
*/
|
|
218
365
|
deliver(address: string, type: string, payload: unknown): void {
|
|
219
366
|
const reg = this.byAddress.get(address);
|
|
220
367
|
if (!reg) throw new UnknownAddressError(`no live registration at "${address}"`);
|
|
368
|
+
refuseSenderField(payload);
|
|
221
369
|
const def = reg.defs.get(type);
|
|
222
370
|
if (!def) {
|
|
223
371
|
throw new EventValidationError(
|
|
@@ -228,7 +376,25 @@ export class RegistrationTable {
|
|
|
228
376
|
if (!parsed.success) {
|
|
229
377
|
throw new EventValidationError(`invalid "${type}" payload: ${parsed.error.message}`);
|
|
230
378
|
}
|
|
231
|
-
|
|
379
|
+
// `type` after the payload: a payload never names the event the Machine receives (ADR-0011).
|
|
380
|
+
const event: DeliveredEvent = {
|
|
381
|
+
...parsed.data,
|
|
382
|
+
type: def.name,
|
|
383
|
+
...(reg.sender ? { [SENDER_KEY]: reg.sender } : {}),
|
|
384
|
+
};
|
|
385
|
+
// Exact, with the real payload (ADR-0015): a delivery whose selection holds a writing that is
|
|
386
|
+
// not this sender's never reaches the Machine.
|
|
387
|
+
const others = reg.sender ? notTaken(reg, event) : [];
|
|
388
|
+
if (others.length) {
|
|
389
|
+
const owned = (w: NotTakenWriting): string =>
|
|
390
|
+
w.owners.length ? `, which is ${w.owners.map(senderText).join(" and ")}'s` : "";
|
|
391
|
+
throw new NotTakenError(
|
|
392
|
+
`"${type}" from ${senderText(reg.sender!)} would run the writing on ` +
|
|
393
|
+
`${others.map((w) => `"${w.state}"${owned(w)}`).join(" and ")}`,
|
|
394
|
+
others,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
reg.deliver(event);
|
|
232
398
|
}
|
|
233
399
|
}
|
|
234
400
|
|
|
@@ -236,6 +402,9 @@ export class RegistrationTable {
|
|
|
236
402
|
export type RunBinding = {
|
|
237
403
|
runId: string;
|
|
238
404
|
workflow: string;
|
|
405
|
+
/** The hash of the Assembly the run holds (ADR-0069): the one it started under, through
|
|
406
|
+
* restarts. Absent for a run no Assembly started. */
|
|
407
|
+
assembly?: string;
|
|
239
408
|
table: RegistrationTable;
|
|
240
409
|
/** The host's Sandbox backend, when it has a cluster (`workspace()` resolves it here —
|
|
241
410
|
* one cluster per orchestrator instance, so it is host infrastructure like the table). */
|
|
@@ -248,18 +417,30 @@ export type RunBinding = {
|
|
|
248
417
|
instanceHarness?: string;
|
|
249
418
|
/**
|
|
250
419
|
* The Harness bearer for a placement (ADR-0058): `harnessToken(signingKey, placement)`, closed
|
|
251
|
-
* over the key by the host. The Agent
|
|
420
|
+
* over the key by the host. The Agent's Actor asks it for the pod its Turn resolved to; the echo
|
|
252
421
|
* attach asks it for the Workspace's. Absent (bare unit-test bindings, the stub path), no bearer
|
|
253
422
|
* is sent — which only a Harness that checks nothing accepts.
|
|
254
423
|
*/
|
|
255
424
|
harnessBearer?: (placement: string) => string;
|
|
425
|
+
/**
|
|
426
|
+
* Where an Effector placement is, and the key its wire is signed with (ADR-0068): the Instance
|
|
427
|
+
* Effector's Service and `effectorKey(signingKey, "instance")`, closed over by the host. An http
|
|
428
|
+
* Effect asks it; absent (bare unit-test bindings, an instance with no Effector), it faults.
|
|
429
|
+
*/
|
|
430
|
+
effector?: (placement: EffectorPlacement) => EffectorTarget | undefined;
|
|
256
431
|
/**
|
|
257
432
|
* Record an Agent invocation's durable admission in the host ledger (ADR-0016): persisted
|
|
258
433
|
* beside the snapshot in the same RunBlob save, keyed by iid (globally unique, so the map is
|
|
259
434
|
* flat). Optional so a bare unit-test binding can omit it — then admissions simply are not
|
|
260
435
|
* durable.
|
|
261
436
|
*/
|
|
262
|
-
recordAdmission?: (instanceId: string, admission: AgentAdmission) => void;
|
|
437
|
+
recordAdmission?: (instanceId: string, admission: AgentAdmission, delivered?: readonly string[]) => void;
|
|
438
|
+
/**
|
|
439
|
+
* The admission ledger's READ half (ADR-0016): the last admission ledgered under this iid. A
|
|
440
|
+
* continued Turn asks it whether its conversation was ever admitted — only then can the Harness
|
|
441
|
+
* have lost it (ADR-0062).
|
|
442
|
+
*/
|
|
443
|
+
admissionOf?: (instanceId: string) => AgentAdmission | undefined;
|
|
263
444
|
/**
|
|
264
445
|
* The epoch ledger's READ half (ADR-0057): the current epoch of a continued conversation,
|
|
265
446
|
* keyed by its base Instance ID ({@link continuedIid}). The input mapper reads it AT MINT TIME,
|
|
@@ -273,9 +454,32 @@ export type RunBinding = {
|
|
|
273
454
|
* it ended, and the next `continue` on that Agent mints `<conversation>/<epoch>` — a virgin
|
|
274
455
|
* conversation. No fault leaves a conversation worth continuing (ADR-0035): a runaway either
|
|
275
456
|
* had its reroll or holds a poisoned context, and infra and no-signal mean the Harness that
|
|
276
|
-
* held it is unreachable. Persisted beside the snapshot with the admission ledger.
|
|
457
|
+
* held it is unreachable. Persisted beside the snapshot with the admission ledger. `reason` is
|
|
458
|
+
* the fault's: the host raises it as the next `continue`'s `conversation-new` notice (ADR-0062).
|
|
277
459
|
*/
|
|
278
|
-
bumpEpoch?: (conversation: string) => void;
|
|
460
|
+
bumpEpoch?: (conversation: string, reason: string) => void;
|
|
461
|
+
/**
|
|
462
|
+
* The notices' write half (ADR-0062): keep a notice for the next admission in scope — `to` is a
|
|
463
|
+
* continued conversation's base id or a Sandbox name, as the notice's `scope` says. Persisted at
|
|
464
|
+
* the write, like the ledgers. `source` names the event, so one event raised twice is one notice.
|
|
465
|
+
* A burned conversation's notice is raised by `bumpEpoch` itself, in the same save.
|
|
466
|
+
*/
|
|
467
|
+
raiseNotice?: (notice: Notice, to: string, source?: string) => void;
|
|
468
|
+
/** Reserve the pending notices in one admission's scope. They are delivered by the
|
|
469
|
+
* `recordAdmission` that ledgers it (its `delivered` ids), or released if the admission fails. */
|
|
470
|
+
takeNotices?: (scope: NoticeScope) => TakenNotices;
|
|
471
|
+
releaseNotices?: (ids: readonly string[]) => void;
|
|
472
|
+
/**
|
|
473
|
+
* The pick the Turn now at this address has outstanding, or answered and not yet read (ADR-0024):
|
|
474
|
+
* what the Agent's Actor asks when its Harness restarted under a Turn — a Turn whose pick is still
|
|
475
|
+
* out, or whose answer never reached the model, is cut, not faulted (ADR-0031). Read after the host
|
|
476
|
+
* settles its holds, so `answered` is the Rest rule's verdict now. An earlier Turn's pick on the
|
|
477
|
+
* same continued address is not this Turn's: its answer went as a notice. Absent outside a RunHost.
|
|
478
|
+
*/
|
|
479
|
+
outstandingOf?: (instanceId: string) => OutstandingPick | undefined;
|
|
480
|
+
/** The held answer reached the model by another road — a re-admission's notice — so the poll's
|
|
481
|
+
* copy is spent (ADR-0031). */
|
|
482
|
+
heldAnswerDelivered?: (instanceId: string) => void;
|
|
279
483
|
/**
|
|
280
484
|
* Surface absorbed-retry telemetry on the run feed (ADR-0016): attempts are observable, but
|
|
281
485
|
* as `{ child, attempt }` — state-key-class data, never iids (ADR-0014's line holds on the
|
|
@@ -287,7 +491,7 @@ export type RunBinding = {
|
|
|
287
491
|
* settlement pick, so the run's narrative can name what a Turn hosted elsewhere decided.
|
|
288
492
|
* Host-supplied; absent (bare unit-test bindings), turns leave no markers.
|
|
289
493
|
*/
|
|
290
|
-
marker?: (event:
|
|
494
|
+
marker?: (event: RunMarker) => void;
|
|
291
495
|
/**
|
|
292
496
|
* Attach the run-narrative echo to a Workspace's Harness (ADR-0023): the host replays the
|
|
293
497
|
* run's feed-so-far to `endpoint` — the Harness in `placement`'s pod, pushed with that
|
|
@@ -306,13 +510,63 @@ export type RunBinding = {
|
|
|
306
510
|
*/
|
|
307
511
|
hostStopping?: boolean;
|
|
308
512
|
/**
|
|
309
|
-
*
|
|
513
|
+
* Every reply since the pick that `machine`'s Agent has outstanding, or is delivering now, oldest
|
|
514
|
+
* first (ADR-0024) — what an `answer` action reads as its `chain` (ADR-0067). `seat` is the place
|
|
515
|
+
* the action runs from: an Agent's state, and then only that Agent's pick counts; or `null`, a
|
|
516
|
+
* transition below no Agent's state, and then `event` — the event whose transition runs the action
|
|
517
|
+
* — names the Agent: the sender of a pick, or the Agent whose pick started the child whose done
|
|
518
|
+
* or error event it is. Undefined when no such pick is being answered, when the event names an
|
|
519
|
+
* Agent with none, or when the event names no Agent outside a pick's own send while two Agents'
|
|
520
|
+
* states are active or more than one pick is outstanding: then the action cannot tell whose
|
|
521
|
+
* answer it shapes, and sets none.
|
|
522
|
+
*/
|
|
523
|
+
chainOf?: (machine: AnyActorRef, seat: AnswerSeat, event?: AnyEventObject) => PickAnswer[] | undefined;
|
|
524
|
+
/** Set what that pick's answer is (ADR-0067): it replaces the default, and the last call wins.
|
|
525
|
+
* Kept with the hold, so a restore answers the same. */
|
|
526
|
+
setAnswer?: (machine: AnyActorRef, seat: AnswerSeat, value: unknown, event?: AnyEventObject) => void;
|
|
527
|
+
/**
|
|
528
|
+
* The Effect ledger's READ half (ADR-0067): one Effect's row, by its id — what a restored Effect
|
|
529
|
+
* reads to deliver its output, re-attach, or settle `unknown`, and never to run twice.
|
|
530
|
+
*/
|
|
531
|
+
effectRow?: (effectId: string) => EffectRow | undefined;
|
|
532
|
+
/** Every row of the run's Effect ledger, by Effect id — what a push reads its pairs from (each
|
|
533
|
+
* push of its Workspace and Repo Slot that published, ADR-0067). */
|
|
534
|
+
effectRows?: () => Readonly<Record<string, EffectRow>>;
|
|
535
|
+
/**
|
|
536
|
+
* The Effect ledger's WRITE half (ADR-0067), in the same RunBlob as the snapshot. `"now"` (the
|
|
537
|
+
* default) saves AT THE WRITE — so an intent row is on disk, with the snapshot that holds its
|
|
538
|
+
* Effect, before the call leaves. `"with-transition"` is for a row written INSIDE a transition (an
|
|
539
|
+
* abort, D1): it is kept in memory for the save that carries that transition — a save now would
|
|
540
|
+
* read the Machine before its route — and saved after the transition if nothing else saves it.
|
|
541
|
+
* Optional: a bare binding keeps no ledger, and nothing it runs is durable.
|
|
542
|
+
*/
|
|
543
|
+
recordEffect?: (effectId: string, row: EffectRow, save?: "now" | "with-transition") => void;
|
|
544
|
+
/**
|
|
545
|
+
* iid → the abort still in flight for it (ADR-0024). The Agent's Actor fills this on the way out and
|
|
310
546
|
* waits on it before admitting, so an abort can never overtake the next turn on the same
|
|
311
547
|
* instance — flue QUEUES per instance, and an abort that lost that race would settle the new
|
|
312
548
|
* submission before it ran. Created on demand: the ordering must hold for ANY binding, not only
|
|
313
549
|
* one the host remembered to equip.
|
|
314
550
|
*/
|
|
315
551
|
pendingAborts?: Map<string, Promise<void>>;
|
|
552
|
+
/**
|
|
553
|
+
* A named Chart of the run's own Workflow, by its short name (ADR-0070, D-C): what an Open body
|
|
554
|
+
* whose input is `{ chart: { name } }` binds. The host resolves it among the Charts of the
|
|
555
|
+
* wrapper that is the run's Workflow (`<wrapper>` of `<wrapper>/<name>`, or the Workflow itself),
|
|
556
|
+
* and only for a body that wrapper invokes itself. Absent outside a RunHost: no name resolves.
|
|
557
|
+
*/
|
|
558
|
+
chartNamed?: (name: string) => ChartText | undefined;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/** A conversation's latest outstanding pick, as the Agent's Actor reads it (ADR-0024). `answered`: the Machine
|
|
562
|
+
* came back to Rest, or left the Agent's state (`turnComplete`); `fetched`: the answer reached the
|
|
563
|
+
* model. */
|
|
564
|
+
export type OutstandingPick = {
|
|
565
|
+
pick: string;
|
|
566
|
+
answered: boolean;
|
|
567
|
+
turnComplete: boolean;
|
|
568
|
+
answer?: PickAnswer;
|
|
569
|
+
fetched: boolean;
|
|
316
570
|
};
|
|
317
571
|
|
|
318
572
|
/** One absorbed-retry attempt (a no-signal nudge), as the run feed carries it. */
|
|
@@ -330,6 +584,30 @@ export type TurnMarker =
|
|
|
330
584
|
| { kind: "admission"; agent: string; endpoint: string; prompt: string }
|
|
331
585
|
| { kind: "pick"; agent: string; endpoint: string; event: string; payload?: Record<string, unknown> };
|
|
332
586
|
|
|
587
|
+
/**
|
|
588
|
+
* ADR-0064's two feed lines per wait for capacity: a Workspace's Sandbox has no node — `placing`,
|
|
589
|
+
* with the reason (`on`) and the scheduler's or the quota's own words — and then it has one —
|
|
590
|
+
* `placed`, `after` milliseconds since `since`. A changed reason adds no line: `RunStatus.waiting`
|
|
591
|
+
* carries the latest. `child` is the Workspace's actor path below the run root, its ids joined by
|
|
592
|
+
* `/` (`""` for a run whose root IS the Workspace). Instance-token class like the Turn markers: the
|
|
593
|
+
* message can name nodes and taints, so these ride the per-run feed alone.
|
|
594
|
+
*/
|
|
595
|
+
export type PlacingMarker =
|
|
596
|
+
| { kind: "placing"; child: string; on: "node" | "quota"; message: string; since: string }
|
|
597
|
+
| { kind: "placed"; child: string; since: string; after: number };
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* A child below an Agent's state failed while a pick of that Agent was outstanding (ADR-0024): the
|
|
601
|
+
* pick's answer carries no reply for it — the Agent is not told — so the operator reads it here: the
|
|
602
|
+
* Agent, its pick, the child (its slot, or its invoke id) and the error's message. Instance-token
|
|
603
|
+
* class like the Turn markers: an error message is mechanism text, as `fault` is.
|
|
604
|
+
*/
|
|
605
|
+
export type ChildErrorMarker = { kind: "child-error"; agent: string; pick: string; child: string; error: string };
|
|
606
|
+
|
|
607
|
+
/** Every marker a run's mechanism puts on its per-run feed — an Effect's lines among them
|
|
608
|
+
* (ADR-0067): its start, its stream, its output, its abort. */
|
|
609
|
+
export type RunMarker = TurnMarker | PlacingMarker | ChildErrorMarker | EffectMarker;
|
|
610
|
+
|
|
333
611
|
const bindings = new WeakMap<AnyActorSystem, RunBinding>();
|
|
334
612
|
|
|
335
613
|
/** Host-side: bind a run's actor system to its identity, before the actor starts. */
|
|
@@ -349,6 +627,12 @@ export function boundEpoch(system: AnyActorSystem, conversation: string): number
|
|
|
349
627
|
return bindings.get(system)?.epochOf?.(conversation) ?? 0;
|
|
350
628
|
}
|
|
351
629
|
|
|
630
|
+
/** The bound run, or undefined outside a jr2 host — the SOFT read an Effect uses: with no host it
|
|
631
|
+
* keeps no ledger and writes no feed, and still runs. */
|
|
632
|
+
export function boundRun(system: AnyActorSystem): RunBinding | undefined {
|
|
633
|
+
return bindings.get(system);
|
|
634
|
+
}
|
|
635
|
+
|
|
352
636
|
/** Actor-side: resolve the run this actor tree belongs to. Throws outside a jr2 host. */
|
|
353
637
|
export function runBindingOf(system: AnyActorSystem): RunBinding {
|
|
354
638
|
const binding = bindings.get(system);
|