@jr2/orchestrator 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +56 -20
- package/docs/concepts/architecture.md +30 -17
- package/docs/concepts/charts.md +6 -3
- package/docs/concepts/converge.md +3 -0
- package/docs/concepts/effects.md +86 -31
- package/docs/concepts/machines-and-workflows.md +9 -0
- package/docs/concepts/pools-and-sources.md +6 -6
- package/docs/concepts/secrets-and-trust.md +126 -42
- package/docs/concepts/the-menu.md +3 -0
- package/docs/concepts/workspaces.md +129 -51
- package/docs/get-started/an-agent-in-a-sandbox.md +21 -19
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +17 -4
- package/docs/guides/bind-open-parts.md +5 -4
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +1 -1
- package/docs/guides/let-an-agent-write-a-chart.md +5 -1
- package/docs/guides/look-inside-a-sandbox.md +18 -15
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +19 -18
- package/docs/guides/set-an-agents-access-and-dials.md +67 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/use-a-hosted-provider.md +6 -0
- package/docs/guides/watch-a-run.md +47 -24
- package/docs/guides/write-a-workflow.md +1 -1
- package/docs/llms.txt +33 -12
- package/docs/reference/authoring-surface/agent.md +34 -11
- package/docs/reference/authoring-surface/answer.md +19 -3
- package/docs/reference/authoring-surface/arg.md +27 -5
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/command.md +36 -21
- package/docs/reference/authoring-surface/fail.md +17 -4
- package/docs/reference/authoring-surface/file.md +36 -16
- package/docs/reference/authoring-surface/http.md +108 -8
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +1 -1
- package/docs/reference/authoring-surface/open.md +36 -5
- package/docs/reference/authoring-surface/paramsof.md +22 -4
- package/docs/reference/authoring-surface/pool.md +4 -4
- package/docs/reference/authoring-surface/push.md +30 -12
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/secret.md +25 -4
- package/docs/reference/authoring-surface/source.md +3 -3
- package/docs/reference/authoring-surface/type-agentdefinition.md +2 -2
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +5 -6
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +22 -4
- package/docs/reference/authoring-surface/type-customize.md +4 -2
- package/docs/reference/authoring-surface/type-dispatchoutput.md +8 -1
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +2 -4
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +7 -1
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +2 -2
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +32 -7
- package/docs/reference/authoring-surface/type-reposlot.md +5 -5
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcespec.md +10 -10
- package/docs/reference/authoring-surface/type-workspaced.md +29 -8
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +65 -37
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +30 -10
- package/docs/reference/cli.md +1 -1
- package/docs/reference/config.md +13 -4
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +17 -10
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +29 -19
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +30 -30
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +35 -17
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +8 -2
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +6 -4
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +21 -9
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +2 -1
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +8 -9
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +62 -65
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +9 -1
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +13 -12
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +4 -1
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +18 -19
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +8 -8
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +4 -4
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +24 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +5 -3
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +110 -67
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +101 -74
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +5 -6
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +2 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +181 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +74 -63
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +31 -20
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions.md +3 -0
- package/docs/reference/glossary.md +51 -12
- package/docs/reference/http-api.md +45 -7
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +5 -4
- package/package.json +3 -3
- package/src/actor.ts +161 -35
- package/src/agent.ts +6 -6
- package/src/ambient.ts +3 -5
- package/src/chart.ts +186 -19
- package/src/charts.ts +9 -2
- package/src/config.ts +16 -6
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +251 -77
- package/src/customize.ts +11 -3
- package/src/effect-checks.ts +50 -3
- package/src/effect.ts +343 -127
- package/src/effector-wire.ts +6 -3
- package/src/fail.ts +17 -4
- package/src/harness-client.ts +74 -31
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +125 -16
- package/src/holds.ts +3 -0
- package/src/http-effect.ts +88 -32
- package/src/http.ts +56 -17
- package/src/index.ts +15 -1
- package/src/internal.ts +1 -0
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +3 -4
- package/src/model-keys.ts +24 -0
- package/src/names.ts +19 -6
- package/src/notices.ts +443 -54
- package/src/open-body.ts +85 -19
- package/src/open.ts +36 -5
- package/src/parts.ts +40 -28
- package/src/party.ts +33 -0
- package/src/pick.ts +41 -7
- package/src/pool.ts +44 -23
- package/src/registration.ts +53 -11
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +106 -20
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +63 -16
- package/src/secret-placement.ts +9 -2
- package/src/setup.ts +86 -6
- package/src/tokens.ts +107 -32
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +10 -10
- package/src/workspace.ts +264 -253
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
package/src/actor.ts
CHANGED
|
@@ -50,11 +50,12 @@ import {
|
|
|
50
50
|
type ThinkingLevel,
|
|
51
51
|
type WorkspaceAccess,
|
|
52
52
|
} from "./agent.ts";
|
|
53
|
+
import type { AnyFramePath } from "./worktree.ts";
|
|
53
54
|
import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
|
|
54
55
|
import type { HarnessRestarts, MemoryKill } from "./workspace.ts";
|
|
55
56
|
import { INSTANCE_HARNESS_SERVICE } from "./names.ts";
|
|
56
57
|
import { SENDER_KEY } from "@jr2/agent-protocol";
|
|
57
|
-
import { agentAddress, continuedIid, allowedPicks, resolveAccepts, runBindingOf } from "./registration.ts";
|
|
58
|
+
import { agentAddress, agentSlot, continuedIid, allowedPicks, resolveAccepts, runBindingOf } from "./registration.ts";
|
|
58
59
|
import type { NoticeScope } from "./notices.ts";
|
|
59
60
|
import { restOf } from "./rest.ts";
|
|
60
61
|
import { SUBMISSION_HARNESS_RESTARTED, SUBMISSION_RUNAWAY, type Notice, type PodMemoryKill } from "./wire.ts";
|
|
@@ -143,15 +144,15 @@ export type AgentTurnInput = {
|
|
|
143
144
|
/**
|
|
144
145
|
* The other half of the FRAME (ADR-0057) — WHERE this Turn works: the absolute directory the
|
|
145
146
|
* Harness roots the Working tools at. Per Turn by nature, which is why no definition names one:
|
|
146
|
-
* a Worktree path (`/work/<slot>/<
|
|
147
|
+
* a Worktree path (`/work/<slot>/<name>`) exists only once a Machine has cut it (ADR-0075).
|
|
147
148
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
149
|
+
* Required under Repo Slots: a Turn with no `cwd` there is REFUSED before admission, and a Turn
|
|
150
|
+
* that wants `/work` states it. A `workspace: "none"` Agent frames no directory (it has no Working
|
|
151
|
+
* tools to root — ADR-0028), and a Workspace with no Repo Slot, or a run with no Workspace at all,
|
|
152
|
+
* frames nothing and the Harness keeps `/work`. A relative path is refused at admission, by the
|
|
153
|
+
* Harness.
|
|
153
154
|
*/
|
|
154
|
-
cwd?:
|
|
155
|
+
cwd?: AnyFramePath;
|
|
155
156
|
/**
|
|
156
157
|
* This turn's DIALS (ADR-0018) — how hard to run, layered over the definition's own
|
|
157
158
|
* values. One definition value may be carried by several Machines, so the same persona
|
|
@@ -358,6 +359,10 @@ export interface AgentRunPort {
|
|
|
358
359
|
* Machine edit that changed it. */
|
|
359
360
|
export type AgentAdmitOptions = {
|
|
360
361
|
definition: AgentDefinition;
|
|
362
|
+
/** This Agent's slot (ADR-0076): the actor path of its Machine and its agent name — the
|
|
363
|
+
* registration's. The Harness keeps it with the conversation and states it on each control
|
|
364
|
+
* request; it never derives it from an Instance ID, whose actor ids may hold a `/`. */
|
|
365
|
+
slot: string;
|
|
361
366
|
signal?: AbortSignal;
|
|
362
367
|
/** The pending notices in this admission's scope (ADR-0062), for the Briefing's Turn part. Not
|
|
363
368
|
* on the persisted input either: they are the Orchestrator's, and delivered once. */
|
|
@@ -555,20 +560,15 @@ const READMIT_PROMPT =
|
|
|
555
560
|
* an ABSENT one, and a directory a state named is never second-guessed — a Menu-only Agent handed
|
|
556
561
|
* one simply has no Working tools to root there (ADR-0028/0031).
|
|
557
562
|
*
|
|
558
|
-
* Absent, a Menu-only Agent frames no directory
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
* ADR-0057 was written for
|
|
563
|
-
* frames nothing, so the Harness keeps `/work`, the folder a slotless Workspace's steps share.
|
|
563
|
+
* Absent, a Menu-only Agent frames no directory, and a Workspace with no Repo Slot, or a run outside
|
|
564
|
+
* any Workspace, frames nothing, so the Harness keeps `/work`, the folder a slotless Workspace's
|
|
565
|
+
* steps share. Under Repo Slots the Workspace only clones, so no Worktree exists for a default to
|
|
566
|
+
* name, and the kit gives no slot a meaning: an absent `cwd` is REFUSED, because a wrong guess is
|
|
567
|
+
* the silent failure ADR-0057 was written for (ADR-0075).
|
|
564
568
|
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* the reviewer its path as "cwd and prompt"). A `workspace: "read"` Agent that frames nothing
|
|
569
|
-
* under a one-slot Workspace therefore works in the branch checkout with `write`/`edit` withheld
|
|
570
|
-
* and `bash` kept — the tool layer's hint, not containment (ADR-0028): containment is the
|
|
571
|
-
* detached review Worktree, and only a reviewing state that requested one can frame it.
|
|
569
|
+
* `workspace: "read"` withholds `write`/`edit` and keeps `bash`: the tool layer's hint, not
|
|
570
|
+
* containment (ADR-0028). The containment is a detached Worktree that the state cuts and names as
|
|
571
|
+
* its `cwd`.
|
|
572
572
|
*
|
|
573
573
|
* The refusal throws at invoke, like the Open-model fence: loudly, at the first Turn, before a
|
|
574
574
|
* surface is registered or a pod is spent.
|
|
@@ -583,12 +583,11 @@ function frameCwd(
|
|
|
583
583
|
if (workspace === "none") return undefined;
|
|
584
584
|
const slots = Object.keys(ambient?.repos ?? {});
|
|
585
585
|
if (slots.length === 0) return undefined;
|
|
586
|
-
|
|
586
|
+
// A default here is ADR-0057's incident: an Agent's search hits `default/`, and a relative write
|
|
587
|
+
// lands outside each Worktree. An author who wants `/work` states it.
|
|
587
588
|
throw new Error(
|
|
588
|
-
`turn ${instanceId}: agent "${input.agentName}" works under a Workspace
|
|
589
|
-
|
|
590
|
-
`in (\`cwd: context.workspace.repos.<slot>\`, one of those), because the kit gives no slot a ` +
|
|
591
|
-
`meaning (ADR-0051) and a Turn's Frame is where it works (ADR-0057)`,
|
|
589
|
+
`turn ${instanceId}: agent "${input.agentName}" works under a Workspace with Repo Slots (${slots.join(", ")}) ` +
|
|
590
|
+
'and its Turn framed no `cwd`: say where it works, a Worktree, or `cwd: "/work"` above them (ADR-0057)',
|
|
592
591
|
);
|
|
593
592
|
}
|
|
594
593
|
|
|
@@ -701,7 +700,7 @@ export function agentActorWith(
|
|
|
701
700
|
|
|
702
701
|
// The Frame's other half (ADR-0057): WHERE this Turn works. Resolved HERE, before any surface
|
|
703
702
|
// is registered and before a pod is spent, because the answer is a fact about the run — a
|
|
704
|
-
// Worktree
|
|
703
|
+
// Worktree exists only once a run has cut it, so no definition could have named it.
|
|
705
704
|
const cwd = frameCwd(instanceId, input, workspace, ambient);
|
|
706
705
|
const framed: AgentRunInput = cwd === undefined ? input : { ...input, cwd };
|
|
707
706
|
|
|
@@ -732,8 +731,15 @@ export function agentActorWith(
|
|
|
732
731
|
// This Agent's continued conversation in this Machine (ADR-0057) — the base id the epoch ledger
|
|
733
732
|
// burns. This Turn is PART of it only when it continues it: a fresh Turn is new by definition.
|
|
734
733
|
const conversation = continuedIid(self._parent, binding.runId, input.agentName);
|
|
734
|
+
// This Agent's slot (ADR-0076): the same identity without the run id. The Harness states it on
|
|
735
|
+
// every control request, and the Orchestrator speaks here only for it.
|
|
736
|
+
const slot = agentSlot(self._parent, input.agentName);
|
|
735
737
|
// The iids whose surface the Harness has read (see `carried` below).
|
|
736
738
|
const served = new Set<string>();
|
|
739
|
+
// Per iid, the notices a step boundary's surface read reserved and the Harness has not yet
|
|
740
|
+
// acknowledged (ADR-0062): by the id the read named, every id it stands for — equal notices
|
|
741
|
+
// are said once — and what each was raised as, for `reraiseUnheard`.
|
|
742
|
+
const boundary = new Map<string, Map<string, Array<{ id: string; raised: { notice: Notice; source?: string } }>>>();
|
|
737
743
|
const registerSurface = (iid: string) =>
|
|
738
744
|
disposers.push(
|
|
739
745
|
binding.table.register({
|
|
@@ -744,6 +750,9 @@ export function agentActorWith(
|
|
|
744
750
|
defs,
|
|
745
751
|
menu,
|
|
746
752
|
sandbox,
|
|
753
|
+
slot,
|
|
754
|
+
boundaryNotices: () => readBoundary(iid),
|
|
755
|
+
noticesHeard: (ids) => boundaryHeard(iid, ids),
|
|
747
756
|
deliver: (event) => {
|
|
748
757
|
signaled = true;
|
|
749
758
|
// The settlement-pick marker (ADR-0023), BEFORE the delivery moves the Machine: the pick
|
|
@@ -824,7 +833,8 @@ export function agentActorWith(
|
|
|
824
833
|
// LIVE conversation stamped on the record, so a restore settle-follows the live submission
|
|
825
834
|
// AND re-addresses it (see `currentIid` above). The notices the admission carried are
|
|
826
835
|
// delivered by this same write (ADR-0062).
|
|
827
|
-
|
|
836
|
+
// The conversation is a member of its Workspace's audiences from this save on (ADR-0062).
|
|
837
|
+
const ledger = (admission: AgentAdmission, delivered: readonly string[], taker: string) =>
|
|
828
838
|
binding.recordAdmission?.(
|
|
829
839
|
instanceId,
|
|
830
840
|
{
|
|
@@ -835,6 +845,10 @@ export function agentActorWith(
|
|
|
835
845
|
...(input.turnId === undefined ? {} : { turnId: input.turnId }),
|
|
836
846
|
},
|
|
837
847
|
delivered,
|
|
848
|
+
taker,
|
|
849
|
+
noticeWorkspace === undefined
|
|
850
|
+
? undefined
|
|
851
|
+
: { workspace: noticeWorkspace, slot, continued: Boolean(input.continuation) && taker === conversationIid() },
|
|
838
852
|
);
|
|
839
853
|
|
|
840
854
|
// Where this Turn hears notices (ADR-0062). Its continued conversation's — the base id the
|
|
@@ -845,7 +859,11 @@ export function agentActorWith(
|
|
|
845
859
|
const noticeScope: NoticeScope = {
|
|
846
860
|
...(input.continuation ? { conversation } : {}),
|
|
847
861
|
...(noticeWorkspace !== undefined ? { workspace: noticeWorkspace } : {}),
|
|
862
|
+
slot,
|
|
848
863
|
};
|
|
864
|
+
// The Instance ID of the continued conversation this Turn rides — its epoch's spelling, which
|
|
865
|
+
// the minted `instanceId` is. A fresh Turn has none.
|
|
866
|
+
const conversationIid = (): string | undefined => (input.continuation ? instanceId : undefined);
|
|
849
867
|
// No `agent`: the notice names no Turn — what the pod's list says (below).
|
|
850
868
|
// No `limit`: the sizes read were not sizes, and the notice says none.
|
|
851
869
|
const memoryLimit = ({ limit, peak, agent }: { limit?: string; peak?: string; agent?: string }): Notice => ({
|
|
@@ -859,12 +877,15 @@ export function agentActorWith(
|
|
|
859
877
|
// Turn's Agent. A stopped actor's last poll names nothing: the state that asked for the Turn has
|
|
860
878
|
// exited, and the pod's list carries the kill. Keyed by the kill's id, so a second running
|
|
861
879
|
// conversation's stream, or the pod's list, adds nothing.
|
|
880
|
+
// The kill cut this Turn (D4 rule 2): its Agent's next Turn retries the broken work, and hears it.
|
|
862
881
|
const onMemoryLimit = (kill: StreamedKill): void => {
|
|
863
882
|
if (stopped || noticeWorkspace === undefined) return;
|
|
864
883
|
binding.raiseNotice?.(
|
|
865
884
|
memoryLimit({ limit: kill.limit, peak: kill.peak, agent: input.agentName }),
|
|
866
885
|
noticeWorkspace,
|
|
867
886
|
kill.legacy ? kill.id : guardSource(noticeWorkspace, kill.id),
|
|
887
|
+
[],
|
|
888
|
+
{ retry: { slot, cut: currentIid }, at: new Date().toISOString() },
|
|
868
889
|
);
|
|
869
890
|
};
|
|
870
891
|
// …but a stream is not every kill's path. A kill while no Submission ran — a process an earlier
|
|
@@ -906,6 +927,8 @@ export function agentActorWith(
|
|
|
906
927
|
memoryLimit({ limit: kill.limit }),
|
|
907
928
|
noticeWorkspace,
|
|
908
929
|
kernelSource(noticeWorkspace, kill.at),
|
|
930
|
+
[],
|
|
931
|
+
{ at: kill.at },
|
|
909
932
|
);
|
|
910
933
|
};
|
|
911
934
|
let killMark = input.attach?.killMark;
|
|
@@ -937,7 +960,9 @@ export function agentActorWith(
|
|
|
937
960
|
noticeWorkspace,
|
|
938
961
|
guardSource(noticeWorkspace, newest.id),
|
|
939
962
|
kills.slice(0, -1).map((kill) => guardSource(noticeWorkspace, kill.id)),
|
|
963
|
+
{ at: newest.at, told: newest.told.map((t) => t.instanceId) },
|
|
940
964
|
);
|
|
965
|
+
applyTold(kills);
|
|
941
966
|
const toldHere = (kill: ListedKill): boolean =>
|
|
942
967
|
kill.told.some((t) => t.agent === input.agentName && t.instanceId === currentIid);
|
|
943
968
|
// The first kill after the Turn's mark. A mark no longer in the bounded list is older than
|
|
@@ -956,6 +981,86 @@ export function agentActorWith(
|
|
|
956
981
|
};
|
|
957
982
|
};
|
|
958
983
|
|
|
984
|
+
// The conversations each listed kill's `bash` answer told leave its audience (D4 rule 3) — a
|
|
985
|
+
// kill raised off a stream before its answer returned is found here too.
|
|
986
|
+
const applyTold = (kills: readonly ListedKill[]): void => {
|
|
987
|
+
if (noticeWorkspace === undefined) return;
|
|
988
|
+
const told = kills
|
|
989
|
+
.filter((kill) => kill.told.length > 0)
|
|
990
|
+
.map((kill) => ({ source: guardSource(noticeWorkspace, kill.id), takers: kill.told.map((t) => t.instanceId) }));
|
|
991
|
+
if (told.length > 0) binding.noticesTold?.(told);
|
|
992
|
+
};
|
|
993
|
+
// The step boundary's read (ADR-0062): this conversation's pending notices in their audience,
|
|
994
|
+
// reserved for this Turn until the Harness acknowledges the append. A guard kill's audience
|
|
995
|
+
// leaves out the conversations its `bash` answer told, which only the pod's list says — so when
|
|
996
|
+
// one is among them, the list is read first, and a read that could not tell takes no guard kill.
|
|
997
|
+
const readBoundary = async (iid: string): Promise<Array<{ id: string; notice: Notice }>> => {
|
|
998
|
+
if (stopped || iid !== currentIid || binding.takeNotices === undefined) return [];
|
|
999
|
+
const scope: NoticeScope = { ...noticeScope, taker: iid };
|
|
1000
|
+
if (noticeWorkspace !== undefined) {
|
|
1001
|
+
const guard = guardSource(noticeWorkspace, "");
|
|
1002
|
+
if ((binding.peekNotices?.(scope) ?? []).some((p) => p.source?.startsWith(guard))) {
|
|
1003
|
+
let kills: ListedKill[] | undefined;
|
|
1004
|
+
try {
|
|
1005
|
+
kills = await client.memoryKills({
|
|
1006
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(memoryKillsReadMs)]),
|
|
1007
|
+
});
|
|
1008
|
+
} catch {
|
|
1009
|
+
kills = undefined;
|
|
1010
|
+
}
|
|
1011
|
+
if (kills === undefined) scope.exceptPrefix = guard;
|
|
1012
|
+
else applyTold(kills);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (stopped || iid !== currentIid) return [];
|
|
1016
|
+
const taken = binding.takeNotices(scope);
|
|
1017
|
+
const held = boundary.get(iid) ?? new Map();
|
|
1018
|
+
const said = new Map<string, string>();
|
|
1019
|
+
const out: Array<{ id: string; notice: Notice }> = [];
|
|
1020
|
+
taken.ids.forEach((id, i) => {
|
|
1021
|
+
const raised = taken.raised?.[i] ?? { notice: taken.notices[i]! };
|
|
1022
|
+
const key = JSON.stringify(raised.notice);
|
|
1023
|
+
const first = said.get(key);
|
|
1024
|
+
if (first === undefined) {
|
|
1025
|
+
said.set(key, id);
|
|
1026
|
+
held.set(id, [{ id, raised }]);
|
|
1027
|
+
out.push({ id, notice: raised.notice });
|
|
1028
|
+
} else held.get(first)!.push({ id, raised });
|
|
1029
|
+
});
|
|
1030
|
+
if (held.size > 0) boundary.set(iid, held);
|
|
1031
|
+
return out;
|
|
1032
|
+
};
|
|
1033
|
+
// A step boundary whose notices the Harness did not append — a stopped model with no Allowed
|
|
1034
|
+
// picks, or a Turn ending as it read (`between-steps.ts`) — holds them until the next admission
|
|
1035
|
+
// of the conversation, which takes them again: a wake or a nudge carries them (ADR-0062).
|
|
1036
|
+
const releaseBoundary = (iid: string): void => {
|
|
1037
|
+
const held = boundary.get(iid);
|
|
1038
|
+
if (held === undefined) return;
|
|
1039
|
+
boundary.delete(iid);
|
|
1040
|
+
binding.releaseNotices?.(
|
|
1041
|
+
[...held.values()].flatMap((hs) => hs.map((h) => h.id)),
|
|
1042
|
+
iid,
|
|
1043
|
+
);
|
|
1044
|
+
};
|
|
1045
|
+
// The Harness appended them: heard in this conversation, in one save. An id this read did not
|
|
1046
|
+
// name changes nothing.
|
|
1047
|
+
const boundaryHeard = (iid: string, ids: readonly string[]): number => {
|
|
1048
|
+
const held = boundary.get(iid);
|
|
1049
|
+
if (held === undefined) return 0;
|
|
1050
|
+
const heard = ids.flatMap((id) => held.get(id) ?? []);
|
|
1051
|
+
for (const id of ids) held.delete(id);
|
|
1052
|
+
if (held.size === 0) boundary.delete(iid);
|
|
1053
|
+
if (heard.length === 0) return 0;
|
|
1054
|
+
binding.acknowledgeNotices?.(
|
|
1055
|
+
heard.map((h) => h.id),
|
|
1056
|
+
iid,
|
|
1057
|
+
);
|
|
1058
|
+
const bucket = carriedBy.get(iid) ?? { heard: [], unheard: [], task: false, fresh: [] };
|
|
1059
|
+
bucket.heard.push(...heard.map((h) => h.raised));
|
|
1060
|
+
carriedBy.set(iid, bucket);
|
|
1061
|
+
return ids.filter((id) => heard.some((h) => h.id === id)).length;
|
|
1062
|
+
};
|
|
1063
|
+
|
|
959
1064
|
// The Turn's admission and a reroll's carry the pending notices in scope — a reroll's
|
|
960
1065
|
// conversation is fresh, and has heard nothing. A nudge's and a wake's carry only the guard
|
|
961
1066
|
// kills of this Turn that came after its own `bash` call's last kill verdict (`hearPodKills`):
|
|
@@ -996,26 +1101,34 @@ export function agentActorWith(
|
|
|
996
1101
|
) => {
|
|
997
1102
|
await hearKernelKill();
|
|
998
1103
|
const { known, unsaid, said } = await hearPodKills();
|
|
1104
|
+
releaseBoundary(turn.instanceId);
|
|
1105
|
+
// The conversation that takes a Workspace notice (ADR-0062): the one this admission rides, its
|
|
1106
|
+
// Instance ID. A continued Turn's is the continued conversation's; a fresh Turn's, a reroll's
|
|
1107
|
+
// and a burned conversation's successor's are each a new conversation, which has heard none.
|
|
1108
|
+
// A Workspace notice is delivered once in each, so every conversation in the Workspace hears
|
|
1109
|
+
// it — each child of a Pool has its own — and no conversation hears it twice.
|
|
1110
|
+
const taker = turn.instanceId;
|
|
999
1111
|
// A read that could not tell leaves the same Turn no way to know which kills its own `bash`
|
|
1000
1112
|
// reported: it takes none, as a nudge takes none then, and they stay for the next Turn.
|
|
1001
1113
|
const sameTurn: NoticeScope = known
|
|
1002
|
-
? { ...noticeScope, except: said }
|
|
1114
|
+
? { ...noticeScope, taker, except: said }
|
|
1003
1115
|
: {
|
|
1004
1116
|
...noticeScope,
|
|
1117
|
+
taker,
|
|
1005
1118
|
...(noticeWorkspace === undefined ? {} : { exceptPrefix: guardSource(noticeWorkspace, "") }),
|
|
1006
1119
|
};
|
|
1007
1120
|
const scope: NoticeScope | undefined =
|
|
1008
1121
|
opts.notices === "turn"
|
|
1009
|
-
? noticeScope
|
|
1122
|
+
? { ...noticeScope, taker, turn: true }
|
|
1010
1123
|
: opts.notices === "same-turn"
|
|
1011
1124
|
? sameTurn
|
|
1012
1125
|
: unsaid.length > 0
|
|
1013
|
-
? { workspace: noticeWorkspace, only: unsaid }
|
|
1126
|
+
? { workspace: noticeWorkspace, taker, only: unsaid }
|
|
1014
1127
|
: undefined;
|
|
1015
1128
|
const none = { ids: [] as string[], notices: [] as Notice[], raised: [] as Raised[] };
|
|
1016
1129
|
const restored =
|
|
1017
1130
|
opts.include && opts.include.length > 0
|
|
1018
|
-
? (binding.takeNotices?.({ ...noticeScope, only: opts.include }) ?? none)
|
|
1131
|
+
? (binding.takeNotices?.({ ...noticeScope, taker, only: opts.include }) ?? none)
|
|
1019
1132
|
: none;
|
|
1020
1133
|
const main = (scope ? binding.takeNotices?.(scope) : undefined) ?? none;
|
|
1021
1134
|
const taken = {
|
|
@@ -1045,12 +1158,13 @@ export function agentActorWith(
|
|
|
1045
1158
|
try {
|
|
1046
1159
|
admission = await client.admit(turn, {
|
|
1047
1160
|
definition,
|
|
1161
|
+
slot,
|
|
1048
1162
|
signal: controller.signal,
|
|
1049
1163
|
...(notices.length ? { notices } : {}),
|
|
1050
1164
|
...(opts.kept ? { kept: true } : {}),
|
|
1051
1165
|
});
|
|
1052
1166
|
} catch (err) {
|
|
1053
|
-
binding.releaseNotices?.(taken.ids);
|
|
1167
|
+
binding.releaseNotices?.(taken.ids, taker);
|
|
1054
1168
|
if (heard) served.add(turn.instanceId);
|
|
1055
1169
|
throw err;
|
|
1056
1170
|
}
|
|
@@ -1071,7 +1185,7 @@ export function agentActorWith(
|
|
|
1071
1185
|
...(killMark === undefined ? {} : { killMark }),
|
|
1072
1186
|
};
|
|
1073
1187
|
}
|
|
1074
|
-
ledger(admission, taken.ids);
|
|
1188
|
+
ledger(admission, taken.ids, taker);
|
|
1075
1189
|
const bucket = carriedBy.get(turn.instanceId) ?? { heard: [], unheard: [], task: false, fresh: [] };
|
|
1076
1190
|
if (heard) {
|
|
1077
1191
|
bucket.heard.push(...bucket.unheard);
|
|
@@ -1101,7 +1215,9 @@ export function agentActorWith(
|
|
|
1101
1215
|
for (const { notice, source } of again) {
|
|
1102
1216
|
const to = notice.scope === "conversation" ? conversation : noticeWorkspace;
|
|
1103
1217
|
if (to === undefined) continue;
|
|
1104
|
-
|
|
1218
|
+
// A Workspace notice is pending again for the conversation that carried it, which did not
|
|
1219
|
+
// hear it; the others it reached keep theirs.
|
|
1220
|
+
if (binding.restoreNotice) binding.restoreNotice(notice, to, source, iid);
|
|
1105
1221
|
else binding.raiseNotice?.(notice, to);
|
|
1106
1222
|
if (source !== undefined) restored.push(source);
|
|
1107
1223
|
}
|
|
@@ -1122,7 +1238,14 @@ export function agentActorWith(
|
|
|
1122
1238
|
// Pool cancelling a child. The one exception is the host stopping the run for its own
|
|
1123
1239
|
// reasons, which ADR-0007's restore re-attaches to.
|
|
1124
1240
|
if (binding.hostStopping) return;
|
|
1241
|
+
// A step boundary's notices the Harness never acknowledged are pending again (ADR-0062).
|
|
1242
|
+
for (const iid of [...boundary.keys()]) releaseBoundary(iid);
|
|
1125
1243
|
reraiseUnheard();
|
|
1244
|
+
// The Turn ended: a fresh conversation leaves every audience in its Workspace, a continued one
|
|
1245
|
+
// waits for its next Turn (ADR-0062).
|
|
1246
|
+
if (noticeWorkspace !== undefined) {
|
|
1247
|
+
binding.noticeTurnEnded?.(noticeWorkspace, [...new Set([...carriedBy.keys(), currentIid])]);
|
|
1248
|
+
}
|
|
1126
1249
|
// Fire-and-forget, and unreportable BY CONSTRUCTION: this actor is stopped, so there is no
|
|
1127
1250
|
// `agent.fault` left to raise. An orphan that survives a failed abort 404s on every tool
|
|
1128
1251
|
// call and settles on its own.
|
|
@@ -1548,10 +1671,13 @@ export function agentActorWith(
|
|
|
1548
1671
|
const kill = lostHarness && sandbox !== undefined ? await memoryFault(sandbox) : undefined;
|
|
1549
1672
|
if (stopped) return;
|
|
1550
1673
|
if (kill !== undefined && noticeWorkspace !== undefined) {
|
|
1674
|
+
// The kill cut this Turn (D4 rule 2): its Agent's next Turn retries the work, and hears it.
|
|
1551
1675
|
binding.raiseNotice?.(
|
|
1552
1676
|
memoryLimit({ limit: kill.limit, agent: input.agentName }),
|
|
1553
1677
|
noticeWorkspace,
|
|
1554
1678
|
kill.at === undefined ? undefined : kernelSource(noticeWorkspace, kill.at),
|
|
1679
|
+
[],
|
|
1680
|
+
{ retry: { slot, cut: currentIid }, ...(kill.at === undefined ? {} : { at: kill.at }) },
|
|
1555
1681
|
);
|
|
1556
1682
|
}
|
|
1557
1683
|
// A restart the Harness came back from keeps the conversation (ADR-0031); a 404 does not.
|
package/src/agent.ts
CHANGED
|
@@ -59,8 +59,8 @@ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhi
|
|
|
59
59
|
export type WorkspaceAccess = "write" | "read" | "none";
|
|
60
60
|
|
|
61
61
|
// The plain-data Agent definition (ADR-0018): IDENTITY plus one default. A Turn's Frame — what it
|
|
62
|
-
// is about and where it works — is not here and cannot be (ADR-0057): a Worktree
|
|
63
|
-
// once a run has
|
|
62
|
+
// is about and where it works — is not here and cannot be (ADR-0057): a Worktree exists only
|
|
63
|
+
// once a run has cut it, so no definition written before the run could name one.
|
|
64
64
|
//
|
|
65
65
|
// Must stay JSON-serializable: the definition rides the Turn to the Harness (ADR-0049), so
|
|
66
66
|
// anything non-serializable would be silently lost — grow this contract deliberately. ADR-0028
|
|
@@ -119,14 +119,14 @@ export type AgentDefinition = {
|
|
|
119
119
|
* level in the invoke's `input`. */
|
|
120
120
|
thinkingLevel?: ThinkingLevel;
|
|
121
121
|
// The persona in one word (ADR-0028), deliberately not `tools` (that names the control-plane
|
|
122
|
-
// Menu, what it may SAY). `"read"` states intent and stops the honest path: bash stays, and
|
|
123
|
-
// detached
|
|
122
|
+
// Menu, what it may SAY). `"read"` states intent and stops the honest path: bash stays, and a
|
|
123
|
+
// detached Worktree is the containment. `"none"` frames no directory — only Working tools
|
|
124
124
|
// consume one — and places the Turn on the Instance Harness (ADR-0031).
|
|
125
125
|
/** What the Agent may do to the Workspace. Default: `"write"`.
|
|
126
126
|
* - `"write"`: all the Working tools (read, write, edit, bash, grep, glob).
|
|
127
127
|
* - `"read"`: no write or edit tool. The Agent keeps bash, so `"read"` does not stop a command
|
|
128
|
-
* that writes. To keep the Agent from changing the branch, frame its Turn in
|
|
129
|
-
* Worktree:
|
|
128
|
+
* that writes. To keep the Agent from changing the branch, frame its Turn in a detached
|
|
129
|
+
* Worktree: cut it with `{ path, detach: sha }`, then give that `path` as `cwd`.
|
|
130
130
|
* - `"none"`: no Working tools. The Agent only reads its prompt and picks from its Menu. Its Turn
|
|
131
131
|
* runs on the Instance Harness, not in a Sandbox, so it needs no `workspace()` around it. */
|
|
132
132
|
workspace?: WorkspaceAccess;
|
package/src/ambient.ts
CHANGED
|
@@ -21,14 +21,12 @@ import type { AnyActorRef } from "xstate";
|
|
|
21
21
|
export type AmbientHandles = {
|
|
22
22
|
endpoint: string;
|
|
23
23
|
sandbox: string;
|
|
24
|
-
/** Every Repo Slot's
|
|
24
|
+
/** Every Repo Slot's folder, `/work/<slot>`, by slot (ADR-0051). */
|
|
25
25
|
repos: Record<string, string>;
|
|
26
26
|
/** Every Repo Slot's Binding url, by slot (ADR-0051): where a push goes. */
|
|
27
27
|
bindings?: Record<string, string>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/** Detached review-worktree paths, when the spec carried `reviewSha` (ADR-0028). */
|
|
31
|
-
review?: Record<string, string>;
|
|
28
|
+
/** Each Repo Slot's Binding base, where the Binding names one: a cut with no `from` starts here. */
|
|
29
|
+
bases?: Record<string, string>;
|
|
32
30
|
};
|
|
33
31
|
|
|
34
32
|
const byRef = new WeakMap<AnyActorRef, AmbientHandles>();
|