@jr2/orchestrator 0.2.1 → 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 -5
- package/src/actor.ts +507 -54
- package/src/agent.ts +2 -2
- 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 +233 -22
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +313 -0
- package/src/custodian.ts +951 -0
- 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 +247 -25
- package/src/held-secrets.ts +643 -0
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +981 -0
- package/src/http.ts +514 -119
- package/src/images.ts +58 -31
- package/src/index.ts +49 -4
- package/src/instance.ts +445 -59
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +88 -6
- package/src/model-keys.ts +92 -0
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +89 -7
- 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 +61 -86
- package/src/repos.ts +63 -74
- package/src/rest.ts +133 -0
- package/src/run-host.ts +1841 -105
- 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 +285 -89
- 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 +44 -12
- package/src/vocabulary.ts +66 -1
- package/src/wire.ts +156 -2
- package/src/workspace.ts +529 -194
- package/src/sandbox-kubectl.ts +0 -1144
package/src/actor.ts
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
// 1. REGISTERS the invocation's event surface: `tools` names are resolved against the
|
|
5
5
|
// INVOKING MACHINE's vocabulary (per-Machine scoping — an unlisted name fails at invoke time)
|
|
6
6
|
// and registered in the host's table under the instance's agent address, with a deliver
|
|
7
|
-
// closure over THIS invocation's `sendBack`. The Agent's domain tool calls arrive
|
|
8
|
-
//
|
|
7
|
+
// closure over THIS invocation's `sendBack`. The Agent's domain tool calls arrive through its
|
|
8
|
+
// pod's Custodian (`/agents/<iid>/events` — ADR-0013), are validated by the table, and land on the
|
|
9
9
|
// state that invoked the agent — at any nesting depth, no routing, no `instanceId` on
|
|
10
10
|
// domain events (the closure IS the provenance). `/agents/<iid>/surface` serves exactly
|
|
11
|
-
// this registration,
|
|
12
|
-
//
|
|
13
|
-
// Harness re-
|
|
11
|
+
// this registration: the Agent's Menu, fixed for the conversation, and the Allowed picks,
|
|
12
|
+
// state-scoped by lifecycle (ADR-0029 — with no `list_changed` needed, per ADR-0013: the
|
|
13
|
+
// Harness re-reads per Submission).
|
|
14
14
|
//
|
|
15
15
|
// 2. ADMITS the run over the Harness at `input.endpoint` — the port is constructed
|
|
16
16
|
// per-invocation from serializable input (ADR-0007/0011 doctrine), and a dev stub is just
|
|
@@ -51,8 +51,13 @@ import {
|
|
|
51
51
|
type WorkspaceAccess,
|
|
52
52
|
} from "./agent.ts";
|
|
53
53
|
import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
|
|
54
|
+
import type { HarnessRestarts, MemoryKill } from "./workspace.ts";
|
|
54
55
|
import { INSTANCE_HARNESS_SERVICE } from "./names.ts";
|
|
55
|
-
import {
|
|
56
|
+
import { SENDER_KEY } from "@jr2/agent-protocol";
|
|
57
|
+
import { agentAddress, continuedIid, allowedPicks, resolveAccepts, runBindingOf } from "./registration.ts";
|
|
58
|
+
import type { NoticeScope } from "./notices.ts";
|
|
59
|
+
import { restOf } from "./rest.ts";
|
|
60
|
+
import { SUBMISSION_HARNESS_RESTARTED, SUBMISSION_RUNAWAY, type Notice } from "./wire.ts";
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
63
|
* One admitted Submission — the durable re-attach handle (ADR-0016). The wire fields are
|
|
@@ -75,6 +80,21 @@ export type AgentAdmission = {
|
|
|
75
80
|
* aborts the LIVE conversation, never the dead original the ledger is keyed by.
|
|
76
81
|
*/
|
|
77
82
|
instanceId?: string;
|
|
83
|
+
/**
|
|
84
|
+
* The Harness container's restart count when this admission was answered (ADR-0021) — stamped by
|
|
85
|
+
* the ACTOR, for a Turn in a Sandbox Workspace whose watch had spoken. A count above it means
|
|
86
|
+
* the process holding the conversation died; a restore holds the re-attached Turn to the same
|
|
87
|
+
* mark.
|
|
88
|
+
*/
|
|
89
|
+
harnessRestarts?: number;
|
|
90
|
+
/**
|
|
91
|
+
* The Agent and the Harness base URL this admission was answered by — stamped by the ACTOR at
|
|
92
|
+
* ledger time, like `instanceId`. Together with `instanceId` they name the conversation on its
|
|
93
|
+
* Harness, which is what the live set is built from (ADR-0031): the host states the Instance
|
|
94
|
+
* Harness's live conversations from these ledger entries alone.
|
|
95
|
+
*/
|
|
96
|
+
agent?: string;
|
|
97
|
+
endpoint?: string;
|
|
78
98
|
};
|
|
79
99
|
|
|
80
100
|
/**
|
|
@@ -131,7 +151,8 @@ export type AgentTurnInput = {
|
|
|
131
151
|
* share a conversation (the name is in the id), and two Pool children never collide (each
|
|
132
152
|
* worker is its own Machine instance). A conversation jr2 has faulted is never continued: the
|
|
133
153
|
* next `continue` lands on a virgin one (ADR-0035, ADR-0057) — and re-briefing that Turn is the
|
|
134
|
-
* author's, because jr2 cannot write that prompt.
|
|
154
|
+
* author's, because jr2 cannot write that prompt. A Harness restart is the exception: it cut the
|
|
155
|
+
* Turn, and the rebuilt Harness holds the conversation (ADR-0031).
|
|
135
156
|
*/
|
|
136
157
|
continue?: true;
|
|
137
158
|
};
|
|
@@ -150,7 +171,7 @@ export type AgentTurnInput = {
|
|
|
150
171
|
export type AgentTurnPlacement = {
|
|
151
172
|
/** The Harness base URL this Turn is admitted over. */
|
|
152
173
|
endpoint?: string;
|
|
153
|
-
/** The Sandbox whose
|
|
174
|
+
/** The Sandbox whose token (held by that pod's Custodian) may deliver this Turn's picks (ADR-0013). */
|
|
154
175
|
sandbox?: string;
|
|
155
176
|
};
|
|
156
177
|
|
|
@@ -202,8 +223,16 @@ export type AgentRunInput = {
|
|
|
202
223
|
* fresh invocation, which already mints its own conversation.
|
|
203
224
|
*/
|
|
204
225
|
continuation?: boolean;
|
|
205
|
-
/** Event names (from the invoking Machine's vocabulary) this invocation accepts
|
|
226
|
+
/** Event names (from the invoking Machine's vocabulary) this invocation accepts: the invoking
|
|
227
|
+
* state's derived set, which delivery validates against and the Allowed picks narrow. */
|
|
206
228
|
tools: readonly string[];
|
|
229
|
+
/**
|
|
230
|
+
* The Agent's MENU (ADR-0029): the union of `tools` over every state of this Machine that invokes
|
|
231
|
+
* an Agent of this name, in a fixed order — what the surface serves as the tools block, the same
|
|
232
|
+
* for every Turn of the conversation. `jr2Setup` always sets it; absent (a plain `setup()`
|
|
233
|
+
* machine passing finalized input), `tools` is the Menu.
|
|
234
|
+
*/
|
|
235
|
+
menu?: readonly string[];
|
|
207
236
|
};
|
|
208
237
|
|
|
209
238
|
/** Telemetry sent up when the run is out of options: the Submission settled failed/aborted
|
|
@@ -240,7 +269,14 @@ export interface AgentRunPort {
|
|
|
240
269
|
* Follow an admitted submission until it settles. Resolving means the submission completed;
|
|
241
270
|
* rejecting means it settled failed/aborted (or the conversation is gone).
|
|
242
271
|
*/
|
|
243
|
-
settle(admission: AgentAdmission, opts?:
|
|
272
|
+
settle(admission: AgentAdmission, opts?: AgentSettleOptions): Promise<void>;
|
|
273
|
+
/**
|
|
274
|
+
* Does the Harness still hold this conversation (ADR-0062)? Asked before a continued Turn whose
|
|
275
|
+
* conversation was admitted before: a Harness that restarted between the Turns lost it with no
|
|
276
|
+
* fault to say so, and the Turn is told its conversation is new. Answers `true` whenever it
|
|
277
|
+
* cannot tell — unknown is never loss.
|
|
278
|
+
*/
|
|
279
|
+
holds(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<boolean>;
|
|
244
280
|
/**
|
|
245
281
|
* End the instance's in-flight (and queued) work — the turn is over (ADR-0024). Resolving means
|
|
246
282
|
* the intent is RECORDED, not that the submission has settled; jr2 never observes that outcome,
|
|
@@ -255,7 +291,25 @@ export interface AgentRunPort {
|
|
|
255
291
|
* plus the local abandon signal. Not part of {@link AgentRunInput} on purpose — that shape is
|
|
256
292
|
* PERSISTED as the child's input, and a definition frozen into a snapshot would outlive the
|
|
257
293
|
* Machine edit that changed it. */
|
|
258
|
-
export type AgentAdmitOptions = {
|
|
294
|
+
export type AgentAdmitOptions = {
|
|
295
|
+
definition: AgentDefinition;
|
|
296
|
+
signal?: AbortSignal;
|
|
297
|
+
/** The pending notices in this admission's scope (ADR-0062), for the Briefing's Turn part. Not
|
|
298
|
+
* on the persisted input either: they are the Orchestrator's, and delivered once. */
|
|
299
|
+
notices?: Notice[];
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** A memory kill by the Harness's own guard (ADR-0061, layer 4), as the updates stream reports it:
|
|
303
|
+
* the guard's `peak` and `limit`, and `source` — where on which conversation's stream the event
|
|
304
|
+
* sits, so a replayed read of it is the same kill. */
|
|
305
|
+
export type GuardKill = { peak: string; limit: string; source: string };
|
|
306
|
+
|
|
307
|
+
/** What following a settlement may be told beside the settlement itself. */
|
|
308
|
+
export type AgentSettleOptions = {
|
|
309
|
+
signal?: AbortSignal;
|
|
310
|
+
/** The Harness's guard killed the Agent's processes during this Submission (ADR-0061). */
|
|
311
|
+
onMemoryLimit?: (kill: GuardKill) => void;
|
|
312
|
+
};
|
|
259
313
|
|
|
260
314
|
/** Build a port for one invocation from its serializable input (ADR-0011 static-import doctrine),
|
|
261
315
|
* plus the Harness bearer for the placement that input resolved to (ADR-0058) — absent where no
|
|
@@ -285,26 +339,93 @@ export type AgentRunOptions = {
|
|
|
285
339
|
|
|
286
340
|
/**
|
|
287
341
|
* The runaway settlement class (ADR-0035), read STRUCTURALLY off a settle rejection: the wire
|
|
288
|
-
* client's `SettlementFault` carries the Settlement, but this module
|
|
289
|
-
* so the
|
|
290
|
-
*
|
|
291
|
-
*
|
|
342
|
+
* client's `SettlementFault` carries the Settlement, but this module never loads the wire client
|
|
343
|
+
* (see header), so the shape is duck-typed. The literal is the wire's own `SUBMISSION_RUNAWAY` —
|
|
344
|
+
* `./wire.ts` is shapes and constants, nothing a test load path pays for — and wire-types.test.ts
|
|
345
|
+
* holds it to the Harness's. A lost conversation (404) carries no settlement, so it stays
|
|
346
|
+
* terminal like every other class.
|
|
292
347
|
*/
|
|
293
348
|
function runawayReason(err: unknown): string | undefined {
|
|
294
349
|
if (typeof err !== "object" || err === null) return undefined;
|
|
295
350
|
const error = (err as { settlement?: { error?: { type?: string; message?: string } } }).settlement?.error;
|
|
296
|
-
return error?.type ===
|
|
351
|
+
return error?.type === SUBMISSION_RUNAWAY ? (error.message ?? SUBMISSION_RUNAWAY) : undefined;
|
|
297
352
|
}
|
|
298
353
|
|
|
299
|
-
/** The
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
354
|
+
/** The Harness's fixed prefix for a provider that refused the load (ADR-0064), restated like the
|
|
355
|
+
* runaway literal: `PROVIDER_LIMIT` in `@jr2/harness`. */
|
|
356
|
+
const PROVIDER_LIMIT = "provider limit";
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* A provider limit (ADR-0064), read STRUCTURALLY like {@link runawayReason}: the Settlement's own
|
|
360
|
+
* message, unwrapped, so the fault reason STARTS with the fixed prefix — as a memory kill's does
|
|
361
|
+
* (ADR-0061) — and the Machine and `jr2 status` can key on it.
|
|
362
|
+
*/
|
|
363
|
+
function providerLimitReason(err: unknown): string | undefined {
|
|
364
|
+
if (typeof err !== "object" || err === null) return undefined;
|
|
365
|
+
const message = (err as { settlement?: { error?: { message?: unknown } } }).settlement?.error?.message;
|
|
366
|
+
return typeof message === "string" && message.startsWith(PROVIDER_LIMIT) ? message : undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The kubelet's reason for a container the kernel killed at its memory limit (ADR-0061). */
|
|
370
|
+
const OOM_KILLED = "OOMKilled";
|
|
371
|
+
|
|
372
|
+
/** Why a Turn failed when its Harness restarted under it (ADR-0021), in the kubelet's words. */
|
|
373
|
+
function harnessRestartReason(seen: HarnessRestarts): string {
|
|
374
|
+
const last = seen.lastTerminated;
|
|
375
|
+
return `Turn lost (Harness restarted: ${last?.reason ?? "unknown"}, exit ${last?.exitCode ?? "?"})`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* A Submission the Harness settled `failed` because it restarted under it (ADR-0031): the rebuilt
|
|
380
|
+
* stream says so, so the Orchestrator reads a Settlement, not a 404. Read STRUCTURALLY off the
|
|
381
|
+
* Settlement's typed error, like {@link runawayReason} — the wire's `SUBMISSION_HARNESS_RESTARTED`,
|
|
382
|
+
* held to the Harness's by wire-types.test.ts — and the message is only what the fault quotes.
|
|
383
|
+
* It can reach the actor BEFORE the watch's restart count does, and it is the only restart signal
|
|
384
|
+
* an Instance Harness gives — the watch does not see that pod.
|
|
385
|
+
*/
|
|
386
|
+
function settledByRestart(err: unknown): string | undefined {
|
|
387
|
+
if (typeof err !== "object" || err === null) return undefined;
|
|
388
|
+
const settlement = (err as { settlement?: { outcome?: unknown; error?: { type?: unknown; message?: unknown } } })
|
|
389
|
+
.settlement;
|
|
390
|
+
if (settlement?.outcome !== "failed" || settlement.error?.type !== SUBMISSION_HARNESS_RESTARTED) return undefined;
|
|
391
|
+
const message = settlement.error.message;
|
|
392
|
+
return typeof message === "string" ? message : "Harness restarted";
|
|
306
393
|
}
|
|
307
394
|
|
|
395
|
+
const errorMessage = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* A lost conversation (ADR-0027), read STRUCTURALLY like {@link runawayReason}: the wire client's
|
|
399
|
+
* `SettlementFault` marks a 404 `lost`, and this module is wire-free. The one fault class a
|
|
400
|
+
* memory kill can hide behind (ADR-0061).
|
|
401
|
+
*/
|
|
402
|
+
function isLostConversation(err: unknown): boolean {
|
|
403
|
+
return typeof err === "object" && err !== null && (err as { lost?: unknown }).lost === true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The nudge (ADR-0006, absorbed here by ADR-0016). It names no picks: the Harness writes the Turn
|
|
408
|
+
* part after it, stating the Allowed picks in the model's own tool names and narrowed by the guards
|
|
409
|
+
* (ADR-0029, ADR-0062) — a list here would be a second one, in other names, that a closed guard
|
|
410
|
+
* makes wrong. Its first sentence stays true after a pick that left the Turn open, and for the
|
|
411
|
+
* wake: the ask-again rule asks whenever picks are allowed after a settle, picked or not, at once
|
|
412
|
+
* or when they open.
|
|
413
|
+
*/
|
|
414
|
+
const NUDGE_PROMPT =
|
|
415
|
+
`Your previous turn ended, and the workflow is still waiting for your pick. ` +
|
|
416
|
+
`You MUST end your turn by calling exactly one of the workflow tools allowed below. ` +
|
|
417
|
+
`Pick the one that matches the true state of your work and call it now.`;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The re-admission after a Harness restart cut the Turn while the workflow was busy (ADR-0031). The
|
|
421
|
+
* Harness writes a cut call's history result itself ("its result comes in the next message") and the
|
|
422
|
+
* answer rides this admission as a `pick-answer` notice in the Briefing's Turn part, with the Allowed
|
|
423
|
+
* picks after it; this prompt says only what happened to the Turn.
|
|
424
|
+
*/
|
|
425
|
+
const READMIT_PROMPT =
|
|
426
|
+
`Your previous turn was cut: the process that ran you restarted while the workflow was busy. ` +
|
|
427
|
+
`The workflow did not stop. Continue your task from here.`;
|
|
428
|
+
|
|
308
429
|
/**
|
|
309
430
|
* Where this Turn works — the Frame's other half (ADR-0057), resolved before admission.
|
|
310
431
|
*
|
|
@@ -435,7 +556,7 @@ export function agentActorWith(
|
|
|
435
556
|
}
|
|
436
557
|
endpoint = binding.instanceHarness;
|
|
437
558
|
// The registration records the PLACEMENT's name as its delivery scope — ADR-0013's
|
|
438
|
-
// doctrine, extended to the second placement: the Instance Harness
|
|
559
|
+
// doctrine, extended to the second placement: the Instance Harness's Custodian holds a token
|
|
439
560
|
// signed for this name (deploy.ts), so it may speak for the Turns hosted there and for no
|
|
440
561
|
// Workspace's. The Instance token still may (it is the operator, tokens.ts).
|
|
441
562
|
sandbox = INSTANCE_HARNESS_SERVICE;
|
|
@@ -460,8 +581,10 @@ export function agentActorWith(
|
|
|
460
581
|
// Register this invocation's event surface (throws on a name outside the INVOKING MACHINE's
|
|
461
582
|
// vocabulary — ADR-0011's invoke-time check, scoped to `self._parent.logic` because names are
|
|
462
583
|
// per-Machine — which errors the run loudly at the invoking state).
|
|
463
|
-
// `signaled
|
|
464
|
-
//
|
|
584
|
+
// `signaled`: a Menu pick was delivered in this invocation. It closes the runaway REROLL gate
|
|
585
|
+
// (below): replaying the identical prompt would deliver it again. It does not decide the nudge
|
|
586
|
+
// — the ask-again rule does, from the Allowed picks after the settle, because a delivered pick
|
|
587
|
+
// may leave the Turn open (a mode change, a pick with no target). ONE invocation can hold
|
|
465
588
|
// more than one surface: a runaway reroll (ADR-0035) is a fresh conversation whose menu
|
|
466
589
|
// dials `/mcp/<derived iid>`, so its address must be live too — same defs, same deliver,
|
|
467
590
|
// because whichever conversation answers, it answers THIS invocation.
|
|
@@ -473,7 +596,17 @@ export function agentActorWith(
|
|
|
473
596
|
// already ended.
|
|
474
597
|
let currentIid = input.attach?.instanceId ?? instanceId;
|
|
475
598
|
const defs = resolveAccepts(self, input.tools);
|
|
599
|
+
// The Menu (ADR-0029): resolved against the same vocabulary, and served as the tools block.
|
|
600
|
+
const menu = resolveAccepts(self, input.menu ?? input.tools);
|
|
476
601
|
const disposers: Array<() => void> = [];
|
|
602
|
+
// Who this Turn's picks are from (ADR-0015): the slot, written on every event it delivers; and
|
|
603
|
+
// the invoke id that finds its state in the Machine. Both static, so a restore recomputes them.
|
|
604
|
+
const seat = { sender: { party: "agent", id: input.agentName } as const, invokeId: self.id };
|
|
605
|
+
// This Agent's continued conversation in this Machine (ADR-0057) — the base id the epoch ledger
|
|
606
|
+
// burns. This Turn is PART of it only when it continues it: a fresh Turn is new by definition.
|
|
607
|
+
const conversation = continuedIid(self._parent, binding.runId, input.agentName);
|
|
608
|
+
// The iids whose surface the Harness has read (see `carried` below).
|
|
609
|
+
const served = new Set<string>();
|
|
477
610
|
const registerSurface = (iid: string) =>
|
|
478
611
|
disposers.push(
|
|
479
612
|
binding.table.register({
|
|
@@ -482,13 +615,15 @@ export function agentActorWith(
|
|
|
482
615
|
kind: "agent",
|
|
483
616
|
id: iid,
|
|
484
617
|
defs,
|
|
618
|
+
menu,
|
|
485
619
|
sandbox,
|
|
486
620
|
deliver: (event) => {
|
|
487
621
|
signaled = true;
|
|
488
622
|
// The settlement-pick marker (ADR-0023), BEFORE the delivery moves the Machine: the pick
|
|
489
623
|
// must land on the feed ahead of the status delta it causes, or the narrative reads
|
|
490
|
-
// effect-then-cause.
|
|
491
|
-
|
|
624
|
+
// effect-then-cause. The sender is the Orchestrator's field, not the pick's payload: the
|
|
625
|
+
// printed conversation shows what the Agent picked, nothing more.
|
|
626
|
+
const { type, [SENDER_KEY]: _sender, ...payload } = event;
|
|
492
627
|
binding.marker?.({
|
|
493
628
|
kind: "pick",
|
|
494
629
|
agent: input.agentName,
|
|
@@ -498,27 +633,145 @@ export function agentActorWith(
|
|
|
498
633
|
});
|
|
499
634
|
sendBack(event);
|
|
500
635
|
},
|
|
636
|
+
// The Harness read this Turn's surface: it has the Turn in hand, and prompts it.
|
|
637
|
+
served: () => served.add(iid),
|
|
501
638
|
// The state that invoked us — the machine the menu derived from, so the only one whose
|
|
502
639
|
// guards can say whether a pick would move anything (ADR-0029). Same `_parent` the ambient
|
|
503
640
|
// walk above uses; structural, so a sibling's snapshot is unreachable.
|
|
504
641
|
invoker: self._parent,
|
|
642
|
+
...seat,
|
|
643
|
+
// Where a pick's answer goes when its route leaves the Agent's state (ADR-0024): the
|
|
644
|
+
// conversation this Turn continues, and none for a fresh Turn — no later Turn heard it.
|
|
645
|
+
...(input.continuation ? { conversation } : {}),
|
|
505
646
|
}),
|
|
506
647
|
);
|
|
507
648
|
registerSurface(currentIid);
|
|
508
649
|
|
|
509
650
|
// The Harness bearer (ADR-0058) is derived from the SAME name the registration records as its
|
|
510
|
-
// delivery scope: the pod hosting this Turn. One name, both directions — the
|
|
651
|
+
// delivery scope: the pod hosting this Turn. One name, both directions — the Custodian there may
|
|
511
652
|
// speak for these picks, and only this bearer may drive the conversation there.
|
|
512
653
|
const client = portFactory(endpoint, sandbox === undefined ? undefined : binding.harnessBearer?.(sandbox));
|
|
513
|
-
|
|
654
|
+
// When this Turn started: a memory kill that ended the Harness before it is not this Turn's
|
|
655
|
+
// (ADR-0061). Undefined from a backend that cannot see the container — and never throws:
|
|
656
|
+
// naming the fault is a courtesy, the fault itself lands either way.
|
|
657
|
+
const turnStarted = new Date();
|
|
658
|
+
const memoryFault = (name: string): Promise<MemoryKill | undefined> =>
|
|
659
|
+
binding.sandbox?.memoryFault(name, turnStarted).catch(() => undefined) ?? Promise.resolve(undefined);
|
|
660
|
+
// Replaced after a Harness restart that cut the Turn while its state was busy: the cut aborted it.
|
|
661
|
+
let controller = new AbortController();
|
|
662
|
+
|
|
514
663
|
// Shared per run, created on demand so the ordering guarantee holds for any binding.
|
|
515
664
|
const pendingAborts = (binding.pendingAborts ??= new Map<string, Promise<void>>());
|
|
516
665
|
let stopped = false;
|
|
666
|
+
|
|
667
|
+
// A Harness restart ends its Turns (ADR-0021): the Submission it held is cut (ADR-0031 keeps
|
|
668
|
+
// the conversation, not the Turn), and a Harness that crash-loops only ever refuses
|
|
669
|
+
// connections, so `wait` would reconnect forever. The watch sees the restart count pass its value at this Turn's
|
|
670
|
+
// admission, and the Turn fails at once — not its Workspace: the pod and `/work` survive.
|
|
671
|
+
// Sandbox Workspaces only: the Instance Harness is a StatefulSet the watch does not see, and
|
|
672
|
+
// its rebuilt stream settles the Turn `failed` instead (ADR-0031, `settledByRestart`).
|
|
673
|
+
let heardRestarts: HarnessRestarts | undefined;
|
|
674
|
+
let restartMark = input.attach?.harnessRestarts;
|
|
675
|
+
let restarted: HarnessRestarts | undefined;
|
|
676
|
+
// Whether a Harness has answered an admission of this Turn — before that there is nothing to
|
|
677
|
+
// lose, and nothing to hold a restart count against.
|
|
678
|
+
let admitted = input.attach !== undefined;
|
|
679
|
+
if (workspace !== "none" && sandbox !== undefined && binding.sandbox !== undefined) {
|
|
680
|
+
disposers.push(
|
|
681
|
+
binding.sandbox.harnessRestarts(sandbox, (seen) => {
|
|
682
|
+
heardRestarts = seen;
|
|
683
|
+
if (stopped || restarted !== undefined || !admitted) return;
|
|
684
|
+
if (restartMark === undefined) {
|
|
685
|
+
restartMark = seen.restartCount;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (seen.restartCount > restartMark) {
|
|
689
|
+
restarted = seen;
|
|
690
|
+
controller.abort(new Error("the Harness restarted"));
|
|
691
|
+
}
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
517
695
|
// Ledgered under the ORIGINAL iid — the persisted input's key, like a nudge's — with the
|
|
518
696
|
// LIVE conversation stamped on the record, so a restore settle-follows the live submission
|
|
519
|
-
// AND re-addresses it (see `currentIid` above).
|
|
520
|
-
|
|
521
|
-
|
|
697
|
+
// AND re-addresses it (see `currentIid` above). The notices the admission carried are
|
|
698
|
+
// delivered by this same write (ADR-0062).
|
|
699
|
+
const ledger = (admission: AgentAdmission, delivered: readonly string[]) =>
|
|
700
|
+
binding.recordAdmission?.(
|
|
701
|
+
instanceId,
|
|
702
|
+
{ ...admission, instanceId: currentIid, agent: input.agentName, endpoint },
|
|
703
|
+
delivered,
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
// Where this Turn hears notices (ADR-0062). Its continued conversation's — the base id the
|
|
707
|
+
// epoch ledger burns — only when it CONTINUES one: a fresh Turn is new by definition, and the
|
|
708
|
+
// notice waits for the `continue` whose context is gone. Its Workspace's only when it works in
|
|
709
|
+
// one: a Menu-only Agent hears conversation notices alone.
|
|
710
|
+
const noticeWorkspace = workspace !== "none" ? sandbox : undefined;
|
|
711
|
+
const noticeScope: NoticeScope = {
|
|
712
|
+
...(input.continuation ? { conversation } : {}),
|
|
713
|
+
...(noticeWorkspace !== undefined ? { workspace: noticeWorkspace } : {}),
|
|
714
|
+
};
|
|
715
|
+
const memoryLimit = (limit: string, peak?: string): Notice => ({
|
|
716
|
+
kind: "memory-limit",
|
|
717
|
+
scope: "workspace",
|
|
718
|
+
agent: input.agentName,
|
|
719
|
+
...(peak === undefined ? {} : { peak }),
|
|
720
|
+
limit,
|
|
721
|
+
});
|
|
722
|
+
// The Harness's guard killed this Workspace's processes during the Turn (ADR-0061, layer 4):
|
|
723
|
+
// the Agent heard it in its `bash` answer; the next Turn in the Workspace hears it here.
|
|
724
|
+
const onMemoryLimit = (kill: GuardKill): void => {
|
|
725
|
+
if (noticeWorkspace === undefined) return;
|
|
726
|
+
binding.raiseNotice?.(memoryLimit(kill.limit, kill.peak), noticeWorkspace, kill.source);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// The Turn's admission and a reroll's carry the pending notices in scope — a reroll's
|
|
730
|
+
// conversation is fresh, and has heard nothing. A nudge's does not: it re-prompts the Turn
|
|
731
|
+
// within itself (ADR-0016), whose Agent read a guard kill in its own `bash` answer, and a notice
|
|
732
|
+
// spent there never reaches the next Turn in the Workspace it is for (ADR-0062). Reserved
|
|
733
|
+
// first, so no concurrent admission carries them too; delivered by the ledger write; released
|
|
734
|
+
// when the admission fails, for the next one to carry.
|
|
735
|
+
//
|
|
736
|
+
// What the last carrying admission took, and under which iid: delivered is not HEARD. If this
|
|
737
|
+
// actor ends the Turn before the Harness reads that iid's surface, the Harness finds the Turn
|
|
738
|
+
// over and settles it with no model asked (ADR-0026) — so `abandon` raises them again.
|
|
739
|
+
let carried: { iid: string; notices: Notice[] } | undefined;
|
|
740
|
+
const admitTurn = async (turn: AgentRunInput, opts: { notices: boolean; extra?: Notice[] } = { notices: true }) => {
|
|
741
|
+
const taken = (opts.notices ? binding.takeNotices?.(noticeScope) : undefined) ?? { ids: [], notices: [] };
|
|
742
|
+
// `extra`: a notice this admission carries straight from the actor, never kept — the answer a
|
|
743
|
+
// cut pick never got (ADR-0031), whose one road to the model is this admission.
|
|
744
|
+
const notices = [...taken.notices, ...(opts.extra ?? [])];
|
|
745
|
+
let admission: AgentAdmission;
|
|
746
|
+
try {
|
|
747
|
+
admission = await client.admit(turn, {
|
|
748
|
+
definition,
|
|
749
|
+
signal: controller.signal,
|
|
750
|
+
...(notices.length ? { notices } : {}),
|
|
751
|
+
});
|
|
752
|
+
} catch (err) {
|
|
753
|
+
binding.releaseNotices?.(taken.ids);
|
|
754
|
+
throw err;
|
|
755
|
+
}
|
|
756
|
+
// The mark is taken once the Harness has ANSWERED: a restart before the answer took the
|
|
757
|
+
// admission with it, and the conversation's 404 says so; a restart after it is this Turn's.
|
|
758
|
+
// Unheard yet (the watch has not listed), the first count heard is the mark.
|
|
759
|
+
admitted = true;
|
|
760
|
+
restartMark = heardRestarts?.restartCount;
|
|
761
|
+
if (restartMark !== undefined) admission = { ...admission, harnessRestarts: restartMark };
|
|
762
|
+
ledger(admission, taken.ids);
|
|
763
|
+
if (taken.notices.length) carried = { iid: turn.instanceId, notices: taken.notices };
|
|
764
|
+
return admission;
|
|
765
|
+
};
|
|
766
|
+
// The notices an unheard admission carried, pending again for the next Turn in their scope.
|
|
767
|
+
const reraiseUnheard = (): void => {
|
|
768
|
+
if (carried === undefined || served.has(carried.iid)) return;
|
|
769
|
+
for (const notice of carried.notices) {
|
|
770
|
+
const to = notice.scope === "conversation" ? conversation : noticeWorkspace;
|
|
771
|
+
if (to !== undefined) binding.raiseNotice?.(notice, to);
|
|
772
|
+
}
|
|
773
|
+
carried = undefined;
|
|
774
|
+
};
|
|
522
775
|
|
|
523
776
|
const abandon = () => {
|
|
524
777
|
if (stopped) return;
|
|
@@ -531,6 +784,7 @@ export function agentActorWith(
|
|
|
531
784
|
// Pool cancelling a child. The one exception is the host stopping the run for its own
|
|
532
785
|
// reasons, which ADR-0007's restore re-attaches to.
|
|
533
786
|
if (binding.hostStopping) return;
|
|
787
|
+
reraiseUnheard();
|
|
534
788
|
// Fire-and-forget, and unreportable BY CONSTRUCTION: this actor is stopped, so there is no
|
|
535
789
|
// `agent.fault` left to raise. An orphan that survives a failed abort 404s on every tool
|
|
536
790
|
// call and settles on its own.
|
|
@@ -542,6 +796,125 @@ export function agentActorWith(
|
|
|
542
796
|
});
|
|
543
797
|
};
|
|
544
798
|
|
|
799
|
+
// The Allowed picks NOW (ADR-0029), read as the surface reads them: one list for the surface,
|
|
800
|
+
// the receipts, the nudge and the wake.
|
|
801
|
+
const allowedNow = (): string[] => allowedPicks({ defs, menu, invoker: self._parent, ...seat });
|
|
802
|
+
// Resolves with the Allowed picks the first time they are not empty — read a microtask after
|
|
803
|
+
// every snapshot of the invoking Machine, so a Gate's answer, an outside event, a settled child
|
|
804
|
+
// or a timer each count — or with `[]` once this actor stops. Never inside the subscriber: a
|
|
805
|
+
// snapshot between macrosteps (a state that sends to itself) is not Rest until the mailbox
|
|
806
|
+
// drains (rest.ts), and a wake with an empty list spends the nudge's budget for nothing. Its own
|
|
807
|
+
// disposer, so `abandon` ends the wait.
|
|
808
|
+
const picksOpen = (): Promise<string[]> =>
|
|
809
|
+
new Promise((resolve) => {
|
|
810
|
+
let done = false;
|
|
811
|
+
let sub: { unsubscribe(): void } | undefined;
|
|
812
|
+
const finish = (allowed: string[]): void => {
|
|
813
|
+
if (done) return;
|
|
814
|
+
done = true;
|
|
815
|
+
const at = disposers.indexOf(dispose);
|
|
816
|
+
if (at >= 0) disposers.splice(at, 1);
|
|
817
|
+
sub?.unsubscribe();
|
|
818
|
+
resolve(allowed);
|
|
819
|
+
};
|
|
820
|
+
const dispose = (): void => finish([]);
|
|
821
|
+
const check = (): void => {
|
|
822
|
+
if (stopped) return finish([]);
|
|
823
|
+
const allowed = allowedNow();
|
|
824
|
+
if (allowed.length > 0) finish(allowed);
|
|
825
|
+
};
|
|
826
|
+
disposers.push(dispose);
|
|
827
|
+
check();
|
|
828
|
+
if (done) return;
|
|
829
|
+
sub = self._parent?.subscribe({ next: () => queueMicrotask(check), error: dispose, complete: dispose });
|
|
830
|
+
if (sub === undefined) finish([]);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// Resolves once the Agent's state is back at Rest — its outstanding pick answered, or nothing
|
|
834
|
+
// running below it — or has been left, or with nothing once this actor stops. Read a microtask
|
|
835
|
+
// after each snapshot of the invoking Machine: the mailbox has drained (rest.ts).
|
|
836
|
+
const agentState = { invoker: self._parent, invokeId: self.id };
|
|
837
|
+
const untilRest = (): Promise<void> =>
|
|
838
|
+
new Promise((resolve) => {
|
|
839
|
+
let done = false;
|
|
840
|
+
let sub: { unsubscribe(): void } | undefined;
|
|
841
|
+
const finish = (): void => {
|
|
842
|
+
if (done) return;
|
|
843
|
+
done = true;
|
|
844
|
+
const at = disposers.indexOf(finish);
|
|
845
|
+
if (at >= 0) disposers.splice(at, 1);
|
|
846
|
+
sub?.unsubscribe();
|
|
847
|
+
resolve();
|
|
848
|
+
};
|
|
849
|
+
const check = (): void => {
|
|
850
|
+
if (stopped) return finish();
|
|
851
|
+
const held = binding.outstandingOf?.(currentIid);
|
|
852
|
+
if (held !== undefined && !held.fetched && !held.answered) return;
|
|
853
|
+
if (restOf(agentState) !== "busy") finish();
|
|
854
|
+
};
|
|
855
|
+
disposers.push(finish);
|
|
856
|
+
check();
|
|
857
|
+
if (done) return;
|
|
858
|
+
sub = self._parent?.subscribe({ next: () => queueMicrotask(check), error: finish, complete: finish });
|
|
859
|
+
if (sub === undefined) finish();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// A Harness restart cut this Turn (ADR-0031). It is NOT an `agent.fault` when the Agent's state is
|
|
863
|
+
// busy — something it started runs below it — or its pick's answer has not reached the model:
|
|
864
|
+
// faulting would route the Machine out of the state and stop the work the Turn did not want
|
|
865
|
+
// stopped. jr2 waits for Rest, then re-admits the Agent on the same conversation, which the
|
|
866
|
+
// rebuilt Harness holds, with the answer as a notice. Undefined: not this case, the caller faults.
|
|
867
|
+
let cuts = 0;
|
|
868
|
+
const readmitAfterCut = async (err: unknown): Promise<AgentAdmission | "stopped" | undefined> => {
|
|
869
|
+
if (stopped) return "stopped";
|
|
870
|
+
if (restarted === undefined && settledByRestart(err) === undefined) return undefined;
|
|
871
|
+
const unread = (): boolean => {
|
|
872
|
+
const held = binding.outstandingOf?.(currentIid);
|
|
873
|
+
return held !== undefined && !held.fetched;
|
|
874
|
+
};
|
|
875
|
+
if (restOf(agentState) !== "busy" && !unread()) return undefined;
|
|
876
|
+
await untilRest();
|
|
877
|
+
if (stopped) return "stopped";
|
|
878
|
+
const held = binding.outstandingOf?.(currentIid);
|
|
879
|
+
const notice: Notice[] =
|
|
880
|
+
held !== undefined && !held.fetched && held.answered && !held.turnComplete
|
|
881
|
+
? [
|
|
882
|
+
{
|
|
883
|
+
kind: "pick-answer",
|
|
884
|
+
scope: "conversation",
|
|
885
|
+
pick: held.pick,
|
|
886
|
+
cut: true,
|
|
887
|
+
...(held.answer !== undefined ? { answer: held.answer } : {}),
|
|
888
|
+
},
|
|
889
|
+
]
|
|
890
|
+
: [];
|
|
891
|
+
cuts++;
|
|
892
|
+
binding.telemetry?.({
|
|
893
|
+
kind: "retry",
|
|
894
|
+
child: self._parent?.id ?? self.id,
|
|
895
|
+
attempt: cuts,
|
|
896
|
+
reason: "re-admitted after a Harness restart",
|
|
897
|
+
});
|
|
898
|
+
restarted = undefined;
|
|
899
|
+
controller = new AbortController();
|
|
900
|
+
// The rebuilt Harness may not read the conversation back: then this Turn lands on a new one,
|
|
901
|
+
// and is told (ADR-0062), as the continue path tells it below.
|
|
902
|
+
if (!(await client.holds(input.agentName, currentIid, { signal: controller.signal }))) {
|
|
903
|
+
binding.raiseNotice?.(
|
|
904
|
+
{ kind: "conversation-new", scope: "conversation", reason: "the Harness no longer holds it" },
|
|
905
|
+
conversation,
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
if (stopped) return "stopped";
|
|
909
|
+
// The notices pending for this Turn ride the re-admission, with the cut pick's answer after them.
|
|
910
|
+
const admission = await admitTurn(
|
|
911
|
+
{ ...framed, attach: undefined, instanceId: currentIid, prompt: READMIT_PROMPT },
|
|
912
|
+
{ notices: true, extra: notice },
|
|
913
|
+
);
|
|
914
|
+
binding.heldAnswerDelivered?.(currentIid);
|
|
915
|
+
return admission;
|
|
916
|
+
};
|
|
917
|
+
|
|
545
918
|
// Down-channel: interrupts only (ADR-0002 split-channel model).
|
|
546
919
|
receive((event) => {
|
|
547
920
|
if (event.type === "CANCEL") abandon();
|
|
@@ -555,8 +928,9 @@ export function agentActorWith(
|
|
|
555
928
|
// - INFRA faults: provider retries run inside the turn, Harness-side, and the wire
|
|
556
929
|
// client reconnects transparently — so a `settle` rejection means the Submission settled
|
|
557
930
|
// failed/aborted, or the conversation is lost (ADR-0027). Terminal, no jr2 re-run.
|
|
558
|
-
// - NO-SIGNAL: the Submission settles COMPLETED
|
|
559
|
-
// calls that a normal
|
|
931
|
+
// - NO-SIGNAL: the Submission settles COMPLETED with the Turn still open and Allowed picks
|
|
932
|
+
// left (the ask-again rule, below). The Harness calls that a normal Submission, so jr2 owns
|
|
933
|
+
// a budgeted re-prompt on the SAME iid (the conversation
|
|
560
934
|
// continues; each nudge is a fresh admission, ledgered like any other).
|
|
561
935
|
// - RUNAWAY: the Harness ended a turn that would not conclude and settled it failed with
|
|
562
936
|
// the typed "runaway" error (ADR-0035). The degenerate context is poisoned, so the
|
|
@@ -564,13 +938,16 @@ export function agentActorWith(
|
|
|
564
938
|
// admitting the identical prompt — closed to continuations, which opted out of exactly that.
|
|
565
939
|
// Any budget exhausting emits the ONE terminal `agent.fault { reason }`.
|
|
566
940
|
void (async () => {
|
|
567
|
-
const fault = (reason: string) => {
|
|
941
|
+
const fault = (reason: string, opts: { conversationKept?: boolean } = {}) => {
|
|
568
942
|
if (stopped) return;
|
|
569
943
|
// Burn the conversation before the fault lands (ADR-0057): the state the fault routes to
|
|
570
944
|
// may invoke this Agent again in the same macrostep, and its mapper must already read the
|
|
571
945
|
// bumped epoch. Continuations only — a fresh invocation mints its own conversation, so
|
|
572
|
-
// there is nothing to burn.
|
|
573
|
-
|
|
946
|
+
// there is nothing to burn. Not when a Harness restart cut the Turn: the rebuilt Harness
|
|
947
|
+
// holds the conversation (ADR-0031), so only the Turn was lost and the next `continue`
|
|
948
|
+
// lands on it; that Turn's `holds` check tells it if the rebuild could not read it (ADR-0062).
|
|
949
|
+
// The fault's reason is the next Turn's news: it is why that conversation starts empty.
|
|
950
|
+
if (input.continuation && !opts.conversationKept) binding.bumpEpoch?.(conversation, reason);
|
|
574
951
|
sendBack({ type: "agent.fault", instanceId, reason } satisfies FaultTelemetry);
|
|
575
952
|
};
|
|
576
953
|
try {
|
|
@@ -581,8 +958,20 @@ export function agentActorWith(
|
|
|
581
958
|
await pendingAborts.get(instanceId);
|
|
582
959
|
let admission = input.attach;
|
|
583
960
|
if (!admission) {
|
|
584
|
-
|
|
585
|
-
|
|
961
|
+
// A continued conversation admitted before may be gone with no fault to say so: its
|
|
962
|
+
// Harness restarted between the Turns. Then this Turn lands on a new one, and is told
|
|
963
|
+
// (ADR-0062). A conversation never admitted has nothing to lose, so nothing is asked.
|
|
964
|
+
if (
|
|
965
|
+
input.continuation &&
|
|
966
|
+
binding.admissionOf?.(instanceId) !== undefined &&
|
|
967
|
+
!(await client.holds(input.agentName, instanceId, { signal: controller.signal }))
|
|
968
|
+
) {
|
|
969
|
+
binding.raiseNotice?.(
|
|
970
|
+
{ kind: "conversation-new", scope: "conversation", reason: "the Harness no longer holds it" },
|
|
971
|
+
conversation,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
admission = await admitTurn(framed);
|
|
586
975
|
// The admission marker (ADR-0023): the Turn and its framing, once — a re-attach
|
|
587
976
|
// continues a Turn already announced, and a nudge (below) is mechanism, not narrative
|
|
588
977
|
// (its telemetry already rides the feed).
|
|
@@ -592,7 +981,7 @@ export function agentActorWith(
|
|
|
592
981
|
let rerolls = 0;
|
|
593
982
|
for (;;) {
|
|
594
983
|
try {
|
|
595
|
-
await client.settle(admission, { signal: controller.signal });
|
|
984
|
+
await client.settle(admission, { signal: controller.signal, onMemoryLimit });
|
|
596
985
|
} catch (err) {
|
|
597
986
|
// The reroll gate closes on `signaled` like the nudge gate below: a delivered pick
|
|
598
987
|
// means the workflow already holds this turn's signal, so replaying the identical
|
|
@@ -600,6 +989,14 @@ export function agentActorWith(
|
|
|
600
989
|
// settlement). It also closes when restore dropped `prompt` (`attach` rides instead):
|
|
601
990
|
// there is nothing identical to replay, and the thrown settlement still carries the
|
|
602
991
|
// legible runaway reason.
|
|
992
|
+
// A Harness restart that cut the Turn while its state was busy (ADR-0031): no fault. The
|
|
993
|
+
// workflow did not stop, so neither does the Turn — it waits for Rest, then re-admits.
|
|
994
|
+
const readmitted = await readmitAfterCut(err);
|
|
995
|
+
if (readmitted === "stopped") return;
|
|
996
|
+
if (readmitted !== undefined) {
|
|
997
|
+
admission = readmitted;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
603
1000
|
const reason = stopped || signaled ? undefined : runawayReason(err);
|
|
604
1001
|
if (reason === undefined || input.continuation || input.prompt === undefined || rerolls >= runawayBudget)
|
|
605
1002
|
throw err;
|
|
@@ -615,16 +1012,30 @@ export function agentActorWith(
|
|
|
615
1012
|
// `/mcp/<currentIid>`, so its address must be live before the Harness can run it.
|
|
616
1013
|
currentIid = `${instanceId}-r${rerolls}`;
|
|
617
1014
|
registerSurface(currentIid);
|
|
618
|
-
admission = await
|
|
619
|
-
{ ...framed, attach: undefined, instanceId: currentIid },
|
|
620
|
-
{ definition, signal: controller.signal },
|
|
621
|
-
);
|
|
622
|
-
ledger(admission);
|
|
1015
|
+
admission = await admitTurn({ ...framed, attach: undefined, instanceId: currentIid });
|
|
623
1016
|
continue;
|
|
624
1017
|
}
|
|
625
|
-
|
|
1018
|
+
// THE ASK-AGAIN RULE (ADR-0016): while the Turn is open, a model that stopped is asked again
|
|
1019
|
+
// once its Allowed picks are not empty — and never with an empty list. One rule, three
|
|
1020
|
+
// moments:
|
|
1021
|
+
// - inside the Submission, the Harness's step boundary (`between-steps.ts`) restates the
|
|
1022
|
+
// Turn part when the list changed since the model last read it; that message keeps the
|
|
1023
|
+
// engine's loop going, and costs no Submission;
|
|
1024
|
+
// - after it settles with picks LEFT, the NUDGE, here;
|
|
1025
|
+
// - after it settles with NONE left (the Turn waits on a Gate, a child's invoke, a timer),
|
|
1026
|
+
// the WAKE: this actor watches the invoking Machine and asks the first time a pick
|
|
1027
|
+
// opens. A Turn that was never meant to pick waits until its state exits.
|
|
1028
|
+
// The nudge and the wake are one Submission kind — the same text, the same conversation,
|
|
1029
|
+
// the same budget — so a Machine that opens and closes picks under a stopped model cannot
|
|
1030
|
+
// ask it without bound. A Turn whose pick left the Agent's state is over: the invoke was
|
|
1031
|
+
// stopped, and nothing is asked.
|
|
1032
|
+
if (stopped) return;
|
|
1033
|
+
let allowed = allowedNow();
|
|
1034
|
+
const woken = allowed.length === 0;
|
|
1035
|
+
if (woken) allowed = await picksOpen();
|
|
1036
|
+
if (stopped || allowed.length === 0) return;
|
|
626
1037
|
if (nudges >= nudgeBudget) {
|
|
627
|
-
fault(`agent completed its turn without
|
|
1038
|
+
fault(`agent completed its turn without a pick that ends it; still allowed: ${allowed.join(", ")}`);
|
|
628
1039
|
return;
|
|
629
1040
|
}
|
|
630
1041
|
nudges++;
|
|
@@ -632,17 +1043,59 @@ export function agentActorWith(
|
|
|
632
1043
|
kind: "retry",
|
|
633
1044
|
child: self._parent?.id ?? self.id,
|
|
634
1045
|
attempt: nudges,
|
|
635
|
-
reason: "no-signal nudge",
|
|
1046
|
+
reason: woken ? "picks allowed again" : "no-signal nudge",
|
|
636
1047
|
});
|
|
637
|
-
admission = await
|
|
638
|
-
{
|
|
639
|
-
|
|
1048
|
+
admission = await admitTurn(
|
|
1049
|
+
{
|
|
1050
|
+
...framed,
|
|
1051
|
+
attach: undefined,
|
|
1052
|
+
instanceId: currentIid,
|
|
1053
|
+
prompt: NUDGE_PROMPT,
|
|
1054
|
+
},
|
|
1055
|
+
{ notices: false },
|
|
640
1056
|
);
|
|
641
|
-
ledger(admission);
|
|
642
1057
|
}
|
|
643
1058
|
} catch (err) {
|
|
644
1059
|
if (stopped) return;
|
|
645
|
-
|
|
1060
|
+
// The Harness restarted under this Turn (ADR-0021): whatever the stream said as it was cut,
|
|
1061
|
+
// the restart is why.
|
|
1062
|
+
// A Settlement that names the restart is the same fault in the Harness's words (ADR-0021
|
|
1063
|
+
// wording, ADR-0031's rebuilt stream), so it carries the same `Turn lost` prefix.
|
|
1064
|
+
const restartSettled = restarted === undefined ? settledByRestart(err) : undefined;
|
|
1065
|
+
const message =
|
|
1066
|
+
restarted !== undefined
|
|
1067
|
+
? harnessRestartReason(restarted)
|
|
1068
|
+
: restartSettled !== undefined
|
|
1069
|
+
? `Turn lost (${restartSettled})`
|
|
1070
|
+
: (providerLimitReason(err) ?? errorMessage(err));
|
|
1071
|
+
// A lost conversation may be a memory kill (ADR-0061): the kernel's group kill took the
|
|
1072
|
+
// Harness with the Agent's processes. The operator publishes the container's last end on
|
|
1073
|
+
// the Sandbox, so the fault is NAMED — the fixed prefix `memory limit` — instead of
|
|
1074
|
+
// "Turn lost", and the Machine's policy can tell the two apart.
|
|
1075
|
+
//
|
|
1076
|
+
// The fault reaches the Machine (ADR-0016); the next Agent in this Workspace is told
|
|
1077
|
+
// through the Briefing, never through the Frame (ADR-0057, ADR-0062) — a notice raised
|
|
1078
|
+
// BEFORE the fault, because the state it routes to may admit in the same macrostep. Keyed
|
|
1079
|
+
// by the kill itself, so every Turn the one kill ended raises one notice between them.
|
|
1080
|
+
// A restart the Settlement reported before the watch did may still be a memory kill: the
|
|
1081
|
+
// Sandbox is asked, as for a 404.
|
|
1082
|
+
const lostHarness =
|
|
1083
|
+
restarted !== undefined
|
|
1084
|
+
? restarted.lastTerminated?.reason === OOM_KILLED
|
|
1085
|
+
: isLostConversation(err) || restartSettled !== undefined;
|
|
1086
|
+
const kill = lostHarness && sandbox !== undefined ? await memoryFault(sandbox) : undefined;
|
|
1087
|
+
if (stopped) return;
|
|
1088
|
+
if (kill !== undefined && noticeWorkspace !== undefined) {
|
|
1089
|
+
binding.raiseNotice?.(
|
|
1090
|
+
memoryLimit(kill.limit ?? "unknown"),
|
|
1091
|
+
noticeWorkspace,
|
|
1092
|
+
kill.at === undefined ? undefined : `${noticeWorkspace}@${kill.at}`,
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
// A restart the Harness came back from keeps the conversation (ADR-0031); a 404 does not.
|
|
1096
|
+
fault(kill !== undefined ? `${kill.reason}\n ${message}` : message, {
|
|
1097
|
+
conversationKept: restarted !== undefined || restartSettled !== undefined,
|
|
1098
|
+
});
|
|
646
1099
|
}
|
|
647
1100
|
})();
|
|
648
1101
|
|