@goah/cli 0.11.4 → 0.13.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/dist/cli.js +165 -66
- package/dist/cli.js.map +1 -1
- package/dist/console/assets/index-3sYrDMmt.css +1 -0
- package/dist/console/assets/index-Rtc_B7cs.js +52 -0
- package/dist/console/assets/index-Rtc_B7cs.js.map +1 -0
- package/dist/console/index.html +2 -2
- package/dist/control.d.ts +13 -13
- package/dist/control.d.ts.map +1 -1
- package/dist/control.js +77 -50
- package/dist/control.js.map +1 -1
- package/dist/index.d.ts +6 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -28
- package/dist/index.js.map +1 -1
- package/dist/live-config.js +2 -2
- package/dist/live-config.js.map +1 -1
- package/dist/prompt-queue.d.ts.map +1 -1
- package/dist/prompt-queue.js +5 -0
- package/dist/prompt-queue.js.map +1 -1
- package/dist/setup-wizard.d.ts +13 -2
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +87 -63
- package/dist/setup-wizard.js.map +1 -1
- package/dist/thread-inspect.d.ts +47 -0
- package/dist/thread-inspect.d.ts.map +1 -0
- package/dist/thread-inspect.js +78 -0
- package/dist/thread-inspect.js.map +1 -0
- package/dist/transcript.d.ts +2 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +2 -0
- package/dist/transcript.js.map +1 -0
- package/dist/tui-theme.d.ts +1 -0
- package/dist/tui-theme.d.ts.map +1 -1
- package/dist/tui-theme.js +1 -0
- package/dist/tui-theme.js.map +1 -1
- package/dist/tui.d.ts +16 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +332 -80
- package/dist/tui.js.map +1 -1
- package/dist/update.d.ts +1 -0
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +15 -7
- package/dist/update.js.map +1 -1
- package/dist/web-console.d.ts +1 -1
- package/dist/web-console.d.ts.map +1 -1
- package/dist/web-console.js +27 -37
- package/dist/web-console.js.map +1 -1
- package/dist/welcome.d.ts +2 -2
- package/dist/welcome.d.ts.map +1 -1
- package/dist/welcome.js +24 -36
- package/dist/welcome.js.map +1 -1
- package/node_modules/goah-ledger-contract/dist/execution.d.ts +272 -130
- package/node_modules/goah-ledger-contract/dist/execution.d.ts.map +1 -1
- package/node_modules/goah-ledger-contract/dist/execution.js +31 -19
- package/node_modules/goah-ledger-contract/dist/execution.js.map +1 -1
- package/node_modules/goah-ledger-contract/dist/index.d.ts +1 -2
- package/node_modules/goah-ledger-contract/dist/index.d.ts.map +1 -1
- package/node_modules/goah-ledger-contract/dist/index.js +1 -2
- package/node_modules/goah-ledger-contract/dist/index.js.map +1 -1
- package/node_modules/goah-ledger-contract/dist/kernel.d.ts +1 -1
- package/node_modules/goah-ledger-contract/dist/kernel.d.ts.map +1 -1
- package/node_modules/goah-ledger-contract/dist/kernel.js +1 -1
- package/node_modules/goah-ledger-contract/dist/kernel.js.map +1 -1
- package/node_modules/goah-ledger-contract/dist/transcript.d.ts +63 -0
- package/node_modules/goah-ledger-contract/dist/transcript.d.ts.map +1 -0
- package/node_modules/goah-ledger-contract/dist/transcript.js +115 -0
- package/node_modules/goah-ledger-contract/dist/transcript.js.map +1 -0
- package/node_modules/goah-ledger-contract/package.json +3 -4
- package/node_modules/goah-ledger-sqlite/dist/index.d.ts +32 -23
- package/node_modules/goah-ledger-sqlite/dist/index.d.ts.map +1 -1
- package/node_modules/goah-ledger-sqlite/dist/index.js +764 -394
- package/node_modules/goah-ledger-sqlite/dist/index.js.map +1 -1
- package/node_modules/goah-ledger-sqlite/package.json +2 -2
- package/node_modules/goah-runner-pi/dist/configurator.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/configurator.js +186 -30
- package/node_modules/goah-runner-pi/dist/configurator.js.map +1 -1
- package/node_modules/goah-runner-pi/dist/credential-store.d.ts +2 -0
- package/node_modules/goah-runner-pi/dist/credential-store.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/credential-store.js +117 -23
- package/node_modules/goah-runner-pi/dist/credential-store.js.map +1 -1
- package/node_modules/goah-runner-pi/dist/index.d.ts +18 -6
- package/node_modules/goah-runner-pi/dist/index.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/index.js +97 -12
- package/node_modules/goah-runner-pi/dist/index.js.map +1 -1
- package/node_modules/goah-runner-pi/dist/pi-worker.d.ts +6 -2
- package/node_modules/goah-runner-pi/dist/pi-worker.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/pi-worker.js +232 -70
- package/node_modules/goah-runner-pi/dist/pi-worker.js.map +1 -1
- package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts +2 -0
- package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts.map +1 -0
- package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js +3 -0
- package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js.map +1 -0
- package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts +2 -0
- package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts.map +1 -0
- package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js +5 -0
- package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js.map +1 -0
- package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts +2 -0
- package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts.map +1 -0
- package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js +5 -0
- package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js.map +1 -0
- package/node_modules/goah-runner-pi/dist/test-helpers.d.ts +1 -1
- package/node_modules/goah-runner-pi/dist/test-helpers.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/test-helpers.js +3 -1
- package/node_modules/goah-runner-pi/dist/test-helpers.js.map +1 -1
- package/node_modules/goah-runner-pi/dist/verification-worker.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/verification-worker.js +3 -5
- package/node_modules/goah-runner-pi/dist/verification-worker.js.map +1 -1
- package/node_modules/goah-runner-pi/package.json +2 -2
- package/node_modules/goah-supervisor/dist/context-view.d.ts +4 -5
- package/node_modules/goah-supervisor/dist/context-view.d.ts.map +1 -1
- package/node_modules/goah-supervisor/dist/context-view.js +7 -20
- package/node_modules/goah-supervisor/dist/context-view.js.map +1 -1
- package/node_modules/goah-supervisor/dist/index.d.ts +38 -36
- package/node_modules/goah-supervisor/dist/index.d.ts.map +1 -1
- package/node_modules/goah-supervisor/dist/index.js +762 -575
- package/node_modules/goah-supervisor/dist/index.js.map +1 -1
- package/node_modules/goah-supervisor/dist/roles.d.ts +2 -1
- package/node_modules/goah-supervisor/dist/roles.d.ts.map +1 -1
- package/node_modules/goah-supervisor/dist/roles.js +20 -5
- package/node_modules/goah-supervisor/dist/roles.js.map +1 -1
- package/node_modules/goah-supervisor/dist/verification.d.ts +8 -29
- package/node_modules/goah-supervisor/dist/verification.d.ts.map +1 -1
- package/node_modules/goah-supervisor/dist/verification.js +71 -46
- package/node_modules/goah-supervisor/dist/verification.js.map +1 -1
- package/node_modules/goah-supervisor/package.json +2 -2
- package/node_modules/goah-testkit/dist/faux-runner-worker.js +16 -9
- package/node_modules/goah-testkit/dist/faux-runner-worker.js.map +1 -1
- package/node_modules/goah-testkit/dist/index.d.ts +13 -16
- package/node_modules/goah-testkit/dist/index.d.ts.map +1 -1
- package/node_modules/goah-testkit/dist/index.js +27 -44
- package/node_modules/goah-testkit/dist/index.js.map +1 -1
- package/node_modules/goah-testkit/package.json +5 -5
- package/package.json +7 -8
- package/dist/console/assets/index-BJTGvlCH.js +0 -52
- package/dist/console/assets/index-BJTGvlCH.js.map +0 -1
- package/dist/console/assets/index-msBn41Cl.css +0 -1
- package/dist/inspect.d.ts +0 -61
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js +0 -110
- package/dist/inspect.js.map +0 -1
- package/dist/metrics.d.ts +0 -2
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js +0 -2
- package/dist/metrics.js.map +0 -1
- package/dist/session.d.ts +0 -2
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -2
- package/dist/session.js.map +0 -1
- package/node_modules/goah-ledger-contract/dist/metrics.d.ts +0 -40
- package/node_modules/goah-ledger-contract/dist/metrics.d.ts.map +0 -1
- package/node_modules/goah-ledger-contract/dist/metrics.js +0 -25
- package/node_modules/goah-ledger-contract/dist/metrics.js.map +0 -1
- package/node_modules/goah-ledger-contract/dist/session.d.ts +0 -63
- package/node_modules/goah-ledger-contract/dist/session.d.ts.map +0 -1
- package/node_modules/goah-ledger-contract/dist/session.js +0 -115
- package/node_modules/goah-ledger-contract/dist/session.js.map +0 -1
- package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts +0 -2
- package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts.map +0 -1
- package/node_modules/goah-testkit/dist/mock-connector-worker.js +0 -29
- package/node_modules/goah-testkit/dist/mock-connector-worker.js.map +0 -1
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { capabilityFor, controlStream, evaluateMetric, goalStream, interruptedSessionEvents, memoryStream, wakeStream, } from "goah-ledger-contract";
|
|
2
|
+
import { assertAgentHandoff, assertHandoff, assertTurnOutput, goalStream, goalRoute, goalExecutionBinding, goalTurnBinding, humanInboxRoute, humanRequestRoute, humanBinding, humanTurnBinding, specialistBinding, specialistTurnBinding, memoryStream, wakeStream, } from "goah-ledger-contract";
|
|
4
3
|
import { composeActiveContext, selectRecoveryEvents, selectWorkingMemory } from "./context-view.js";
|
|
5
|
-
import {
|
|
4
|
+
import { defaultTurnPrompt } from "./roles.js";
|
|
6
5
|
export { composeActiveContext, selectRecoveryEvents } from "./context-view.js";
|
|
7
6
|
/** Dispatches each wake to the runner selected by its opaque Runner Profile. */
|
|
8
7
|
export class RunnerRouter {
|
|
@@ -24,12 +23,13 @@ export class RunnerRouter {
|
|
|
24
23
|
const handle = runner.prepare(request);
|
|
25
24
|
if (handle.pid)
|
|
26
25
|
this.#byPid.set(handle.pid, runner);
|
|
27
|
-
void handle.result.
|
|
26
|
+
void handle.result.then(() => { if (handle.pid)
|
|
27
|
+
this.#byPid.delete(handle.pid); }, () => { if (handle.pid)
|
|
28
28
|
this.#byPid.delete(handle.pid); });
|
|
29
29
|
return handle;
|
|
30
30
|
}
|
|
31
|
-
async terminateProcess(pid) {
|
|
32
|
-
const runner = this.#byPid.get(pid);
|
|
31
|
+
async terminateProcess(pid, runnerProfileId) {
|
|
32
|
+
const runner = this.#byPid.get(pid) ?? this.runners.get(runnerProfileId ?? this.fallback);
|
|
33
33
|
if (!runner)
|
|
34
34
|
return;
|
|
35
35
|
await runner.terminateProcess(pid);
|
|
@@ -40,19 +40,17 @@ export class Supervisor {
|
|
|
40
40
|
clock;
|
|
41
41
|
#leaseMs;
|
|
42
42
|
#memoryTailChars;
|
|
43
|
-
#allowExternalActions;
|
|
44
|
-
#approvers;
|
|
45
|
-
#auditWriters;
|
|
46
43
|
#claimTail = Promise.resolve();
|
|
47
|
-
#
|
|
48
|
-
#metricCollectors = new Map();
|
|
49
|
-
#metricContracts = new Map();
|
|
50
|
-
#silence;
|
|
44
|
+
#humanTail = Promise.resolve();
|
|
51
45
|
#retryPolicy;
|
|
46
|
+
#turnRetryPolicy;
|
|
52
47
|
#profiles;
|
|
53
48
|
#runnerProfiles;
|
|
54
49
|
#handles = new Map();
|
|
55
|
-
#
|
|
50
|
+
#executions = new Map();
|
|
51
|
+
#agentExecutions = new Map();
|
|
52
|
+
#handoffValidations = new Map();
|
|
53
|
+
#handoffAttemptSeq = new Map();
|
|
56
54
|
#runner;
|
|
57
55
|
constructor(ledger, runner, clock, options = {}) {
|
|
58
56
|
this.ledger = ledger;
|
|
@@ -60,31 +58,25 @@ export class Supervisor {
|
|
|
60
58
|
this.#runner = runner;
|
|
61
59
|
this.#leaseMs = options.leaseMs ?? 30_000;
|
|
62
60
|
this.#memoryTailChars = options.memoryTailChars ?? 12_000;
|
|
63
|
-
this.#allowExternalActions = options.allowExternalActions ?? false;
|
|
64
|
-
this.#approvers = new Set(options.approvers ?? ["human", "ceo"]);
|
|
65
|
-
this.#auditWriters = new Set(options.auditWriters ?? ["verifier", "audit"]);
|
|
66
|
-
this.#silence = options.silence === null ? null : { maxSilentMs: options.silence?.maxSilentMs ?? 12 * 3_600_000, notify: options.silence?.notify ?? "ceo" };
|
|
67
61
|
this.#retryPolicy = options.retryPolicy ?? { maxAttempts: 0, baseDelayMs: 1_000 };
|
|
62
|
+
this.#turnRetryPolicy = options.turnRetryPolicy ?? { maxAttempts: 3, baseDelayMs: 1_000 };
|
|
63
|
+
for (const profile of options.profiles ?? []) {
|
|
64
|
+
if (profile.agent === "ceo" && profile.role !== "ceo" || profile.agent !== "ceo" && profile.role === "ceo")
|
|
65
|
+
throw new Error("exactly the ceo Agent must use the ceo role");
|
|
66
|
+
}
|
|
68
67
|
this.#profiles = new Map([["ceo", { agent: "ceo", role: "ceo" }], ...(options.profiles ?? []).map((profile) => [profile.agent, profile])]);
|
|
69
68
|
this.#runnerProfiles = new Map((options.runnerProfiles ?? []).map((profile) => [profile.id, profile]));
|
|
70
|
-
this.#verifyMetricsAfterWake = options.verifyMetricsAfterWake ?? false;
|
|
71
|
-
}
|
|
72
|
-
registerConnector(connector) { this.#connectors.set(connector.manifest.connector, connector); }
|
|
73
|
-
registerMetricContract(goalId, contract) { this.#metricContracts.set(goalId, contract); }
|
|
74
|
-
registerMetricCollector(goalId, contract, spec, intervalMs = 60_000) {
|
|
75
|
-
this.registerMetricContract(goalId, contract);
|
|
76
|
-
this.#metricCollectors.set(goalId, { goalId, contract, spec, intervalMs, nextAt: 0 });
|
|
77
69
|
}
|
|
78
70
|
/**
|
|
79
|
-
* Hot-swap the runner after a config reload. Refused while a
|
|
80
|
-
*
|
|
81
|
-
* The next
|
|
71
|
+
* Hot-swap the runner after a config reload. Refused while a Turn is active:
|
|
72
|
+
* a live child must never observe its runner vanish mid-flight.
|
|
73
|
+
* The next Turn uses the new runner, and spawn-time env
|
|
82
74
|
* resolution applies the new credentials to the next spawn.
|
|
83
75
|
*/
|
|
84
76
|
swapRunner(runner, profiles) {
|
|
85
|
-
const active = this.ledger.
|
|
77
|
+
const active = this.ledger.turns().filter((turn) => turn.status === "in_progress");
|
|
86
78
|
if (active.length > 0)
|
|
87
|
-
throw new Error("cannot swap runner while a
|
|
79
|
+
throw new Error("cannot swap runner while a Turn is in progress");
|
|
88
80
|
this.#runner = runner;
|
|
89
81
|
if (profiles) {
|
|
90
82
|
this.#runnerProfiles.clear();
|
|
@@ -93,48 +85,330 @@ export class Supervisor {
|
|
|
93
85
|
}
|
|
94
86
|
}
|
|
95
87
|
get runner() { return this.#runner; }
|
|
96
|
-
createGoal(goal, actor = "human") { this
|
|
97
|
-
|
|
88
|
+
createGoal(goal, actor = "human") { if (!this.#validGoalOwner(goal.owner, goal))
|
|
89
|
+
throw new Error("Goal owner role does not match Root/Child position"); if (goal.parentId === null && this.ledger.goals().some((candidate) => candidate.id !== goal.id && candidate.parentId === null && candidate.phase !== "complete"))
|
|
90
|
+
throw new Error("CEO already has an unfinished Root Goal"); this.ledger.putGoal(goal, actor); }
|
|
91
|
+
createRootGoal(objective, id = randomUUID(), sourceTurnId) {
|
|
98
92
|
if (!objective.trim())
|
|
99
93
|
throw new Error("root objective is required");
|
|
100
94
|
if (this.ledger.goals().some((goal) => goal.parentId === null && goal.owner === "ceo" && goal.phase !== "complete"))
|
|
101
95
|
throw new Error("CEO already has an unfinished Root Goal; use work_on_goal");
|
|
102
96
|
const goal = { id, parentId: null, objective, observationMethod: null, verificationMethod: null, owner: "ceo", phase: "active", revision: 0 };
|
|
103
|
-
this.ledger.putGoal(goal, "human");
|
|
97
|
+
this.ledger.putGoal(goal, "human", undefined, { operation: "create", reason: "Human established a durable Root Goal", evidence: [], authority: { kind: "human" }, ...(sourceTurnId ? { sourceTurnId } : {}) });
|
|
104
98
|
return this.#goal(id);
|
|
105
99
|
}
|
|
106
100
|
startGoal(objective, id = randomUUID()) {
|
|
107
101
|
const goal = this.createRootGoal(objective, id);
|
|
108
|
-
const wake = this.#enqueueTrigger("ceo", `root:${id}:created
|
|
102
|
+
const wake = this.#enqueueTrigger("ceo", `root:${id}:created`, { goalId: goal.id });
|
|
109
103
|
if (!wake)
|
|
110
104
|
throw new Error("CEO wake was not admitted for an active root goal");
|
|
111
105
|
return { goal, wake };
|
|
112
106
|
}
|
|
113
|
-
|
|
107
|
+
threadFor(agent = "ceo") {
|
|
108
|
+
const parentThreadId = agent === "ceo" ? null : this.threadFor("ceo").id;
|
|
109
|
+
const existing = this.ledger.threads().find((thread) => thread.agent === agent && thread.parentThreadId === parentThreadId);
|
|
110
|
+
if (existing)
|
|
111
|
+
return existing;
|
|
112
|
+
const now = this.#now();
|
|
113
|
+
const thread = { id: randomUUID(), agent, parentThreadId, createdAt: now, updatedAt: now };
|
|
114
|
+
this.ledger.putThread(thread, "supervisor");
|
|
115
|
+
return thread;
|
|
116
|
+
}
|
|
117
|
+
async startHumanTurn(message) {
|
|
118
|
+
const previous = this.#humanTail;
|
|
119
|
+
let release;
|
|
120
|
+
this.#humanTail = new Promise((resolve) => { release = resolve; });
|
|
121
|
+
await previous;
|
|
122
|
+
try {
|
|
123
|
+
return await this.#startHumanTurn(message);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
release();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async #startHumanTurn(message) {
|
|
114
130
|
if (!message.trim())
|
|
115
131
|
throw new Error("message is required");
|
|
116
|
-
const
|
|
117
|
-
this.ledger.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
const thread = this.threadFor("ceo");
|
|
133
|
+
const active = this.ledger.activeTurn(thread.id);
|
|
134
|
+
if (active) {
|
|
135
|
+
if (active.source !== "human") {
|
|
136
|
+
const goalId = active.goalId;
|
|
137
|
+
await this.interruptTurn(active.id);
|
|
138
|
+
if (goalId) {
|
|
139
|
+
const goal = this.ledger.goal(goalId);
|
|
140
|
+
if (goal?.phase === "active")
|
|
141
|
+
this.#enqueueTrigger(goal.owner, `human-interrupted:${active.id}`, { goalId: goal.id });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const handle = this.#handles.get(active.id);
|
|
146
|
+
if (handle && !handle.steer)
|
|
147
|
+
await this.interruptTurn(active.id);
|
|
148
|
+
else if (handle?.steer) {
|
|
149
|
+
const item = this.#appendTurnItem(active.id, "user_message", { text: message }, "human", randomUUID(), "in_progress");
|
|
150
|
+
try {
|
|
151
|
+
await handle.steer(message);
|
|
152
|
+
const stored = this.ledger.turnItems(active.id).find((candidate) => candidate.id === item.id);
|
|
153
|
+
if (this.ledger.turn(active.id)?.status === "in_progress" && stored?.status === "in_progress") {
|
|
154
|
+
this.ledger.putTurnItem({ ...stored, status: "completed", completedAt: this.#now() }, "human");
|
|
155
|
+
return { threadId: thread.id, turnId: active.id, steered: true };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
const stored = this.ledger.turnItems(active.id).find((candidate) => candidate.id === item.id);
|
|
160
|
+
if (this.ledger.turn(active.id)?.status === "in_progress" && stored?.status === "in_progress")
|
|
161
|
+
this.ledger.putTurnItem({ ...stored, status: "failed", completedAt: this.#now() }, "human");
|
|
162
|
+
if (this.ledger.turn(active.id)?.status === "in_progress")
|
|
163
|
+
await this.interruptTurn(active.id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.#appendTurnItem(active.id, "user_message", { text: message }, "human");
|
|
168
|
+
return { threadId: thread.id, turnId: active.id, steered: true };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await this.#awaitAgentExecution("ceo");
|
|
173
|
+
if (this.ledger.activeTurn(thread.id))
|
|
174
|
+
return this.#startHumanTurn(message);
|
|
175
|
+
const now = this.#now();
|
|
176
|
+
this.ledger.putThread({ ...thread, updatedAt: now }, "supervisor");
|
|
177
|
+
const leaseToken = randomUUID();
|
|
178
|
+
const turn = { id: randomUUID(), threadId: thread.id, source: "human", ...humanTurnBinding(), status: "in_progress", attempt: 1, error: null, startedAt: now, endedAt: null, leaseUntil: new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), leaseToken, runnerPid: null, runnerProfileId: this.#runnerProfileId("ceo") };
|
|
179
|
+
this.ledger.putTurn(turn, "human");
|
|
180
|
+
this.#appendTurnItem(turn.id, "user_message", { text: message }, "human");
|
|
181
|
+
const context = { source: { kind: "human" } };
|
|
182
|
+
const deliveredMail = this.#contextMail(humanBinding());
|
|
183
|
+
void this.#trackExecution(turn, "ceo", context, () => this.#humanContext(turn.id, "ceo", deliveredMail), null, deliveredMail.map((mail) => mail.id));
|
|
184
|
+
return { threadId: thread.id, turnId: turn.id, steered: false };
|
|
185
|
+
}
|
|
186
|
+
async interruptTurn(turnId) {
|
|
187
|
+
const turn = this.ledger.turn(turnId);
|
|
188
|
+
if (!turn || turn.status !== "in_progress")
|
|
189
|
+
throw new Error("active Turn not found");
|
|
190
|
+
this.#clearHandoffState(turn.id);
|
|
191
|
+
this.ledger.finishTurn(turn.id, "interrupted", { message: "interrupted by Human" }, this.#now(), "human");
|
|
192
|
+
const handle = this.#handles.get(turn.id);
|
|
193
|
+
try {
|
|
194
|
+
await handle?.terminate();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.#recordCleanupFailure(turn.id, error);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
this.#handles.delete(turn.id);
|
|
201
|
+
}
|
|
202
|
+
await this.#executions.get(turn.id);
|
|
203
|
+
return this.ledger.turn(turn.id);
|
|
204
|
+
}
|
|
205
|
+
async #executeTurn(initial, agent, turnContext, contextFactory, sourceWake = null, deliveredMailIds = [], recordRevisionAtStart = -1, sourceWakeTriggers = []) {
|
|
206
|
+
while (this.ledger.turn(initial.id)?.status === "in_progress") {
|
|
207
|
+
const execution = this.ledger.turn(initial.id);
|
|
208
|
+
const leaseToken = execution.leaseToken;
|
|
209
|
+
let handle = null;
|
|
210
|
+
let renewal = null;
|
|
211
|
+
try {
|
|
212
|
+
handle = this.runner.prepare({ agent, execution, ...(sourceWake ? { sourceWake, sourceWakeTriggers } : {}), turn: turnContext, context: contextFactory(), now: () => this.#now(), emit: (trace) => this.#recordTurnTrace(initial.id, leaseToken, trace.type, trace.data, agent), rpc: (method, params) => this.#agentRpcForTurn(initial.id, agent, turnContext, method, params, sourceWake?.id) });
|
|
213
|
+
this.#handles.set(initial.id, handle);
|
|
214
|
+
if (handle.pid)
|
|
215
|
+
this.ledger.attachTurnProcess(initial.id, leaseToken, handle.pid);
|
|
216
|
+
renewal = setInterval(() => { try {
|
|
217
|
+
this.ledger.renewTurnLease(initial.id, leaseToken, new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), this.#now());
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
void this.#terminateHandle(initial.id, handle);
|
|
221
|
+
} }, Math.max(25, Math.floor(this.#leaseMs / 3)));
|
|
222
|
+
renewal.unref();
|
|
223
|
+
handle.begin();
|
|
224
|
+
const result = await handle.result;
|
|
225
|
+
if (renewal)
|
|
226
|
+
clearInterval(renewal);
|
|
227
|
+
renewal = null;
|
|
228
|
+
await this.#terminateHandle(initial.id, handle);
|
|
229
|
+
this.#handles.delete(initial.id);
|
|
230
|
+
const current = this.ledger.turn(initial.id);
|
|
231
|
+
if (!current || current.status !== "in_progress")
|
|
232
|
+
return;
|
|
233
|
+
if (current.attempt > 1)
|
|
234
|
+
this.ledger.appendEvent({ streamId: `turn:${current.id}`, ts: this.#now(), actor: "supervisor", type: "turn.retry_finished", data: { attempt: current.attempt, outcome: result.outcome }, ignorable: true });
|
|
235
|
+
if (result.outcome !== "abnormal")
|
|
236
|
+
this.#closeStreamingItems(current.id);
|
|
237
|
+
if (result.outcome === "abnormal") {
|
|
238
|
+
if (current.attempt < this.#turnRetryPolicy.maxAttempts) {
|
|
239
|
+
this.ledger.repairTurnAttempt(current.id, result.reason, this.#now(), "supervisor");
|
|
240
|
+
this.ledger.appendEvent({ streamId: `turn:${current.id}`, ts: this.#now(), actor: "supervisor", type: "turn.retry_started", data: { attempt: current.attempt + 1, reason: result.reason }, ignorable: true });
|
|
241
|
+
this.ledger.putTurn({ ...current, attempt: current.attempt + 1, leaseUntil: new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), runnerPid: null }, "supervisor");
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, this.#turnRetryPolicy.baseDelayMs * 2 ** (current.attempt - 1)));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
this.#failTurn(current.id, result.reason);
|
|
246
|
+
this.#scheduleTurnRecovery(sourceWake, sourceWakeTriggers, current.id, agent);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (turnContext.goalBinding) {
|
|
250
|
+
if (result.outcome !== "handoff")
|
|
251
|
+
throw new Error("Goal-bound Turn requires Handoff");
|
|
252
|
+
this.#commitGoalTurn(current.id, agent, turnContext, result.output, sourceWake?.id ?? null, deliveredMailIds, recordRevisionAtStart);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (result.outcome !== "response")
|
|
256
|
+
throw new Error("ordinary Turn cannot finish with Handoff");
|
|
257
|
+
if (!this.ledger.turnItems(current.id).some((item) => item.type === "assistant_message" && item.data.text === result.response.content))
|
|
258
|
+
this.#appendTurnItem(current.id, "assistant_message", { text: result.response.content }, agent);
|
|
259
|
+
this.ledger.finishTurn(current.id, "completed", null, this.#now(), "supervisor", deliveredMailIds);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (renewal)
|
|
264
|
+
clearInterval(renewal);
|
|
265
|
+
if (handle)
|
|
266
|
+
await this.#terminateHandle(initial.id, handle);
|
|
267
|
+
this.#handles.delete(initial.id);
|
|
268
|
+
const current = this.ledger.turn(initial.id);
|
|
269
|
+
if (current?.status === "in_progress") {
|
|
270
|
+
this.#failTurn(current.id, error instanceof Error ? error.message : String(error));
|
|
271
|
+
this.#scheduleTurnRecovery(sourceWake, sourceWakeTriggers, current.id, agent);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
#trackExecution(initial, agent, turnContext, contextFactory, sourceWake = null, deliveredMailIds = [], recordRevisionAtStart = -1, sourceWakeTriggers = []) { const running = this.#executeTurn(initial, agent, turnContext, contextFactory, sourceWake, deliveredMailIds, recordRevisionAtStart, sourceWakeTriggers); this.#executions.set(initial.id, running); this.#agentExecutions.set(agent, running); void running.finally(() => { if (this.#executions.get(initial.id) === running)
|
|
278
|
+
this.#executions.delete(initial.id); if (this.#agentExecutions.get(agent) === running)
|
|
279
|
+
this.#agentExecutions.delete(agent); }); return running; }
|
|
280
|
+
async #awaitAgentExecution(agent) { const running = this.#agentExecutions.get(agent); if (running)
|
|
281
|
+
await running; }
|
|
282
|
+
#humanContext(turnId, agent, mail) {
|
|
283
|
+
const turn = this.ledger.turn(turnId);
|
|
284
|
+
const current = this.#turnHistory(turn.id);
|
|
285
|
+
const turnSourceSeqs = this.ledger.readStream(`turn:${turn.id}`).filter((event) => event.type === "item.user_message.started").map((event) => event.seq);
|
|
286
|
+
if (turn.bindingKind === "goal") {
|
|
287
|
+
const context = { source: { kind: "human" }, goalBinding: { goalId: turn.goalId, goalRevision: turn.goalRevision } };
|
|
288
|
+
const base = this.#loadContext({ ...goalExecutionBinding(agent, turn.goalId), attempt: turn.attempt }, context, mail, []);
|
|
289
|
+
const sourceSeqs = Array.isArray(base.sourceSeqs) ? base.sourceSeqs.filter((value) => typeof value === "number") : [];
|
|
290
|
+
return { ...base, text: `${String(base.text ?? "")}\n\n# Current Turn\n\n${current}`, sourceSeqs: [...new Set([...sourceSeqs, ...turnSourceSeqs])].sort((a, b) => a - b) };
|
|
291
|
+
}
|
|
292
|
+
const profile = this.#profiles.get(agent) ?? { agent, role: "ceo" };
|
|
293
|
+
const runnerProfile = this.#runnerProfiles.get(profile.runnerProfile ?? "default");
|
|
294
|
+
const recent = this.ledger.turns(turn.threadId).filter((candidate) => candidate.id !== turn.id && candidate.status === "completed").slice(-8).flatMap((candidate) => this.ledger.turnItems(candidate.id).filter((item) => item.type === "user_message" || item.type === "assistant_message").map((item) => `${item.type === "user_message" ? "Human" : "Assistant"}: ${String(item.data.text ?? "")}`));
|
|
295
|
+
const sourceSeqs = [...turnSourceSeqs, ...this.#mailSourceSeqs(mail)];
|
|
296
|
+
const incoming = mail.map((item) => `- [${item.level}] ${item.id} from ${item.from}: ${boundedJson(item.body)}`);
|
|
297
|
+
const context = { source: { kind: "human" } };
|
|
298
|
+
return { text: [...(recent.length ? [`# Recent conversation\n\n${recent.join("\n")}`] : []), ...(incoming.length ? [`# Incoming\n\n${incoming.join("\n")}`] : []), `# Current Turn\n\n${current}`].join("\n\n"), sourceSeqs, capabilities: profile.capabilities ?? defaultCapabilities(profile.role), systemPrompt: profile.systemPrompt ?? defaultTurnPrompt(profile.role, agent, context), ...(runnerProfile ? { runnerProfile } : {}) };
|
|
299
|
+
}
|
|
300
|
+
#goalContext(wake, turn, turnId, mail, wakeTriggers) { const base = this.#loadContext(wake, turn, mail, wakeTriggers); if (!base || typeof base !== "object" || Array.isArray(base))
|
|
301
|
+
return base; const value = base; const history = this.#turnHistory(turnId); return { ...value, ...(history ? { text: `${String(value.text ?? "")}\n\n# Current Turn retry history\n\n${history}` } : {}) }; }
|
|
302
|
+
#threadAgent(threadId) { const thread = this.ledger.thread(threadId); if (!thread)
|
|
303
|
+
throw new Error("Turn Thread not found"); return thread.agent; }
|
|
304
|
+
#turnHistory(turnId) { const items = this.ledger.turnItems(turnId).map((item) => { const data = item.data; if (item.type === "user_message")
|
|
305
|
+
return `Human: ${String(data.text ?? "")}`; if (item.type === "assistant_message")
|
|
306
|
+
return `Assistant: ${String(data.text ?? "")}`; if (item.type === "tool_call")
|
|
307
|
+
return `Tool call: ${String(data.tool ?? "")} ${JSON.stringify(item.data)}`; if (item.type === "tool_result")
|
|
308
|
+
return `Tool result: ${JSON.stringify(data.result ?? item.data)}`; return `${item.type}: ${JSON.stringify(item.data)}`; }); const facts = this.ledger.readStream(`turn:${turnId}`).filter((event) => event.type === "turn.retry_started").map((event) => `${event.type}: ${JSON.stringify(event.data)}`); return [...items, ...facts].join("\n"); }
|
|
309
|
+
#failTurn(turnId, reason) { const current = this.ledger.turn(turnId); if (!current || current.status !== "in_progress")
|
|
310
|
+
return; this.#clearHandoffState(turnId); this.ledger.finishTurn(turnId, "failed", { message: reason }, this.#now(), "supervisor"); }
|
|
311
|
+
#preemptGoalTurn(goalId, reason) { const turn = this.ledger.turns().find((candidate) => candidate.status === "in_progress" && candidate.bindingKind === "goal" && candidate.goalId === goalId); if (!turn)
|
|
312
|
+
return; this.#clearHandoffState(turn.id); this.ledger.finishTurn(turn.id, "interrupted", { message: reason }, this.#now(), "supervisor"); const handle = this.#handles.get(turn.id); if (handle)
|
|
313
|
+
void this.#terminateHandle(turn.id, handle).finally(() => this.#handles.delete(turn.id)); }
|
|
314
|
+
async #terminateHandle(turnId, handle) { try {
|
|
315
|
+
await handle.terminate();
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
this.#recordCleanupFailure(turnId, error);
|
|
319
|
+
} }
|
|
320
|
+
#recordCleanupFailure(turnId, error) { this.ledger.appendEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor: "supervisor", type: "runner.cleanup_failed", data: { message: error instanceof Error ? error.message : String(error) }, ignorable: true }); }
|
|
321
|
+
#scheduleTurnRecovery(sourceWake, triggers, turnId, agent) { if (!sourceWake)
|
|
322
|
+
return; const turn = this.ledger.turn(turnId); if (!turn)
|
|
323
|
+
return; const role = this.#role(agent); const goal = turn.bindingKind === "goal" ? this.ledger.goal(turn.goalId) : null; if (turn.bindingKind === "human" || turn.bindingKind === "goal" && (!goal || goal.phase !== "active" || goal.owner !== agent) || turn.bindingKind === "specialist" && role !== turn.specialistRole)
|
|
324
|
+
return; const prior = Math.max(0, ...triggers.map((trigger) => parseRecoveryTrigger(trigger.triggerRef)?.attempt ?? 0)); const next = prior + 1; if (next < this.#retryPolicy.maxAttempts) {
|
|
325
|
+
const delay = this.#retryPolicy.baseDelayMs * 2 ** prior;
|
|
326
|
+
const binding = turn.bindingKind === "goal" ? goalExecutionBinding(agent, turn.goalId) : specialistBinding(agent, turn.specialistRole);
|
|
327
|
+
this.ledger.putSchedule({ id: recoveryScheduleId(turnId, next), ...binding, nextWakeAt: new Date(this.clock.now().getTime() + delay).toISOString(), reason: recoveryRef(turnId), setBy: "supervisor", status: "pending", resolvedAt: null }, "supervisor", sourceWake.id);
|
|
328
|
+
}
|
|
329
|
+
else if (role === "child" && goal?.parentId) {
|
|
330
|
+
const parent = this.ledger.goal(goal.parentId);
|
|
331
|
+
if (parent?.phase === "active")
|
|
332
|
+
this.#enqueueTrigger(parent.owner, childRetryExhaustedRef(turnId), { goalId: parent.id });
|
|
333
|
+
} }
|
|
334
|
+
#closeStreamingItems(turnId) { const open = this.ledger.turnItems(turnId).filter((item) => item.status === "in_progress"); if (open.some((item) => item.type === "tool_call" || item.type === "user_message"))
|
|
335
|
+
throw new Error("Runner returned while an input or tool call was still open"); for (const item of open)
|
|
336
|
+
this.ledger.putTurnItem({ ...item, status: "completed", completedAt: this.#now() }, "supervisor"); }
|
|
337
|
+
#appendTurnItem(turnId, type, data, actor, id = randomUUID(), status = "completed") {
|
|
338
|
+
const now = this.#now();
|
|
339
|
+
const item = { id, turnId, ordinal: this.ledger.turnItems(turnId).length + 1, type, status, data, createdAt: now, completedAt: status === "in_progress" ? null : now };
|
|
340
|
+
this.ledger.putTurnItem(item, actor);
|
|
341
|
+
return item;
|
|
342
|
+
}
|
|
343
|
+
#recordTurnTrace(turnId, leaseToken, type, data, actor = "ceo") {
|
|
344
|
+
if (type === "transcript.completed" || type === "transcript.interrupted")
|
|
345
|
+
return;
|
|
346
|
+
if (type === "transcript.started" && this.ledger.readStream(`turn:${turnId}`).some((event) => event.type === "transcript.started"))
|
|
347
|
+
return;
|
|
348
|
+
this.ledger.appendTurnEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor, type, data }, leaseToken);
|
|
349
|
+
if (type === "message.assistant.completed") {
|
|
350
|
+
const value = data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
351
|
+
if (!["ordinary", "handoff"].includes(String(value.completionIntent)) || value.commitState !== (value.completionIntent === "handoff" ? "provisional" : "committed"))
|
|
352
|
+
throw new Error("Runner assistant completion is missing normalized intent/state");
|
|
353
|
+
const text = messageTextContent(value.message?.content);
|
|
354
|
+
if (text)
|
|
355
|
+
this.#appendTurnItem(turnId, "assistant_message", { text }, actor);
|
|
356
|
+
}
|
|
357
|
+
else if (type === "plan.updated")
|
|
358
|
+
this.#appendTurnItem(turnId, "plan", data, actor);
|
|
359
|
+
else if (type === "message.assistant.delta") {
|
|
360
|
+
const input = data;
|
|
361
|
+
const delta = input.delta;
|
|
362
|
+
if (delta?.type === "thinking_start" || delta?.type === "thinking_delta" || delta?.type === "thinking_end") {
|
|
363
|
+
const turn = this.ledger.turn(turnId);
|
|
364
|
+
const id = `${turnId}:attempt:${turn.attempt}:reasoning:${String(input.messageId ?? "message")}`;
|
|
365
|
+
const existing = this.ledger.turnItems(turnId).find((item) => item.id === id);
|
|
366
|
+
const text = `${String(existing?.data?.text ?? "")}${delta.type === "thinking_delta" ? String(delta.delta ?? "") : ""}`;
|
|
367
|
+
if (!existing)
|
|
368
|
+
this.#appendTurnItem(turnId, "reasoning", { text }, actor, id, delta.type === "thinking_end" ? "completed" : "in_progress");
|
|
369
|
+
else
|
|
370
|
+
this.ledger.putTurnItem({ ...existing, data: { text }, status: delta.type === "thinking_end" ? "completed" : "in_progress", completedAt: delta.type === "thinking_end" ? this.#now() : null }, actor);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (type === "tool.called") {
|
|
374
|
+
const input = data;
|
|
375
|
+
const turn = this.ledger.turn(turnId);
|
|
376
|
+
const id = `${turnId}:attempt:${turn.attempt}:tool:${String(input.callId)}`;
|
|
377
|
+
if (this.ledger.turnItems(turnId).some((item) => item.id === id))
|
|
378
|
+
throw new Error("duplicate tool call id in Turn attempt");
|
|
379
|
+
this.#appendTurnItem(turnId, "tool_call", { callId: String(input.callId), tool: String(input.name), arguments: input.arguments ?? null }, actor, id, "in_progress");
|
|
380
|
+
}
|
|
381
|
+
else if (type === "tool.completed") {
|
|
382
|
+
const input = data;
|
|
383
|
+
const turn = this.ledger.turn(turnId);
|
|
384
|
+
const id = `${turnId}:attempt:${turn.attempt}:tool:${String(input.callId)}`;
|
|
385
|
+
const existing = this.ledger.turnItems(turnId).find((item) => item.id === id);
|
|
386
|
+
if (!existing)
|
|
387
|
+
throw new Error("tool result has no matching call in Turn attempt");
|
|
388
|
+
this.ledger.putTurnItem({ ...existing, status: input.isError ? "failed" : "completed", completedAt: this.#now() }, actor);
|
|
389
|
+
this.#appendTurnItem(turnId, "tool_result", { callId: String(input.callId), result: input.result ?? null }, actor);
|
|
390
|
+
}
|
|
122
391
|
}
|
|
123
392
|
sendToCeo(body, level = "decision") {
|
|
124
|
-
const mail = { id: randomUUID(), to: "ceo", from: "human", level, body, readAt: null };
|
|
393
|
+
const mail = { id: randomUUID(), to: "ceo", from: "human", level, ...humanInboxRoute(), body, readAt: null };
|
|
125
394
|
this.ledger.putMail(mail, "human");
|
|
126
395
|
const wake = this.#enqueueTrigger("ceo", `mail:${mail.id}`);
|
|
127
396
|
if (!wake)
|
|
128
397
|
throw new Error("CEO wake was not admitted");
|
|
129
398
|
return { mail, wake };
|
|
130
399
|
}
|
|
131
|
-
delegate(request, actor = "ceo", wakeId) {
|
|
132
|
-
|
|
400
|
+
delegate(request, actor = "ceo", wakeId) { const candidate = { ...request.childGoal, parentId: request.parentGoalId, phase: "active", revision: 0 }; if (!this.#validGoalOwner(candidate.owner, candidate))
|
|
401
|
+
throw new Error("Child Goal owner must use a Child Agent profile"); return this.ledger.commitDelegation(request, actor, wakeId); }
|
|
402
|
+
async reassignGoal(request, actor = "ceo", wakeId) { const goal = this.#goal(request.goalId); if (!this.#validGoalOwner(request.newOwner, { ...goal, owner: request.newOwner }))
|
|
403
|
+
throw new Error("Child Goal owner must use a Child Agent profile"); const thread = this.ledger.threads().find((candidate) => candidate.agent === goal.owner); const active = thread ? this.ledger.activeTurn(thread.id) : null; if (active?.bindingKind === "goal" && active.goalId === goal.id)
|
|
404
|
+
await this.interruptTurn(active.id); return this.ledger.commitReassignment(request, actor, wakeId); }
|
|
133
405
|
teamList(now = this.#now()) { return deriveTeam(this.ledger, now); }
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
406
|
+
agentRole(agent) { return this.#role(agent); }
|
|
407
|
+
updateGoal(id, patch, actor = "human", change) {
|
|
408
|
+
if (patch.objective === undefined && patch.observationMethod === undefined && patch.verificationMethod === undefined)
|
|
409
|
+
throw new Error("goal update requires objective, observation method, or verification method");
|
|
137
410
|
const current = this.#goal(id);
|
|
411
|
+
this.#assertGoalMutationAuthority(current, actor);
|
|
138
412
|
if (patch.objective !== undefined && patch.objective !== current.objective && current.parentId !== null && (patch.observationMethod === undefined || patch.verificationMethod === undefined))
|
|
139
413
|
throw new Error("child objective revision requires replacement observation and verification methods");
|
|
140
414
|
const next = {
|
|
@@ -144,18 +418,19 @@ export class Supervisor {
|
|
|
144
418
|
...(patch.objective !== undefined && patch.objective !== current.objective && current.parentId === null && patch.verificationMethod === undefined ? { verificationMethod: null } : {}),
|
|
145
419
|
revision: current.revision + 1,
|
|
146
420
|
};
|
|
147
|
-
this.
|
|
421
|
+
this.#preemptGoalTurn(current.id, "Goal definition changed");
|
|
422
|
+
this.ledger.putGoal(next, actor, change?.sourceWakeId, { operation: "revise", reason: change?.reason ?? "Goal definition revised", evidence: change?.evidence ?? [], ...(change?.sourceTurnId ? { sourceTurnId: change.sourceTurnId } : {}), ...(change?.sourceWakeId ? { sourceWakeId: change.sourceWakeId } : {}) });
|
|
148
423
|
if (next.parentId === null && next.owner === "ceo" && actor === "human")
|
|
149
|
-
this.#enqueueTrigger("ceo", `root:${id}:revised:${next.revision}
|
|
424
|
+
this.#enqueueTrigger("ceo", `root:${id}:revised:${next.revision}`, { goalId: next.id });
|
|
150
425
|
return next;
|
|
151
426
|
}
|
|
152
427
|
confirmObservationMethod(id, observationMethod) {
|
|
153
428
|
const current = this.#goal(id);
|
|
154
429
|
if (current.parentId !== null)
|
|
155
430
|
throw new Error("human confirmation applies only to a root goal");
|
|
156
|
-
return this.updateGoal(id, { observationMethod, verificationMethod: observationMethod }, "human");
|
|
431
|
+
return this.updateGoal(id, { observationMethod, verificationMethod: observationMethod }, "human", { reason: "Human confirmed the Root observation and verification method", evidence: [] });
|
|
157
432
|
}
|
|
158
|
-
reviseChildGoal(id, objective, observationMethod, verificationMethod, actor, reason, evidence, wakeId) {
|
|
433
|
+
reviseChildGoal(id, objective, observationMethod, verificationMethod, actor, reason, evidence, wakeId, sourceTurnId) {
|
|
159
434
|
const current = this.#goal(id);
|
|
160
435
|
if (current.parentId === null)
|
|
161
436
|
throw new Error("CEO cannot revise a root goal");
|
|
@@ -165,143 +440,119 @@ export class Supervisor {
|
|
|
165
440
|
if (!this.ledger.eventsSince(seq - 1).some((event) => event.seq === seq))
|
|
166
441
|
throw new Error(`evidence event does not exist: ${seq}`);
|
|
167
442
|
this.ledger.appendEvent({ streamId: wakeId ? wakeStream(wakeId) : goalStream(id), ts: this.#now(), actor, type: "goal.revision_requested", data: { goalId: id, fromRevision: current.revision, objective, observationMethod, verificationMethod, reason, evidence } });
|
|
168
|
-
return this.updateGoal(id, { objective, observationMethod, verificationMethod }, actor);
|
|
443
|
+
return this.updateGoal(id, { objective, observationMethod, verificationMethod }, actor, { reason, evidence, ...(wakeId ? { sourceWakeId: wakeId } : {}), ...(sourceTurnId ? { sourceTurnId } : {}) });
|
|
169
444
|
}
|
|
170
445
|
completeGoal(request, actor = "human", wakeId) {
|
|
171
446
|
const goal = this.ledger.completeGoal(request, actor, wakeId);
|
|
172
|
-
this.#
|
|
173
|
-
if (goal.parentId && actor !== "ceo")
|
|
174
|
-
this.#enqueueTrigger("ceo", `goal:${goal.id}:complete:${goal.revision}`);
|
|
447
|
+
this.#preemptGoalTurn(goal.id, "Goal completed");
|
|
175
448
|
return goal;
|
|
176
449
|
}
|
|
177
|
-
transitionGoal(id, phase, actor = "human") {
|
|
450
|
+
transitionGoal(id, phase, actor = "human", scheduleMotion = true, sourceTurnId) {
|
|
178
451
|
const current = this.#goal(id);
|
|
452
|
+
this.#assertGoalMutationAuthority(current, actor);
|
|
179
453
|
if (current.phase === phase)
|
|
180
454
|
return current;
|
|
181
455
|
if (phase === "complete")
|
|
182
456
|
throw new Error("goal completion requires reason and evidence");
|
|
183
457
|
const next = { ...current, phase, revision: current.revision + 1 };
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (phase === "active")
|
|
188
|
-
this.#enqueueTrigger(next.owner, `${next.parentId ? "goal" : "root"}:${id}:resumed:${next.revision}
|
|
189
|
-
if (next.parentId && actor !== "ceo" && phase === "blocked")
|
|
190
|
-
this.#enqueueTrigger("ceo", `goal:${id}:${phase}:${next.revision}`);
|
|
458
|
+
const operation = phase === "paused" ? "pause" : phase === "blocked" ? "block" : "resume";
|
|
459
|
+
this.#preemptGoalTurn(current.id, `Goal ${operation}`);
|
|
460
|
+
this.ledger.putGoal(next, actor, undefined, { operation, reason: `Goal ${operation} requested by ${actor}`, evidence: [], ...(sourceTurnId ? { sourceTurnId } : {}) });
|
|
461
|
+
if (phase === "active" && scheduleMotion)
|
|
462
|
+
this.#enqueueTrigger(next.owner, `${next.parentId ? "goal" : "root"}:${id}:resumed:${next.revision}`, { goalId: next.id });
|
|
191
463
|
return next;
|
|
192
464
|
}
|
|
193
|
-
planWake(agent, at, reason, setBy = agent) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
465
|
+
planWake(agent, at, reason, setBy = agent, target) {
|
|
466
|
+
const parsed = Date.parse(at);
|
|
467
|
+
if (!Number.isFinite(parsed))
|
|
468
|
+
throw new Error("schedule time is invalid");
|
|
469
|
+
const nextWakeAt = new Date(parsed).toISOString();
|
|
470
|
+
const binding = target ?? this.#singleActiveGoalTarget(agent);
|
|
471
|
+
const role = this.#role(agent);
|
|
472
|
+
if (!binding && role !== "verifier" && role !== "audit")
|
|
473
|
+
throw new Error("Goal-owning Agent schedules require an active Goal target");
|
|
474
|
+
if (binding) {
|
|
475
|
+
const goal = this.ledger.goal(binding.goalId);
|
|
476
|
+
if (!goal || goal.phase !== "active" || !this.#validGoalOwner(agent, goal))
|
|
477
|
+
throw new Error("schedule target is not an active Goal owned by this Agent");
|
|
205
478
|
}
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await this.runner.terminateProcess(wake.runnerPid);
|
|
211
|
-
return this.#wake(wake.id);
|
|
479
|
+
const executionTarget = binding ? goalExecutionBinding(agent, binding.goalId) : specialistBinding(agent, role);
|
|
480
|
+
const schedule = { id: `schedule:${agent}${binding ? `:${binding.goalId}` : ""}:${randomUUID()}`, ...executionTarget, nextWakeAt, reason, setBy, status: "pending", resolvedAt: null };
|
|
481
|
+
this.ledger.putSchedule(schedule, setBy);
|
|
482
|
+
return nextWakeAt <= this.#now() ? this.#enqueueSchedule(schedule) : null;
|
|
212
483
|
}
|
|
484
|
+
async stopAgentWake(agent, goalId) { const role = this.#role(agent); if (goalId) {
|
|
485
|
+
const goal = this.#goal(goalId);
|
|
486
|
+
if (!this.#validGoalOwner(agent, goal))
|
|
487
|
+
throw new Error("Goal stop target is not owned by the Agent");
|
|
488
|
+
} const binding = role === "verifier" || role === "audit" ? null : goalId ? { goalId } : this.#singleActiveGoalTarget(agent); if (role !== "verifier" && role !== "audit" && !binding)
|
|
489
|
+
throw new Error("Goal stop requires an explicit Goal target"); const wake = this.ledger.wakes().find((item) => item.agent === agent && (item.status === "queued" || item.status === "claimed") && (binding ? item.bindingKind === "goal" && item.goalId === binding.goalId : item.bindingKind === "specialist")); if (wake)
|
|
490
|
+
this.ledger.cancelWake(wake.id, this.#now()); const thread = this.ledger.threads().find((item) => item.agent === agent); const turn = thread ? this.ledger.activeTurn(thread.id) : null; if (turn && (binding ? turn.bindingKind === "goal" && turn.goalId === binding.goalId : turn.bindingKind === "specialist"))
|
|
491
|
+
await this.interruptTurn(turn.id); return wake ? this.#wake(wake.id) : null; }
|
|
213
492
|
async recover() {
|
|
214
|
-
this.ledger.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
493
|
+
for (const wake of this.ledger.wakes().filter((candidate) => candidate.status === "claimed"))
|
|
494
|
+
this.ledger.releaseWake(wake.id, this.#now());
|
|
495
|
+
for (const turn of this.ledger.turns().filter((candidate) => candidate.status === "in_progress" && !this.#handles.has(candidate.id))) {
|
|
496
|
+
if (turn.runnerPid)
|
|
497
|
+
await this.runner.terminateProcess(turn.runnerPid, turn.runnerProfileId);
|
|
498
|
+
this.#failTurn(turn.id, "orphaned Runner ownership recovered after Supervisor restart");
|
|
499
|
+
const thread = this.ledger.thread(turn.threadId);
|
|
500
|
+
if (!thread)
|
|
501
|
+
continue;
|
|
502
|
+
if (turn.bindingKind === "goal") {
|
|
503
|
+
const goal = this.ledger.goal(turn.goalId);
|
|
504
|
+
if (goal?.phase === "active" && goal.owner === thread.agent)
|
|
505
|
+
this.#enqueueTrigger(thread.agent, recoveryRef(turn.id), { goalId: goal.id });
|
|
506
|
+
}
|
|
507
|
+
else if (turn.bindingKind === "specialist" && this.#role(thread.agent) === turn.specialistRole)
|
|
508
|
+
this.#enqueueTrigger(thread.agent, recoveryRef(turn.id));
|
|
219
509
|
}
|
|
220
510
|
}
|
|
221
511
|
async tick() {
|
|
222
|
-
const
|
|
223
|
-
const wake = claimed?.wake ?? null;
|
|
512
|
+
const wake = await this.#claimNextWake();
|
|
224
513
|
if (!wake)
|
|
225
514
|
return null;
|
|
226
|
-
|
|
227
|
-
let running = wake;
|
|
228
|
-
let handle = null;
|
|
229
|
-
let renewal;
|
|
515
|
+
let createdTurnId = null;
|
|
230
516
|
try {
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
now: () => this.#now(),
|
|
241
|
-
emit: (trace) => this.ledger.appendRunnerEvent({ streamId: wakeStream(running.id), ts: this.#now(), actor: running.agent, type: trace.type, data: trace.data }, leaseToken),
|
|
242
|
-
rpc: (method, params) => this.#agentRpc(running, turn, leaseToken, method, params),
|
|
243
|
-
});
|
|
244
|
-
this.#handles.set(running.id, handle);
|
|
245
|
-
if (handle.pid)
|
|
246
|
-
running = this.ledger.attachWakeProcess(running.id, leaseToken, handle.pid, this.#now());
|
|
247
|
-
renewal = setInterval(() => {
|
|
248
|
-
try {
|
|
249
|
-
const now = this.clock.now();
|
|
250
|
-
this.ledger.renewWakeLease(running.id, leaseToken, new Date(now.getTime() + this.#leaseMs).toISOString(), now.toISOString());
|
|
517
|
+
await this.#awaitAgentExecution(wake.agent);
|
|
518
|
+
const currentWake = this.#wake(wake.id);
|
|
519
|
+
if (currentWake.status !== "claimed")
|
|
520
|
+
return currentWake;
|
|
521
|
+
if (currentWake.bindingKind === "goal") {
|
|
522
|
+
const goal = this.ledger.goal(currentWake.goalId);
|
|
523
|
+
if (!goal || goal.owner !== currentWake.agent || goal.phase !== "active" || !this.#validGoalOwner(currentWake.agent, goal)) {
|
|
524
|
+
this.ledger.cancelWake(currentWake.id, this.#now());
|
|
525
|
+
return this.#wake(currentWake.id);
|
|
251
526
|
}
|
|
252
|
-
catch {
|
|
253
|
-
void handle?.terminate();
|
|
254
|
-
}
|
|
255
|
-
}, Math.max(25, Math.floor(this.#leaseMs / 3)));
|
|
256
|
-
renewal.unref();
|
|
257
|
-
handle.begin();
|
|
258
|
-
const result = await handle.result;
|
|
259
|
-
clearInterval(renewal);
|
|
260
|
-
renewal = undefined;
|
|
261
|
-
await handle.terminate();
|
|
262
|
-
this.#handles.delete(running.id);
|
|
263
|
-
if (result.outcome === "abnormal") {
|
|
264
|
-
await this.#markAbnormal(running, result.reason);
|
|
265
|
-
return this.#wake(running.id);
|
|
266
|
-
}
|
|
267
|
-
if (result.outcome === "response") {
|
|
268
|
-
if (turn.goalBinding)
|
|
269
|
-
throw new Error("a Goal-bound Turn must finish with a handoff");
|
|
270
|
-
const mailId = interactionMailId(running);
|
|
271
|
-
if (!mailId)
|
|
272
|
-
throw new Error("an ordinary response requires an interaction mail");
|
|
273
|
-
this.ledger.commitInteraction({ agent: running.agent, wakeId: running.id, mailId, ts: this.#now(), response: result.response });
|
|
274
|
-
this.ledger.finishWake(running.id, "done", this.#now());
|
|
275
|
-
return this.#wake(running.id);
|
|
276
|
-
}
|
|
277
|
-
if (turn.goalBinding)
|
|
278
|
-
this.#validateGoalTurnRecord(running, turn, workRecordRevisionAtStart);
|
|
279
|
-
const output = turn.goalBinding ? this.#goalWakeOutput(result.output, turn.goalBinding) : result.output;
|
|
280
|
-
this.#validateCeoHandoff(running, output);
|
|
281
|
-
const outgoingMail = output.mail.map((draft) => ({ id: randomUUID(), to: draft.to, from: running.agent, level: draft.level, body: draft.body, readAt: null }));
|
|
282
|
-
const schedule = output.nextWakeAt
|
|
283
|
-
? { id: `schedule:${running.agent}`, agent: running.agent, nextWakeAt: output.nextWakeAt, reason: "handoff.next_steps", setBy: running.agent }
|
|
284
|
-
: null;
|
|
285
|
-
const handoffEvent = this.ledger.commitHandoff({ agent: running.agent, wakeId: running.id, ts: this.#now(), output, outgoingMail, schedule });
|
|
286
|
-
this.ledger.finishWake(running.id, "done", this.#now());
|
|
287
|
-
if (this.#role(running.agent) !== "ceo" && handoffTriggersCeo(output.handoff) && this.#hasActiveRoot()) {
|
|
288
|
-
this.#enqueueTrigger("ceo", `child-handoff:${handoffEvent.seq}`);
|
|
289
527
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
528
|
+
const wakeTriggers = this.ledger.wakeTriggers(currentWake.id).filter((trigger) => trigger.status === "pending");
|
|
529
|
+
const turnContext = this.#turnContext(currentWake, wakeTriggers);
|
|
530
|
+
if (!this.#validExecution(currentWake.agent, turnContext)) {
|
|
531
|
+
this.ledger.cancelWake(currentWake.id, this.#now());
|
|
532
|
+
return this.#wake(currentWake.id);
|
|
294
533
|
}
|
|
295
|
-
|
|
534
|
+
const thread = this.threadFor(currentWake.agent);
|
|
535
|
+
const now = this.#now();
|
|
536
|
+
const leaseToken = randomUUID();
|
|
537
|
+
const turnBinding = currentWake.bindingKind === "goal" ? goalTurnBinding(currentWake.goalId, turnContext.goalBinding.goalRevision) : currentWake.bindingKind === "human" ? humanTurnBinding() : specialistTurnBinding(currentWake.specialistRole);
|
|
538
|
+
const execution = { id: randomUUID(), threadId: thread.id, source: turnContext.source.kind, ...turnBinding, status: "in_progress", attempt: 1, error: null, startedAt: now, endedAt: null, leaseUntil: new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), leaseToken, runnerPid: null, runnerProfileId: this.#runnerProfileId(currentWake.agent) };
|
|
539
|
+
this.ledger.putThread({ ...thread, updatedAt: this.#now() }, "supervisor");
|
|
540
|
+
this.ledger.startTurnFromWake(currentWake.id, execution, now);
|
|
541
|
+
createdTurnId = execution.id;
|
|
542
|
+
const deliveredMail = this.#contextMail(currentWake);
|
|
543
|
+
const deliveredMailIds = deliveredMail.map((mail) => mail.id);
|
|
544
|
+
this.ledger.appendEvent({ streamId: `turn:${execution.id}`, ts: now, actor: "supervisor", type: "run.admitted", data: { ...turnContext, wakeTriggers }, ignorable: true });
|
|
545
|
+
const revision = turnContext.goalBinding ? this.ledger.workRecord(turnContext.goalBinding.goalId)?.recordRevision ?? -1 : -1;
|
|
546
|
+
await this.#trackExecution(execution, currentWake.agent, turnContext, () => this.#goalContext(currentWake, turnContext, execution.id, deliveredMail, wakeTriggers), currentWake, deliveredMailIds, revision, wakeTriggers);
|
|
547
|
+
return this.#wake(wake.id);
|
|
296
548
|
}
|
|
297
549
|
catch (error) {
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return this.#wake(running.id);
|
|
550
|
+
if (createdTurnId && this.ledger.turn(createdTurnId)?.status === "in_progress")
|
|
551
|
+
this.#failTurn(createdTurnId, error instanceof Error ? error.message : String(error));
|
|
552
|
+
const current = this.ledger.wake(wake.id);
|
|
553
|
+
if (current?.status === "claimed")
|
|
554
|
+
this.ledger.releaseWake(wake.id, this.#now());
|
|
555
|
+
return this.#wake(wake.id);
|
|
305
556
|
}
|
|
306
557
|
}
|
|
307
558
|
async #claimNextWake() {
|
|
@@ -312,15 +563,24 @@ export class Supervisor {
|
|
|
312
563
|
try {
|
|
313
564
|
for (const schedule of this.ledger.dueSchedules(this.#now()))
|
|
314
565
|
this.#enqueueSchedule(schedule);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
566
|
+
for (const mail of this.ledger.triggeringMail()) {
|
|
567
|
+
const thread = this.ledger.threads().find((candidate) => candidate.agent === mail.to);
|
|
568
|
+
if (thread && this.ledger.activeTurn(thread.id))
|
|
569
|
+
continue;
|
|
570
|
+
const goal = mail.routeKind === "goal" ? this.ledger.goal(mail.goalId) : null;
|
|
571
|
+
const target = goal && goal.owner === mail.to && goal.phase === "active" && this.#validGoalOwner(mail.to, goal) ? { goalId: goal.id } : undefined;
|
|
572
|
+
const humanInteraction = mail.routeKind === "human_inbox" && mail.to === "ceo" && mail.from === "human";
|
|
573
|
+
const specialistInteraction = mail.routeKind === "specialist_inbox" && this.#role(mail.to) === mail.specialistRole;
|
|
574
|
+
if (!target && !humanInteraction && !specialistInteraction)
|
|
575
|
+
continue;
|
|
576
|
+
const base = `mail:${mail.id}`;
|
|
577
|
+
const related = this.ledger.wakeTriggersForAgent(mail.to).map((trigger) => ({ trigger, attempt: mailDeliveryAttempt(trigger.triggerRef, base) })).filter((entry) => entry.attempt !== null);
|
|
578
|
+
const pending = related.filter((entry) => entry.trigger.status === "pending").sort((a, b) => b.attempt - a.attempt)[0];
|
|
579
|
+
const next = related.reduce((highest, entry) => Math.max(highest, entry.attempt), -1) + 1;
|
|
580
|
+
const trigger = pending?.trigger.triggerRef ?? (next === 0 ? base : `${base}@redelivery:${next}`);
|
|
581
|
+
this.#enqueueTrigger(mail.to, trigger, target);
|
|
582
|
+
}
|
|
583
|
+
return this.ledger.claimNextWake(this.#now());
|
|
324
584
|
}
|
|
325
585
|
finally {
|
|
326
586
|
release();
|
|
@@ -336,129 +596,55 @@ export class Supervisor {
|
|
|
336
596
|
return completed;
|
|
337
597
|
}
|
|
338
598
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.ledger.requestAction(requested, action.agent, wakeId);
|
|
348
|
-
if (gated || !connector)
|
|
349
|
-
return this.#action(action.id);
|
|
350
|
-
this.ledger.approveAction(action.id, "supervisor", "declared reversible dry-run capability", action.evidence);
|
|
351
|
-
return this.#dispatch(action.id);
|
|
352
|
-
}
|
|
353
|
-
async approveAction(id, approver, reason, evidence) {
|
|
354
|
-
if (!this.#approvers.has(approver))
|
|
355
|
-
throw new Error("actor is not authorized to approve actions");
|
|
356
|
-
this.ledger.approveAction(id, approver, reason, evidence);
|
|
357
|
-
return this.#dispatch(id);
|
|
358
|
-
}
|
|
359
|
-
rejectAction(id, approver, reason, evidence) {
|
|
360
|
-
if (!this.#approvers.has(approver))
|
|
361
|
-
throw new Error("actor is not authorized to reject actions");
|
|
362
|
-
return this.ledger.rejectAction(id, approver, reason, evidence);
|
|
363
|
-
}
|
|
364
|
-
putAuditAdvice(id, advice, wakeId) {
|
|
365
|
-
const role = this.#profiles.get(advice.by)?.role;
|
|
366
|
-
if (!this.#auditWriters.has(advice.by) && role !== "verifier" && role !== "audit")
|
|
367
|
-
throw new Error("actor is not authorized to write audit advice");
|
|
368
|
-
return this.ledger.putAuditAdvice(id, advice, wakeId);
|
|
369
|
-
}
|
|
370
|
-
ackAuditAdvice(id, agent) { return this.ledger.ackAuditAdvice(id, agent); }
|
|
371
|
-
recordMetric(sample) {
|
|
372
|
-
const goal = this.ledger.goal(sample.goalId);
|
|
373
|
-
if (!goal)
|
|
374
|
-
throw new Error(`metric goal not found: ${sample.goalId}`);
|
|
375
|
-
const contract = this.#metricContracts.get(goal.id);
|
|
376
|
-
if (!contract)
|
|
377
|
-
throw new Error(`metric contract is not registered: ${goal.id}`);
|
|
378
|
-
if (contract.source !== sample.source)
|
|
379
|
-
throw new Error("metric source does not match registered contract");
|
|
380
|
-
this.ledger.appendEvent({ streamId: `metric:${goal.id}`, ts: this.#now(), actor: "supervisor", type: "metric.sampled", data: sample });
|
|
381
|
-
const evaluation = evaluateMetric(contract, this.ledger.metricSamples(goal.id), this.#now());
|
|
382
|
-
this.ledger.appendEvent({ streamId: `metric:${goal.id}`, ts: this.#now(), actor: "supervisor", type: "metric.evaluated", data: evaluation });
|
|
383
|
-
if (evaluation.shouldWakeOwner)
|
|
384
|
-
this.#enqueueTrigger(goal.owner, `metric:${goal.id}:${sample.observedAt}`);
|
|
385
|
-
return evaluation;
|
|
386
|
-
}
|
|
387
|
-
async collectMetricNow(goalId) {
|
|
388
|
-
const registration = this.#metricCollectors.get(goalId);
|
|
389
|
-
if (!registration)
|
|
390
|
-
throw new Error(`metric collector is not registered: ${goalId}`);
|
|
391
|
-
registration.nextAt = this.clock.now().getTime() + registration.intervalMs;
|
|
392
|
-
const sample = await runJsonProcess(registration.spec, { goalId });
|
|
393
|
-
return this.recordMetric({ ...sample, goalId });
|
|
394
|
-
}
|
|
395
|
-
async reconcileAction(id) {
|
|
396
|
-
const action = this.#action(id);
|
|
397
|
-
if (action.status !== "unknown")
|
|
398
|
-
throw new Error("only unknown actions may be reconciled");
|
|
399
|
-
const connector = this.#requiredConnector(action.connector);
|
|
400
|
-
const capability = capabilityFor(connector.manifest, action.kind);
|
|
401
|
-
if (!capability || capability.query === "none")
|
|
402
|
-
throw new Error("connector cannot reconcile this action");
|
|
403
|
-
const result = await runConnector(connector, "query", action);
|
|
404
|
-
if (result.status === "pending")
|
|
405
|
-
return action;
|
|
406
|
-
return this.ledger.transitionAction(id, result.status, { reconciledAt: this.#now(), ...(result.externalRef ? { externalRef: result.externalRef } : {}) });
|
|
407
|
-
}
|
|
408
|
-
async retryUnknownAction(id) {
|
|
409
|
-
const action = this.#action(id);
|
|
410
|
-
if (action.status !== "unknown")
|
|
411
|
-
throw new Error("only unknown actions may be retried");
|
|
412
|
-
const connector = this.#requiredConnector(action.connector);
|
|
413
|
-
const capability = capabilityFor(connector.manifest, action.kind);
|
|
414
|
-
if (!capability?.nativeIdempotency || !capability.automaticRetry)
|
|
415
|
-
throw new Error("connector manifest forbids automatic retry");
|
|
416
|
-
return this.#dispatch(id);
|
|
417
|
-
}
|
|
418
|
-
async #dispatch(id) {
|
|
419
|
-
const current = this.#action(id);
|
|
420
|
-
const connector = this.#requiredConnector(current.connector);
|
|
421
|
-
const dispatching = this.ledger.transitionAction(id, "dispatching");
|
|
422
|
-
const result = await runConnector(connector, "dispatch", dispatching);
|
|
423
|
-
return this.ledger.transitionAction(id, result.status, result.externalRef ? { externalRef: result.externalRef } : {});
|
|
424
|
-
}
|
|
425
|
-
async #markAbnormal(wake, reason) {
|
|
426
|
-
const current = this.#wake(wake.id);
|
|
427
|
-
if (current.runnerPid)
|
|
428
|
-
await this.runner.terminateProcess(current.runnerPid);
|
|
429
|
-
this.ledger.appendEvent({ streamId: wakeStream(current.id), ts: this.#now(), actor: current.agent, type: "wake.abnormal_reason", data: { reason } });
|
|
430
|
-
const repairs = interruptedSessionEvents(this.ledger.eventsForWake(current.id), this.#now(), "supervisor");
|
|
431
|
-
if (repairs.length)
|
|
432
|
-
this.ledger.appendEvents(repairs);
|
|
433
|
-
if (["leased", "running", "queued"].includes(current.status)) {
|
|
434
|
-
this.ledger.finishWake(current.id, "abnormal", this.#now());
|
|
435
|
-
if (current.attempt < this.#retryPolicy.maxAttempts) {
|
|
436
|
-
const delay = this.#retryPolicy.baseDelayMs * 2 ** Math.max(0, current.attempt - 1);
|
|
437
|
-
const schedule = { id: `retry:${current.id}`, agent: current.agent, nextWakeAt: new Date(this.clock.now().getTime() + delay).toISOString(), reason: `recovery:${current.id}`, setBy: "supervisor" };
|
|
438
|
-
this.ledger.putSchedule(schedule, "supervisor", current.id);
|
|
439
|
-
}
|
|
440
|
-
else if (current.agent !== "ceo" && this.#hasActiveRoot()) {
|
|
441
|
-
this.#enqueueTrigger("ceo", `child-retry-exhausted:${current.id}`);
|
|
599
|
+
#enqueueSchedule(schedule) {
|
|
600
|
+
if (schedule.status !== "pending")
|
|
601
|
+
return null;
|
|
602
|
+
if (schedule.bindingKind === "goal") {
|
|
603
|
+
const goal = this.ledger.goal(schedule.goalId);
|
|
604
|
+
if (!goal || goal.owner !== schedule.agent || goal.phase !== "active" || !this.#validGoalOwner(schedule.agent, goal)) {
|
|
605
|
+
this.ledger.supersedeSchedule(schedule.id, this.#now());
|
|
606
|
+
return null;
|
|
442
607
|
}
|
|
443
608
|
}
|
|
609
|
+
else if (!["verifier", "audit"].includes(this.#role(schedule.agent))) {
|
|
610
|
+
this.ledger.cancelSchedule(schedule.id, this.#now());
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
const wake = { id: `wake:${schedule.id}`, ...(schedule.bindingKind === "goal" ? goalExecutionBinding(schedule.agent, schedule.goalId) : specialistBinding(schedule.agent, schedule.specialistRole)), triggerRef: `${schedule.id}@${schedule.nextWakeAt}`, status: "queued", attempt: 0, enqueuedSeq: 0, claimedAt: null, consumedAt: null, turnId: null };
|
|
614
|
+
return this.ledger.consumeSchedule(schedule.id, wake, this.#now()).wake;
|
|
444
615
|
}
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
616
|
+
#enqueueTrigger(agent, triggerRef, target) {
|
|
617
|
+
let binding;
|
|
618
|
+
if (target) {
|
|
619
|
+
const goal = this.ledger.goal(target.goalId);
|
|
620
|
+
if (!goal || goal.owner !== agent || goal.phase !== "active" || !this.#validGoalOwner(agent, goal))
|
|
621
|
+
return null;
|
|
622
|
+
binding = goalExecutionBinding(agent, target.goalId);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const mailId = mailIdFromTriggerRef(triggerRef);
|
|
626
|
+
const mail = mailId ? this.ledger.mailbox().find((candidate) => candidate.id === mailId) : null;
|
|
627
|
+
const role = this.#role(agent);
|
|
628
|
+
if (agent === "ceo" && role === "ceo" && mail?.routeKind === "human_inbox" && mail.from === "human")
|
|
629
|
+
binding = humanBinding();
|
|
630
|
+
else if (role === "verifier" || role === "audit")
|
|
631
|
+
binding = specialistBinding(agent, role);
|
|
632
|
+
else
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
const sameBinding = (wake) => wake.bindingKind === binding.bindingKind && wake.goalId === binding.goalId && wake.specialistRole === binding.specialistRole;
|
|
449
636
|
const exact = this.ledger.wakeByTrigger(agent, triggerRef);
|
|
450
|
-
if (exact)
|
|
637
|
+
if (exact) {
|
|
638
|
+
if (!sameBinding(exact))
|
|
639
|
+
throw new Error("Wake trigger was reused with a different execution binding");
|
|
451
640
|
return exact;
|
|
452
|
-
const ownsLiveGoal = this.ledger.goalsForOwner(agent).some((goal) => goal.phase === "active" || goal.phase === "blocked");
|
|
453
|
-
const ceoInterrupt = agent === "ceo" && (triggerRef.startsWith("interaction:") || triggerRef.startsWith("mail:") || triggerRef.startsWith("child-"));
|
|
454
|
-
if (!ownsLiveGoal && !ceoInterrupt)
|
|
455
|
-
return null;
|
|
456
|
-
const queued = triggerRef.startsWith("interaction:") ? null : this.ledger.queuedWakeForAgent(agent);
|
|
457
|
-
if (queued) {
|
|
458
|
-
this.ledger.appendEvent({ streamId: wakeStream(queued.id), ts: this.#now(), actor: "supervisor", type: "wake.trigger_coalesced", data: { wakeId: queued.id, triggerRef } });
|
|
459
|
-
return queued;
|
|
460
641
|
}
|
|
461
|
-
const
|
|
642
|
+
const pending = this.ledger.wakes().find((wake) => wake.agent === agent && wake.status === "claimed" && sameBinding(wake)) ?? this.ledger.wakes().find((wake) => wake.agent === agent && wake.status === "queued" && sameBinding(wake));
|
|
643
|
+
if (pending) {
|
|
644
|
+
this.ledger.addWakeTrigger(pending.id, triggerRef, "supervisor");
|
|
645
|
+
return pending;
|
|
646
|
+
}
|
|
647
|
+
const wake = { id: randomUUID(), ...binding, triggerRef, status: "queued", attempt: 0, enqueuedSeq: 0, claimedAt: null, consumedAt: null, turnId: null };
|
|
462
648
|
const result = this.ledger.enqueueWake(wake, "supervisor");
|
|
463
649
|
if (result.created)
|
|
464
650
|
return this.#wake(wake.id);
|
|
@@ -467,303 +653,270 @@ export class Supervisor {
|
|
|
467
653
|
throw new Error("deduplicated wake is missing");
|
|
468
654
|
return existing;
|
|
469
655
|
}
|
|
470
|
-
#suppressQueuedWake(agent, reason) {
|
|
471
|
-
const wake = this.ledger.queuedWakeForAgent(agent);
|
|
472
|
-
if (!wake)
|
|
473
|
-
return;
|
|
474
|
-
this.ledger.appendEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: "supervisor", type: "wake.suppressed", data: { reason } });
|
|
475
|
-
this.ledger.finishWake(wake.id, "abnormal", this.#now());
|
|
476
|
-
}
|
|
477
656
|
#hasActiveRoot() { return this.ledger.goals().some((goal) => goal.parentId === null && goal.owner === "ceo" && goal.phase === "active"); }
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
reason: missingObservationGoalIds.length ? "active child goal has no observation method" : idle.length ? "active child goal has no liveness route" : "active root has no motion, review, wait, blocker, or human request",
|
|
496
|
-
};
|
|
497
|
-
this.ledger.appendEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: "supervisor", type: "ceo.motion_invalid", data: violation });
|
|
498
|
-
throw new Error(`CEO motion invalid: ${violation.reason}${violation.idleAgents.length ? ` (${violation.idleAgents.join(", ")})` : ""}`);
|
|
499
|
-
}
|
|
500
|
-
#validateGoalTurnRecord(wake, turn, revisionAtStart) {
|
|
501
|
-
const binding = turn.goalBinding;
|
|
502
|
-
const goal = this.#goal(binding.goalId);
|
|
503
|
-
if (goal.revision !== binding.goalRevision)
|
|
504
|
-
throw new Error("Goal revision changed during the Turn");
|
|
505
|
-
const record = this.ledger.workRecord(goal.id);
|
|
506
|
-
if (!record || record.recordRevision <= revisionAtStart || record.updatedInTurn !== wake.id || record.goalRevision !== goal.revision)
|
|
507
|
-
throw new Error("Goal-bound Turn must update its Work Record before handoff");
|
|
508
|
-
}
|
|
509
|
-
#goalWakeOutput(output, binding) {
|
|
657
|
+
#activeRoot() { return this.ledger.goals().find((goal) => goal.parentId === null && goal.owner === "ceo" && goal.phase === "active") ?? null; }
|
|
658
|
+
#singleActiveGoalTarget(agent) { const goals = this.ledger.goalsForOwner(agent).filter((goal) => goal.phase === "active" && this.#validGoalOwner(agent, goal)); if (goals.length > 1)
|
|
659
|
+
throw new Error("Wake requires an explicit Goal target when an Agent owns multiple active Goals"); return goals.length === 1 ? { goalId: goals[0].id } : undefined; }
|
|
660
|
+
#knownAgent(agent) { return this.#profiles.has(agent) || this.ledger.threads().some((thread) => thread.agent === agent) || this.ledger.goals().some((goal) => goal.owner === agent); }
|
|
661
|
+
#role(agent) { return this.#profiles.get(agent)?.role ?? (agent === "ceo" ? "ceo" : "child"); }
|
|
662
|
+
#validGoalOwner(agent, goal) { const role = this.#role(agent); return goal.owner === agent && (role === "ceo" ? agent === "ceo" && goal.parentId === null : role === "child" && goal.parentId !== null); }
|
|
663
|
+
#validExecution(agent, turn) { const role = this.#role(agent); if (turn.goalBinding) {
|
|
664
|
+
const goal = this.ledger.goal(turn.goalBinding.goalId);
|
|
665
|
+
return Boolean(goal && this.#validGoalOwner(agent, goal));
|
|
666
|
+
} if (turn.source.kind === "human")
|
|
667
|
+
return agent === "ceo" && role === "ceo"; return role === "verifier" || role === "audit"; }
|
|
668
|
+
#commitGoalTurn(turnId, agent, turn, raw, sourceWakeId, mailIds, revisionAtStart) { const binding = turn.goalBinding; const goal = this.#goal(binding.goalId); if (goal.revision !== binding.goalRevision)
|
|
669
|
+
throw new Error("Goal revision changed during the Turn"); const record = this.ledger.workRecord(goal.id); if (!record || record.recordRevision <= revisionAtStart || record.updatedInTurn !== turnId || record.goalRevision !== goal.revision)
|
|
670
|
+
throw new Error("Goal-bound Turn must update its Work Record before handoff"); assertTurnOutput(raw); const validation = this.#handoffValidations.get(raw.validationToken); const execution = this.ledger.turn(turnId); if (!validation || !execution || validation.turnId !== turnId || validation.attemptId !== raw.validationAttemptId || this.#handoffAttemptSeq.get(turnId) !== raw.validationAttemptId || validation.agent !== agent || validation.attempt !== execution.attempt || validation.leaseToken !== execution.leaseToken || validation.goalId !== goal.id || validation.goalRevision !== goal.revision || validation.handoff.outcome !== raw.handoff.outcome || !isSameNumbers(validation.handoff.evidence, raw.handoff.evidence))
|
|
671
|
+
throw new Error("Handoff validation token is stale or does not match this Turn"); const responseItem = [...this.ledger.turnItems(turnId)].reverse().find((item) => item.type === "assistant_message" && item.status === "completed" && item.data.text === validation.message); if (!responseItem)
|
|
672
|
+
throw new Error("accepted Handoff response was not recorded in the Turn"); const output = this.#goalTurnOutput(raw, binding); assertHandoff(output.handoff); const now = this.#now(); const item = { id: randomUUID(), turnId, ordinal: this.ledger.turnItems(turnId).length + 1, type: "handoff", status: "completed", data: output.handoff, createdAt: now, completedAt: now }; this.ledger.commitHandoff({ agent, turnId, sourceWakeId, mailIds, ts: now, output, responseItemId: responseItem.id, item }); this.#clearHandoffState(turnId); }
|
|
673
|
+
#goalTurnOutput(output, binding) {
|
|
510
674
|
const record = this.ledger.workRecord(binding.goalId);
|
|
511
675
|
if (!record)
|
|
512
676
|
throw new Error("Goal Work Record is missing");
|
|
513
|
-
const legacy = "goalId" in output.handoff ? null : output.handoff;
|
|
514
677
|
const handoff = {
|
|
515
678
|
goalId: binding.goalId,
|
|
516
679
|
goalRevision: binding.goalRevision,
|
|
517
680
|
recordRevision: record.recordRevision,
|
|
518
|
-
outcome:
|
|
519
|
-
evidence:
|
|
681
|
+
outcome: output.handoff.outcome,
|
|
682
|
+
evidence: output.handoff.evidence,
|
|
520
683
|
};
|
|
521
|
-
return {
|
|
684
|
+
return { handoff };
|
|
522
685
|
}
|
|
523
|
-
#turnContext(wake) {
|
|
524
|
-
if (
|
|
686
|
+
#turnContext(wake, triggers = this.ledger.wakeTriggers(wake.id).filter((trigger) => trigger.status === "pending")) {
|
|
687
|
+
if (wake.bindingKind === "human")
|
|
525
688
|
return { source: { kind: "human" } };
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
return { source:
|
|
689
|
+
if (wake.bindingKind === "specialist")
|
|
690
|
+
return { source: { kind: "system", reason: triggers.map((trigger) => trigger.triggerRef).join(", ") } };
|
|
691
|
+
const goal = this.#goal(wake.goalId);
|
|
692
|
+
return { source: { kind: "goal", round: (this.ledger.workRecord(goal.id)?.recordRevision ?? 0) + 1 }, goalBinding: { goalId: goal.id, goalRevision: goal.revision } };
|
|
530
693
|
}
|
|
531
|
-
#loadContext(wake, turn) {
|
|
694
|
+
#loadContext(wake, turn, mail, wakeTriggers) {
|
|
532
695
|
const profile = this.#profiles.get(wake.agent) ?? { agent: wake.agent, role: "child" };
|
|
533
696
|
const role = profile.role;
|
|
534
697
|
const capabilities = profile.capabilities ?? defaultCapabilities(role);
|
|
535
698
|
const runnerProfile = this.#runnerProfiles.get(profile.runnerProfile ?? "default");
|
|
536
|
-
if (!turn.goalBinding && interactionMailId(wake)) {
|
|
537
|
-
const mail = this.ledger.mailbox().find((candidate) => candidate.id === interactionMailId(wake));
|
|
538
|
-
const body = mail?.body && typeof mail.body === "object" && !Array.isArray(mail.body) ? mail.body : {};
|
|
539
|
-
const message = typeof body.message === "string" ? body.message : "";
|
|
540
|
-
const mailEvent = this.ledger.eventsSince(0, ["mail.put"]).findLast((event) => event.data.snapshot?.id === mail?.id);
|
|
541
|
-
const recent = this.ledger.eventsSince(0, ["interaction.completed"]).filter((event) => event.actor === wake.agent).slice(-8).map((event) => {
|
|
542
|
-
const data = event.data;
|
|
543
|
-
const prior = this.ledger.mailbox().find((candidate) => candidate.id === data.mailId);
|
|
544
|
-
const priorBody = prior?.body && typeof prior.body === "object" && !Array.isArray(prior.body) ? prior.body : {};
|
|
545
|
-
return `Human: ${typeof priorBody.message === "string" ? priorBody.message : ""}\nAssistant: ${data.response?.content ?? ""}`;
|
|
546
|
-
});
|
|
547
|
-
return {
|
|
548
|
-
text: [...(recent.length ? [`# Recent conversation\n\n${recent.join("\n\n")}`] : []), `# Human message\n\n${message}`].join("\n\n"),
|
|
549
|
-
sourceSeqs: mailEvent ? [mailEvent.seq] : [],
|
|
550
|
-
capabilities,
|
|
551
|
-
systemPrompt: profile.systemPrompt ?? "You are Goah's primary Agent. Respond naturally to the Human, use tools when useful, and keep the final answer concise. Do not create or operate a Goal unless the Human expresses durable Goal intent.",
|
|
552
|
-
...(runnerProfile ? { runnerProfile } : {}),
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
699
|
const goals = role === "ceo" ? this.ledger.goals() : this.ledger.goalsForOwner(wake.agent);
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
? wake.triggerRef.slice("recovery:".length)
|
|
560
|
-
: wake.triggerRef.startsWith("retry:") ? wake.triggerRef.slice("retry:".length).split("@")[0] : null;
|
|
561
|
-
const recoveryEvents = recoveryId ? selectRecoveryEvents(this.ledger.eventsForWake(recoveryId)) : [];
|
|
700
|
+
const handoff = turn.goalBinding ? this.ledger.lastGoalHandoff(turn.goalBinding.goalId) : null;
|
|
701
|
+
const recoveryIds = [...new Set(wakeTriggers.flatMap((trigger) => { const parsed = parseRecoveryTrigger(trigger.triggerRef); const exhausted = childRetryExhaustedTurnId(trigger.triggerRef); return parsed ? [parsed.turnId] : exhausted ? [exhausted] : []; }))];
|
|
702
|
+
const recoveryEvents = recoveryIds.flatMap((recoveryId) => selectRecoveryEvents(this.ledger.turn(recoveryId) ? this.ledger.readStream(`turn:${recoveryId}`) : this.ledger.eventsForWake(recoveryId)));
|
|
562
703
|
const teamHandoffs = role === "ceo"
|
|
563
704
|
? [...this.ledger.eventsSince(0, ["handoff.recorded"])].reverse().filter((event, index, all) => all.findIndex((candidate) => candidate.actor === event.actor) === index)
|
|
564
705
|
: [];
|
|
565
|
-
const actions = this.ledger.actions().filter((action) => action.agent === wake.agent && (action.status === "unknown" || Boolean(action.auditAdvice && !action.adviceAcked)));
|
|
566
|
-
const revisionWarnings = goals.flatMap((goal) => this.#goalRevisionWarning(goal));
|
|
567
706
|
const workingMemory = selectWorkingMemory(this.ledger.readStream(memoryStream(wake.agent)), this.#memoryTailChars);
|
|
568
|
-
const active = composeActiveContext({ role, capabilities, systemPrompt: profile.systemPrompt ??
|
|
707
|
+
const active = composeActiveContext({ role, capabilities, systemPrompt: profile.systemPrompt ?? defaultTurnPrompt(role, wake.agent, turn), wake, wakeTriggers, goals, mail, lastHandoff: handoff, teamHandoffs, team: role === "ceo" ? this.teamList() : [], recoveryEvents, workingMemory });
|
|
569
708
|
const records = this.ledger.workRecords();
|
|
570
709
|
const currentRecord = turn.goalBinding ? this.ledger.workRecord(turn.goalBinding.goalId) : null;
|
|
571
710
|
const currentGoal = turn.goalBinding ? this.ledger.goal(turn.goalBinding.goalId) : null;
|
|
572
711
|
const parentRecord = currentGoal?.parentId ? this.ledger.workRecord(currentGoal.parentId) : null;
|
|
573
712
|
const recordIndex = records.map((record) => `- /goals/${record.goalId}.md · r${record.recordRevision} · ${this.ledger.goal(record.goalId)?.owner ?? "unknown"} · ${this.ledger.goal(record.goalId)?.phase ?? "unknown"}`);
|
|
574
713
|
const workText = [`# Shared Work Record Index\n\n${recordIndex.join("\n")}`, ...(currentRecord ? [`# Your Work Record\n\n${currentRecord.content}`] : []), ...(parentRecord ? [`# Parent Work Record\n\n${parentRecord.content}`] : [])].join("\n\n");
|
|
575
|
-
return { ...active, text: `${active.text}\n\n${workText}`, sourceSeqs: [...new Set([...active.sourceSeqs, ...records.map((record) => record.lastEventSeq)])].sort((a, b) => a - b), workRecord: currentRecord, sharedWorkRecords: records, ...(runnerProfile ? { runnerProfile } : {}) };
|
|
714
|
+
return { ...active, text: `${active.text}\n\n${workText}`, sourceSeqs: [...new Set([...active.sourceSeqs, ...this.#mailSourceSeqs(mail), ...records.map((record) => record.lastEventSeq)])].sort((a, b) => a - b), workRecord: currentRecord, sharedWorkRecords: records, ...(runnerProfile ? { runnerProfile } : {}) };
|
|
576
715
|
}
|
|
577
|
-
#requiredConnector(name) { const value = this.#connectors.get(name); if (!value)
|
|
578
|
-
throw new Error(`connector not registered: ${name}`); return value; }
|
|
579
716
|
#wake(id) { const value = this.ledger.wake(id); if (!value)
|
|
580
717
|
throw new Error(`wake not found: ${id}`); return value; }
|
|
581
|
-
#action(id) { const value = this.ledger.action(id); if (!value)
|
|
582
|
-
throw new Error(`action not found: ${id}`); return value; }
|
|
583
718
|
#goal(id) { const value = this.ledger.goal(id); if (!value)
|
|
584
719
|
throw new Error(`goal not found: ${id}`); return value; }
|
|
585
|
-
#
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const rootSeq = this.ledger.readStream(goalStream(root.id)).filter((event) => event.type === "goal.put").at(-1)?.seq ?? 0;
|
|
592
|
-
const goalSeq = this.ledger.readStream(goalStream(goal.id)).filter((event) => event.type === "goal.put").at(-1)?.seq ?? 0;
|
|
593
|
-
return rootSeq > goalSeq ? [`Goal ${goal.id} predates root revision ${root.revision}; CEO must revise its objective/observation method before new gated actions.`] : [];
|
|
594
|
-
}
|
|
595
|
-
#assertAgentGoalsCurrent(agent) {
|
|
596
|
-
const warnings = this.ledger.goalsForOwner(agent).flatMap((goal) => this.#goalRevisionWarning(goal));
|
|
597
|
-
if (warnings.length)
|
|
598
|
-
throw new Error(warnings.join(" "));
|
|
599
|
-
}
|
|
720
|
+
#assertGoalMutationAuthority(goal, actor) { if (goal.parentId === null) {
|
|
721
|
+
if (actor !== "human")
|
|
722
|
+
throw new Error("only human may modify a root goal");
|
|
723
|
+
return;
|
|
724
|
+
} const parent = this.ledger.goal(goal.parentId); if (parent?.owner !== actor)
|
|
725
|
+
throw new Error("only the parent goal owner may modify a child goal"); }
|
|
600
726
|
#now() { return this.clock.now().toISOString(); }
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const
|
|
727
|
+
#futureTime(value, after) { const parsed = Date.parse(value); if (!Number.isFinite(parsed) || parsed <= Date.parse(after))
|
|
728
|
+
throw new Error("nextWakeAt must be later than the Turn start"); return new Date(parsed).toISOString(); }
|
|
729
|
+
#runnerProfileId(agent) { return this.#profiles.get(agent)?.runnerProfile ?? "default"; }
|
|
730
|
+
#contextMail(target) { const selected = []; let used = 0; const matches = (mail) => target.bindingKind === "goal" ? mail.routeKind === "goal" && mail.goalId === target.goalId : target.bindingKind === "human" ? mail.routeKind === "human_inbox" : mail.routeKind === "specialist_inbox" && mail.specialistRole === target.specialistRole; for (const mail of this.ledger.unreadMail(target.agent).filter(matches)) {
|
|
731
|
+
const cost = Math.min(JSON.stringify(mail.body).length, 2_000) + 128;
|
|
732
|
+
if (selected.length >= 20 || selected.length > 0 && used + cost > this.#memoryTailChars)
|
|
733
|
+
break;
|
|
734
|
+
selected.push(mail);
|
|
735
|
+
used += cost;
|
|
736
|
+
} return selected; }
|
|
737
|
+
#mailSourceSeqs(mail) { const ids = new Set(mail.map((item) => item.id)); if (!ids.size)
|
|
738
|
+
return []; return this.ledger.eventsSince(0, ["mail.put"]).filter((event) => { const data = event.data && typeof event.data === "object" && !Array.isArray(event.data) ? event.data : {}; return typeof data.snapshot?.id === "string" && ids.has(data.snapshot.id); }).map((event) => event.seq); }
|
|
739
|
+
async #agentRpcForTurn(turnId, agent, context, method, params, sourceWakeId) {
|
|
740
|
+
const handoffAttemptId = method === "goal.handoff.validate" ? this.#beginHandoffAttempt(turnId) : null;
|
|
741
|
+
const execution = this.ledger.turn(turnId);
|
|
742
|
+
if (!execution || execution.status !== "in_progress" || !execution.leaseToken || !execution.leaseUntil || execution.leaseUntil < this.#now())
|
|
743
|
+
throw new Error("stale Turn RPC rejected");
|
|
744
|
+
if (context.goalBinding) {
|
|
745
|
+
const goal = this.ledger.goal(context.goalBinding.goalId);
|
|
746
|
+
const thread = this.ledger.thread(execution.threadId);
|
|
747
|
+
if (!goal || goal.phase !== "active" || goal.revision !== context.goalBinding.goalRevision || goal.owner !== agent || execution.bindingKind !== "goal" || execution.goalId !== goal.id || execution.goalRevision !== goal.revision || thread?.agent !== agent)
|
|
748
|
+
throw new Error("stale Goal-bound Turn RPC rejected");
|
|
749
|
+
}
|
|
750
|
+
const input = asRecord(params);
|
|
751
|
+
if (method === "goal.handoff.validate") {
|
|
752
|
+
const candidate = String(input.candidateMessage ?? "");
|
|
753
|
+
this.ledger.appendEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor: agent, type: "rpc.goal.handoff.validate", data: { attemptId: handoffAttemptId, handoff: input.handoff ?? null, candidateMessagePresent: Boolean(candidate.trim()), candidateMessageChars: candidate.length }, ignorable: true });
|
|
754
|
+
return this.#validateHandoffDraft(turnId, handoffAttemptId, agent, execution, context, input);
|
|
755
|
+
}
|
|
756
|
+
const profile = this.#profiles.get(agent) ?? { agent, role: "child" };
|
|
606
757
|
const allowed = new Set(profile.capabilities ?? defaultCapabilities(profile.role));
|
|
607
758
|
if (!allowed.has(method))
|
|
608
759
|
throw new Error(`${profile.role} agent is not allowed to call ${method}`);
|
|
609
|
-
if (!
|
|
760
|
+
if (!context.goalBinding && goalBoundCapability(method))
|
|
610
761
|
throw new Error(`${method} requires a Goal-bound Turn`);
|
|
611
|
-
this.ledger.
|
|
612
|
-
const input = asRecord(params);
|
|
762
|
+
this.ledger.appendEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor: agent, type: `rpc.${method}`, data: params, ignorable: true });
|
|
613
763
|
if (method === "ledger.search")
|
|
614
764
|
return this.ledger.searchEvents(String(input.query), Number(input.limit ?? 20));
|
|
615
765
|
if (method === "team.list")
|
|
616
766
|
return this.teamList();
|
|
617
767
|
if (method === "goal.get")
|
|
618
|
-
return (
|
|
768
|
+
return (context.goalBinding ? this.ledger.goal(context.goalBinding.goalId) : null);
|
|
619
769
|
if (method === "goal.create") {
|
|
620
|
-
if (
|
|
621
|
-
throw new Error("
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: "turn.goal_bound", data: { goalId: goal.id, goalRevision: goal.revision, authority: "human" } }, leaseToken);
|
|
627
|
-
return { goal, goalBinding: turn.goalBinding, instruction: "This Turn is now Goal-bound. Update its Work Record and finish with handoff." };
|
|
770
|
+
if (context.source.kind !== "human" || context.goalBinding || agent !== "ceo")
|
|
771
|
+
throw new Error("Root Goal creation requires an unbound CEO Human Turn");
|
|
772
|
+
const goal = this.createRootGoal(String(input.objective), typeof input.id === "string" ? input.id : undefined, turnId);
|
|
773
|
+
context.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
|
|
774
|
+
this.ledger.putTurn({ ...this.ledger.turn(turnId), ...goalTurnBinding(goal.id, goal.revision) }, "supervisor");
|
|
775
|
+
return { goal, goalBinding: context.goalBinding };
|
|
628
776
|
}
|
|
629
777
|
if (method === "goal.work") {
|
|
630
|
-
if (
|
|
778
|
+
if (context.source.kind !== "human" || context.goalBinding)
|
|
631
779
|
throw new Error("work_on_goal requires an unbound Human Turn");
|
|
632
780
|
const goal = this.#goal(String(input.goalId));
|
|
633
|
-
if (goal.
|
|
634
|
-
throw new Error("work_on_goal requires
|
|
635
|
-
|
|
636
|
-
this.ledger.
|
|
637
|
-
return { goal, goalBinding:
|
|
781
|
+
if (goal.phase !== "active" || !this.#validGoalOwner(agent, goal))
|
|
782
|
+
throw new Error("work_on_goal requires the active owned Root Goal");
|
|
783
|
+
context.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
|
|
784
|
+
this.ledger.putTurn({ ...this.ledger.turn(turnId), ...goalTurnBinding(goal.id, goal.revision) }, "supervisor");
|
|
785
|
+
return { goal, goalBinding: context.goalBinding };
|
|
638
786
|
}
|
|
639
787
|
if (method === "work_record.list")
|
|
640
788
|
return this.ledger.workRecords();
|
|
641
789
|
if (method === "work_record.read")
|
|
642
|
-
return this.ledger.workRecord(String(input.goalId ??
|
|
790
|
+
return this.ledger.workRecord(String(input.goalId ?? context.goalBinding?.goalId ?? ""));
|
|
643
791
|
if (method === "work_record.history")
|
|
644
|
-
return this.ledger.workRecordHistory(String(input.goalId ??
|
|
792
|
+
return this.ledger.workRecordHistory(String(input.goalId ?? context.goalBinding?.goalId ?? ""));
|
|
645
793
|
if (method === "work_record.diff")
|
|
646
|
-
return this.ledger.workRecordDiff(String(input.goalId ??
|
|
794
|
+
return this.ledger.workRecordDiff(String(input.goalId ?? context.goalBinding?.goalId ?? ""), Number(input.fromRevision), Number(input.toRevision));
|
|
647
795
|
if (method === "work_record.search")
|
|
648
796
|
return this.ledger.searchWorkRecords(String(input.query), Number(input.limit ?? 20));
|
|
649
797
|
if (method === "work_record.update") {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
id: String(input.id),
|
|
666
|
-
goalId: String(input.goalId),
|
|
667
|
-
newOwner: String(input.newOwner),
|
|
668
|
-
brief: (input.brief ?? null),
|
|
669
|
-
reason: String(input.reason),
|
|
670
|
-
evidence: numberArray(input.evidence),
|
|
671
|
-
}, wake.agent, wake.id);
|
|
798
|
+
const binding = context.goalBinding;
|
|
799
|
+
return this.ledger.updateWorkRecord({ goalId: binding.goalId, goalRevision: binding.goalRevision, expectedRevision: Number(input.expectedRevision), content: String(input.content), reason: String(input.reason), evidence: numberArray(input.evidence), turnId, ...(sourceWakeId ? { sourceWakeId } : {}) }, agent);
|
|
800
|
+
}
|
|
801
|
+
if (method === "goal.delegate") {
|
|
802
|
+
const binding = context.goalBinding;
|
|
803
|
+
if (String(input.parentGoalId) !== binding.goalId)
|
|
804
|
+
throw new Error("delegation parent must be the Goal bound to this Turn");
|
|
805
|
+
return this.delegate({ id: String(input.id), parentGoalId: binding.goalId, expectedParentRevision: Number(input.expectedParentRevision), childGoal: asChildGoal(input.childGoal), brief: (input.brief ?? null), reason: String(input.reason), evidence: numberArray(input.evidence), sourceTurnId: turnId }, agent, sourceWakeId);
|
|
806
|
+
}
|
|
807
|
+
if (method === "goal.reassign") {
|
|
808
|
+
const goal = this.#goal(String(input.goalId));
|
|
809
|
+
if (goal.parentId !== context.goalBinding.goalId)
|
|
810
|
+
throw new Error("reassignment target must be a child of the Goal bound to this Turn");
|
|
811
|
+
return await this.reassignGoal({ id: String(input.id), goalId: goal.id, expectedGoalRevision: Number(input.expectedGoalRevision), newOwner: String(input.newOwner), brief: (input.brief ?? null), reason: String(input.reason), evidence: numberArray(input.evidence), sourceTurnId: turnId }, agent, sourceWakeId);
|
|
812
|
+
}
|
|
672
813
|
if (method === "goal.revise")
|
|
673
|
-
return this.reviseChildGoal(String(input.goalId), String(input.objective), String(input.observationMethod), String(input.verificationMethod),
|
|
814
|
+
return this.reviseChildGoal(String(input.goalId), String(input.objective), String(input.observationMethod), String(input.verificationMethod), agent, String(input.reason), numberArray(input.evidence), sourceWakeId, turnId);
|
|
674
815
|
if (method === "goal.pause" || method === "goal.resume") {
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
if (!turn.goalBinding && !directHumanRoot)
|
|
816
|
+
const goal = this.#goal(String(input.goalId));
|
|
817
|
+
const directRoot = !context.goalBinding && context.source.kind === "human" && goal.parentId === null;
|
|
818
|
+
if (!context.goalBinding && !directRoot)
|
|
679
819
|
throw new Error(`${method} requires a Goal-bound Turn`);
|
|
680
|
-
const
|
|
681
|
-
if (method === "goal.resume" &&
|
|
682
|
-
|
|
683
|
-
this.ledger.
|
|
684
|
-
return { goal, goalBinding:
|
|
820
|
+
const next = this.transitionGoal(goal.id, method === "goal.pause" ? "paused" : "active", directRoot ? "human" : agent, !directRoot, turnId);
|
|
821
|
+
if (method === "goal.resume" && directRoot) {
|
|
822
|
+
context.goalBinding = { goalId: next.id, goalRevision: next.revision };
|
|
823
|
+
this.ledger.putTurn({ ...this.ledger.turn(turnId), ...goalTurnBinding(next.id, next.revision) }, "supervisor");
|
|
824
|
+
return { goal: next, goalBinding: context.goalBinding };
|
|
685
825
|
}
|
|
686
|
-
return
|
|
826
|
+
return next;
|
|
687
827
|
}
|
|
688
828
|
if (method === "goal.complete") {
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
if (!
|
|
829
|
+
const goal = this.#goal(String(input.goalId));
|
|
830
|
+
const directRoot = !context.goalBinding && context.source.kind === "human" && goal.parentId === null;
|
|
831
|
+
if (!context.goalBinding && !directRoot)
|
|
692
832
|
throw new Error("goal.complete requires a Goal-bound Turn");
|
|
693
|
-
return this.completeGoal({ goalId:
|
|
833
|
+
return this.completeGoal({ goalId: goal.id, revision: Number(input.revision), reason: String(input.reason), evidence: numberArray(input.evidence), sourceTurnId: turnId }, directRoot ? "human" : agent, sourceWakeId);
|
|
834
|
+
}
|
|
835
|
+
if (method === "mail.send") {
|
|
836
|
+
const to = String(input.to);
|
|
837
|
+
if (to === "human")
|
|
838
|
+
throw new Error("Human communication requires human.request");
|
|
839
|
+
if (!this.#knownAgent(to))
|
|
840
|
+
throw new Error(`unknown Agent recipient: ${to}`);
|
|
841
|
+
const goalId = String(input.goalId ?? "").trim();
|
|
842
|
+
if (!goalId)
|
|
843
|
+
throw new Error("Agent Mail requires a Goal route");
|
|
844
|
+
const goal = this.#goal(goalId);
|
|
845
|
+
if (!this.#validGoalOwner(to, goal))
|
|
846
|
+
throw new Error("Agent Mail Goal route does not match the recipient role and ownership");
|
|
847
|
+
const mail = { id: randomUUID(), to, from: agent, level: String(input.level), ...goalRoute(goalId), body: (input.body ?? null), readAt: null };
|
|
848
|
+
this.ledger.putMail(mail, agent, sourceWakeId);
|
|
849
|
+
return mail;
|
|
694
850
|
}
|
|
695
851
|
if (method === "human.request") {
|
|
852
|
+
if (agent !== "ceo")
|
|
853
|
+
throw new Error("only CEO may request Human input");
|
|
696
854
|
const evidence = numberArray(input.evidence);
|
|
855
|
+
if (!evidence.length)
|
|
856
|
+
throw new Error("evidence is required");
|
|
697
857
|
for (const seq of evidence)
|
|
698
858
|
if (!this.ledger.eventsSince(seq - 1).some((event) => event.seq === seq))
|
|
699
859
|
throw new Error(`evidence event does not exist: ${seq}`);
|
|
700
|
-
const mail = { id: randomUUID(), to: "human", from:
|
|
701
|
-
this.ledger.putMail(mail,
|
|
702
|
-
this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: "ceo.human_requested", data: mail.body }, leaseToken);
|
|
860
|
+
const mail = { id: randomUUID(), to: "human", from: agent, level: "decision", ...humanRequestRoute(), body: { type: String(input.type), message: input.message ?? null, evidence }, readAt: null };
|
|
861
|
+
this.ledger.putMail(mail, agent, sourceWakeId);
|
|
703
862
|
return mail;
|
|
704
863
|
}
|
|
705
|
-
if (method === "
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
864
|
+
if (method === "schedule.set")
|
|
865
|
+
return (this.planWake(agent, String(input.at), String(input.reason), agent, context.goalBinding) ?? { scheduled: true });
|
|
866
|
+
if (method === "goal.put") {
|
|
867
|
+
const next = input.goal;
|
|
868
|
+
if (!this.#validGoalOwner(next.owner, next))
|
|
869
|
+
throw new Error("Goal owner role does not match Root/Child position");
|
|
870
|
+
const current = this.ledger.goal(next.id);
|
|
871
|
+
const operation = !current ? "create" : current.phase !== next.phase ? (next.phase === "paused" ? "pause" : next.phase === "active" ? "resume" : next.phase === "blocked" ? "block" : "complete") : current.owner !== next.owner ? "reassign" : "revise";
|
|
872
|
+
this.ledger.putGoal(next, agent, sourceWakeId, { operation, reason: String(input.reason ?? "Advanced Goal mutation"), evidence: numberArray(input.evidence ?? []), sourceTurnId: turnId, ...(sourceWakeId ? { sourceWakeId } : {}) });
|
|
873
|
+
return input.goal;
|
|
709
874
|
}
|
|
710
875
|
if (method === "memory.append") {
|
|
711
876
|
const note = String(input.note ?? "").trim();
|
|
712
877
|
if (!note)
|
|
713
878
|
throw new Error("memory note cannot be empty");
|
|
714
|
-
const
|
|
715
|
-
return { seq:
|
|
879
|
+
const event = this.ledger.appendEvent({ streamId: memoryStream(agent), ts: this.#now(), actor: agent, type: "memory.appended", data: { note, turnId } });
|
|
880
|
+
return { seq: event.seq };
|
|
716
881
|
}
|
|
717
|
-
|
|
718
|
-
return (this.planWake(wake.agent, String(input.at), String(input.reason), wake.agent) ?? { scheduled: true });
|
|
719
|
-
if (method === "audit.ack")
|
|
720
|
-
return this.ackAuditAdvice(String(input.actionId), wake.agent);
|
|
721
|
-
if (method === "audit.write")
|
|
722
|
-
return this.putAuditAdvice(String(input.actionId), { by: wake.agent, body: (input.body ?? null), evidence: numberArray(input.evidence) }, wake.id);
|
|
723
|
-
if (method === "goal.put") {
|
|
724
|
-
this.ledger.putGoal(input.goal, wake.agent, wake.id);
|
|
725
|
-
return input.goal;
|
|
726
|
-
}
|
|
727
|
-
const action = await this.submitAction({
|
|
728
|
-
id: String(input.id), agent: wake.agent, kind: String(input.kind), payload: (input.payload ?? null), reason: String(input.reason), evidence: numberArray(input.evidence), auditAdvice: null, adviceAcked: false,
|
|
729
|
-
}, String(input.connector), wake.id);
|
|
730
|
-
return action;
|
|
882
|
+
throw new Error(`unsupported Agent capability: ${method}`);
|
|
731
883
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
this.recordMetric({ ...sample, goalId: registration.goalId });
|
|
884
|
+
#validateHandoffDraft(turnId, attemptId, agent, execution, context, input) {
|
|
885
|
+
if (!context.goalBinding || execution.bindingKind !== "goal")
|
|
886
|
+
return { accepted: false, fatal: true, attemptId, issues: [{ code: "turn_not_goal_bound", message: "This Turn is no longer Goal-bound." }] };
|
|
887
|
+
const request = { handoff: (input.handoff ?? null), candidateMessage: String(input.candidateMessage ?? "") };
|
|
888
|
+
const issues = [];
|
|
889
|
+
try {
|
|
890
|
+
assertAgentHandoff(request.handoff);
|
|
740
891
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
for (const [goalId, contract] of this.#metricContracts) {
|
|
744
|
-
const goal = this.ledger.goal(goalId);
|
|
745
|
-
if (!goal)
|
|
746
|
-
continue;
|
|
747
|
-
const samples = this.ledger.metricSamples(goal.id);
|
|
748
|
-
const evaluation = evaluateMetric(contract, samples, this.#now());
|
|
749
|
-
if (evaluation.shouldWakeOwner)
|
|
750
|
-
this.#enqueueTrigger(goal.owner, `metric:${goal.id}:${evaluation.status}:${samples.at(-1)?.observedAt ?? "none"}`);
|
|
892
|
+
catch {
|
|
893
|
+
issues.push({ code: "handoff_invalid", message: "Handoff requires a valid outcome and at least one evidence event." });
|
|
751
894
|
}
|
|
895
|
+
const existingMessage = [...this.ledger.turnItems(turnId)].reverse().find((item) => item.type === "assistant_message" && item.status === "completed" && typeof item.data.text === "string" && Boolean(item.data.text.trim()));
|
|
896
|
+
const message = request.candidateMessage.trim() || String(existingMessage?.data?.text ?? "").trim();
|
|
897
|
+
if (!message)
|
|
898
|
+
issues.push({ code: "message_missing", message: "This Goal Turn needs a readable assistant message before it can finish." });
|
|
899
|
+
const goal = this.ledger.goal(context.goalBinding.goalId);
|
|
900
|
+
if (!goal || goal.phase !== "active" || goal.owner !== agent || goal.revision !== context.goalBinding.goalRevision)
|
|
901
|
+
return { accepted: false, fatal: true, attemptId, issues: [{ code: "goal_fence_changed", message: "Goal ownership, phase, or revision changed; this Turn can no longer finish against the old binding." }] };
|
|
902
|
+
const record = this.ledger.workRecord(goal.id);
|
|
903
|
+
if (!record || record.updatedInTurn !== turnId || record.goalRevision !== goal.revision)
|
|
904
|
+
issues.push({ code: "work_record_not_updated", message: "Update this Goal's Work Record in the current Turn before Handoff.", details: { goalId: goal.id, recordRevision: record?.recordRevision ?? null } });
|
|
905
|
+
const evidence = Array.isArray(request.handoff?.evidence) ? request.handoff.evidence : [];
|
|
906
|
+
for (const seq of evidence)
|
|
907
|
+
if (!Number.isInteger(seq) || !this.ledger.eventsSince(seq - 1).some((event) => event.seq === seq))
|
|
908
|
+
issues.push({ code: "evidence_not_found", message: `Evidence event ${String(seq)} does not exist in the Ledger.`, details: { seq } });
|
|
909
|
+
if (issues.length)
|
|
910
|
+
return { accepted: false, fatal: false, attemptId, issues };
|
|
911
|
+
const token = randomUUID();
|
|
912
|
+
this.#handoffValidations.set(token, { turnId, attemptId, agent, attempt: execution.attempt, leaseToken: execution.leaseToken, goalId: goal.id, goalRevision: goal.revision, message, handoff: { outcome: request.handoff.outcome, evidence: [...request.handoff.evidence] } });
|
|
913
|
+
return { accepted: true, fatal: false, attemptId, token, goalId: goal.id, goalRevision: goal.revision };
|
|
752
914
|
}
|
|
753
|
-
|
|
754
|
-
#
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (!last)
|
|
759
|
-
return;
|
|
760
|
-
const silentMs = this.clock.now().getTime() - Date.parse(last.ts);
|
|
761
|
-
if (silentMs <= this.#silence.maxSilentMs)
|
|
762
|
-
return;
|
|
763
|
-
const fact = { silentMs, lastEventSeq: last.seq, lastEventAt: last.ts };
|
|
764
|
-
this.ledger.appendEvent({ streamId: controlStream("watchdog"), ts: this.#now(), actor: "supervisor", type: "watchdog.system_silence", data: { ...fact, notify: this.#silence.notify } });
|
|
765
|
-
this.ledger.putMail({ id: `silence:${last.seq}`, to: this.#silence.notify, from: "supervisor", level: "decision", readAt: null, body: { type: "system_silence", ...fact, note: "No ledger events system-wide within the silence window. Confirm this is expected and hand off." } }, "supervisor");
|
|
766
|
-
}
|
|
915
|
+
#beginHandoffAttempt(turnId) { this.#invalidateHandoffTokens(turnId); const attemptId = (this.#handoffAttemptSeq.get(turnId) ?? 0) + 1; this.#handoffAttemptSeq.set(turnId, attemptId); return attemptId; }
|
|
916
|
+
#invalidateHandoffTokens(turnId) { for (const [token, value] of this.#handoffValidations)
|
|
917
|
+
if (value.turnId === turnId)
|
|
918
|
+
this.#handoffValidations.delete(token); }
|
|
919
|
+
#clearHandoffState(turnId) { this.#invalidateHandoffTokens(turnId); this.#handoffAttemptSeq.delete(turnId); }
|
|
767
920
|
}
|
|
768
921
|
export async function runSupervisorDaemon(supervisor, options = {}) {
|
|
769
922
|
const pollMs = options.pollMs ?? 1_000;
|
|
@@ -776,8 +929,15 @@ export async function runSupervisorDaemon(supervisor, options = {}) {
|
|
|
776
929
|
options.onError?.(error);
|
|
777
930
|
}
|
|
778
931
|
await new Promise((resolve) => {
|
|
779
|
-
|
|
780
|
-
|
|
932
|
+
if (options.signal?.aborted) {
|
|
933
|
+
resolve();
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const onAbort = () => { clearTimeout(timer); resolve(); };
|
|
937
|
+
const timer = setTimeout(() => { options.signal?.removeEventListener("abort", onAbort); resolve(); }, pollMs);
|
|
938
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
939
|
+
if (options.signal?.aborted)
|
|
940
|
+
onAbort();
|
|
781
941
|
});
|
|
782
942
|
}
|
|
783
943
|
}
|
|
@@ -785,103 +945,130 @@ export function deriveTeam(ledger, now = new Date().toISOString()) {
|
|
|
785
945
|
const goals = ledger.goals();
|
|
786
946
|
const wakes = ledger.wakes();
|
|
787
947
|
const schedules = ledger.schedules();
|
|
948
|
+
const threads = ledger.threads();
|
|
949
|
+
const turns = ledger.turns();
|
|
788
950
|
const handoffs = ledger.eventsSince(0, ["handoff.recorded"]);
|
|
789
951
|
const owners = [...new Set(goals.map((goal) => goal.owner))].sort();
|
|
790
952
|
return owners.map((agent) => {
|
|
791
953
|
const owned = goals.filter((goal) => goal.owner === agent);
|
|
792
954
|
const live = owned.filter((goal) => goal.phase !== "complete");
|
|
793
955
|
const agentWakes = wakes.filter((wake) => wake.agent === agent).sort((a, b) => b.enqueuedSeq - a.enqueuedSeq);
|
|
794
|
-
const activeWake = agentWakes.find((wake) =>
|
|
795
|
-
|
|
956
|
+
const activeWake = agentWakes.find((wake) => { if (wake.status !== "claimed" && wake.status !== "queued")
|
|
957
|
+
return false; if (!wake.goalId)
|
|
958
|
+
return true; const goal = ledger.goal(wake.goalId); return goal?.phase === "active" && goal.owner === agent; });
|
|
959
|
+
const threadIds = new Set(threads.filter((thread) => thread.agent === agent).map((thread) => thread.id));
|
|
960
|
+
const activeTurn = turns.find((turn) => threadIds.has(turn.threadId) && turn.status === "in_progress");
|
|
961
|
+
const nextWakeAt = schedules.filter((schedule) => { if (schedule.agent !== agent || schedule.status !== "pending" || schedule.nextWakeAt <= now)
|
|
962
|
+
return false; if (!schedule.goalId)
|
|
963
|
+
return true; const goal = ledger.goal(schedule.goalId); return goal?.phase === "active" && goal.owner === agent; }).map((schedule) => schedule.nextWakeAt).sort()[0] ?? null;
|
|
796
964
|
const lastHandoff = [...handoffs].reverse().find((event) => event.actor === agent) ?? null;
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
let status;
|
|
965
|
+
const value = lastHandoff?.data;
|
|
966
|
+
const lastOutcome = typeof value?.outcome === "string" ? value.outcome : null;
|
|
967
|
+
let motion;
|
|
801
968
|
if (live.length === 0)
|
|
802
|
-
|
|
803
|
-
else if (
|
|
804
|
-
|
|
969
|
+
motion = "retired";
|
|
970
|
+
else if (activeTurn)
|
|
971
|
+
motion = "running";
|
|
805
972
|
else if (activeWake)
|
|
806
|
-
|
|
973
|
+
motion = "queued";
|
|
807
974
|
else if (nextWakeAt)
|
|
808
|
-
|
|
809
|
-
else if (live.every((goal) => goal.phase === "blocked"))
|
|
810
|
-
status = "blocked";
|
|
811
|
-
else if (live.every((goal) => goal.phase === "paused") || typeof blocker === "string" && blocker.length > 0)
|
|
812
|
-
status = "waiting";
|
|
975
|
+
motion = "scheduled";
|
|
813
976
|
else
|
|
814
|
-
|
|
815
|
-
return { agent, goalIds: owned.map((goal) => goal.id),
|
|
977
|
+
motion = "idle";
|
|
978
|
+
return { agent, goalIds: owned.map((goal) => goal.id), motion, lastOutcome, lastHandoffSeq: lastHandoff?.seq ?? null, lastWakeStatus: agentWakes[0]?.status ?? null, nextWakeAt };
|
|
816
979
|
});
|
|
817
980
|
}
|
|
981
|
+
export function deriveRecoveryViews(ledger) {
|
|
982
|
+
const views = [];
|
|
983
|
+
const wakes = ledger.wakes();
|
|
984
|
+
const schedules = ledger.schedules();
|
|
985
|
+
const triggers = wakes.flatMap((wake) => ledger.wakeTriggers(wake.id));
|
|
986
|
+
for (const turn of ledger.turns()) {
|
|
987
|
+
if (turn.status !== "failed")
|
|
988
|
+
continue;
|
|
989
|
+
const sourceWake = wakes.find((wake) => wake.turnId === turn.id);
|
|
990
|
+
const thread = ledger.thread(turn.threadId);
|
|
991
|
+
const goal = turn.bindingKind === "goal" ? ledger.goal(turn.goalId) : null;
|
|
992
|
+
const goalFailure = Boolean(turn.bindingKind === "goal" && thread && goal && goal.phase === "active" && goal.owner === thread.agent);
|
|
993
|
+
const specialistFailure = Boolean(turn.bindingKind === "specialist" && thread && sourceWake?.agent === thread.agent);
|
|
994
|
+
if (!sourceWake || !thread || !goalFailure && !specialistFailure)
|
|
995
|
+
continue;
|
|
996
|
+
const base = { turnId: turn.id, agent: thread.agent };
|
|
997
|
+
const scheduled = schedules.some((schedule) => {
|
|
998
|
+
const recovery = parseRecoveryTrigger(schedule.id);
|
|
999
|
+
const sameTarget = goalFailure ? schedule.bindingKind === "goal" && schedule.goalId === goal.id : schedule.bindingKind === "specialist" && turn.bindingKind === "specialist" && schedule.specialistRole === turn.specialistRole;
|
|
1000
|
+
return schedule.status === "pending" && schedule.setBy === "supervisor" && schedule.agent === thread.agent && sameTarget && schedule.reason === recoveryRef(turn.id) && recovery?.turnId === turn.id && recovery.attempt > 0;
|
|
1001
|
+
});
|
|
1002
|
+
if (scheduled) {
|
|
1003
|
+
views.push({ ...base, state: "scheduled", actionable: false });
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const recoveryWake = triggers.flatMap((trigger) => {
|
|
1007
|
+
const recovery = parseRecoveryTrigger(trigger.triggerRef);
|
|
1008
|
+
const wake = recovery?.turnId === turn.id ? wakes.find((candidate) => candidate.id === trigger.wakeId) : null;
|
|
1009
|
+
const sameTarget = goalFailure ? wake?.bindingKind === "goal" && wake.goalId === goal.id : wake?.bindingKind === "specialist" && turn.bindingKind === "specialist" && wake.specialistRole === turn.specialistRole;
|
|
1010
|
+
return wake?.agent === thread.agent && sameTarget ? [wake] : [];
|
|
1011
|
+
}).sort((left, right) => right.enqueuedSeq - left.enqueuedSeq)[0];
|
|
1012
|
+
if (recoveryWake?.status === "queued" || recoveryWake?.status === "claimed") {
|
|
1013
|
+
views.push({ ...base, state: "queued", actionable: false });
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (recoveryWake?.status === "consumed" && recoveryWake.turnId) {
|
|
1017
|
+
const retry = ledger.turn(recoveryWake.turnId);
|
|
1018
|
+
const state = retry?.status === "in_progress" ? "running" : retry?.status === "completed" ? "recovered" : "superseded";
|
|
1019
|
+
views.push({ ...base, state, actionable: false });
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const parent = goalFailure && goal.parentId ? ledger.goal(goal.parentId) : null;
|
|
1023
|
+
const escalation = triggers.flatMap((trigger) => {
|
|
1024
|
+
const wake = trigger.triggerRef === childRetryExhaustedRef(turn.id) ? wakes.find((candidate) => candidate.id === trigger.wakeId) : null;
|
|
1025
|
+
return wake?.bindingKind === "goal" && parent && wake.agent === parent.owner && wake.goalId === parent.id && parent.phase === "active" ? [wake] : [];
|
|
1026
|
+
}).find((wake) => wake.status === "queued" || wake.status === "claimed" || wake.status === "consumed");
|
|
1027
|
+
if (escalation) {
|
|
1028
|
+
views.push({ ...base, state: "escalated", actionable: false });
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
views.push({ ...base, state: "needed", actionable: true });
|
|
1032
|
+
}
|
|
1033
|
+
return views;
|
|
1034
|
+
}
|
|
818
1035
|
export function renderDashboard(ledger) {
|
|
819
1036
|
const rows = (values) => values.map((value) => `<tr><td><pre>${escapeHtml(JSON.stringify(value, null, 2))}</pre></td></tr>`).join("");
|
|
820
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>goah status</title><style>body{font:14px ui-monospace;margin:32px;background:#101418;color:#dce3e4}section{margin:32px 0}pre{white-space:pre-wrap;border:1px solid #334;padding:12px}</style></head><body><h1>goah</h1><p>seq ${ledger.events().at(-1)?.seq ?? 0}</p><section><h2>Team</h2><table>${rows(deriveTeam(ledger))}</table></section><section><h2>Goals</h2><table>${rows(ledger.goals())}</table></section><section><h2>Wakes</h2><table>${rows(ledger.wakes())}</table></section><section><h2>
|
|
821
|
-
}
|
|
822
|
-
async function runConnector(spec, operation, action) {
|
|
823
|
-
return runJsonProcess(spec, { operation, action });
|
|
824
|
-
}
|
|
825
|
-
async function runJsonProcess(spec, input) {
|
|
826
|
-
const child = spawn(spec.command, spec.args, { detached: process.platform !== "win32", env: minimalEnvironment(spec.env), stdio: ["pipe", "pipe", "pipe"] });
|
|
827
|
-
let stdout = "";
|
|
828
|
-
let stderr = "";
|
|
829
|
-
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
830
|
-
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
831
|
-
child.stdin.end(`${JSON.stringify(input)}\n`);
|
|
832
|
-
const timeoutMs = spec.timeoutMs ?? 30_000;
|
|
833
|
-
let timer;
|
|
834
|
-
const exit = new Promise((resolve, reject) => {
|
|
835
|
-
child.once("error", reject);
|
|
836
|
-
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
837
|
-
});
|
|
838
|
-
timer = setTimeout(() => { void terminateChild(child, 500); }, timeoutMs);
|
|
839
|
-
const result = await exit;
|
|
840
|
-
clearTimeout(timer);
|
|
841
|
-
if (result.code !== 0)
|
|
842
|
-
throw new Error(stderr.trim() || `connector exited (${result.code ?? result.signal})`);
|
|
843
|
-
return JSON.parse(stdout.trim());
|
|
844
|
-
}
|
|
845
|
-
async function terminateChild(child, graceMs) {
|
|
846
|
-
if (!child.pid || child.exitCode !== null || child.signalCode !== null)
|
|
847
|
-
return;
|
|
848
|
-
signalPid(child.pid, "SIGTERM");
|
|
849
|
-
await new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
850
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
851
|
-
signalPid(child.pid, "SIGKILL");
|
|
852
|
-
}
|
|
853
|
-
function signalPid(pid, signal) { try {
|
|
854
|
-
process.kill(process.platform === "win32" ? pid : -pid, signal);
|
|
855
|
-
}
|
|
856
|
-
catch { } }
|
|
857
|
-
function minimalEnvironment(explicit = {}) {
|
|
858
|
-
const env = {};
|
|
859
|
-
for (const name of ["PATH", "TMPDIR", "TMP", "TEMP", "SYSTEMROOT"])
|
|
860
|
-
if (process.env[name] !== undefined)
|
|
861
|
-
env[name] = process.env[name];
|
|
862
|
-
return { ...env, ...explicit };
|
|
1037
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>goah status</title><style>body{font:14px ui-monospace;margin:32px;background:#101418;color:#dce3e4}section{margin:32px 0}pre{white-space:pre-wrap;border:1px solid #334;padding:12px}</style></head><body><h1>goah</h1><p>seq ${ledger.events().at(-1)?.seq ?? 0}</p><section><h2>Team</h2><table>${rows(deriveTeam(ledger))}</table></section><section><h2>Goals</h2><table>${rows(ledger.goals())}</table></section><section><h2>Wakes</h2><table>${rows(ledger.wakes())}</table></section><section><h2>Mailbox</h2><table>${rows(ledger.mailbox())}</table></section></body></html>`;
|
|
863
1038
|
}
|
|
864
1039
|
function escapeHtml(value) { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); }
|
|
865
|
-
function
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
function goalBoundCapability(method) { return ["goal.delegate", "goal.reassign", "goal.revise", "goal.put", "work_record.update", "
|
|
1040
|
+
function messageTextContent(content) { if (typeof content === "string")
|
|
1041
|
+
return content; if (!Array.isArray(content))
|
|
1042
|
+
return ""; return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && part.type === "text" ? String(part.text ?? "") : "").filter(Boolean).join("\n"); }
|
|
1043
|
+
function goalBoundCapability(method) { return ["goal.delegate", "goal.reassign", "goal.revise", "goal.put", "work_record.update", "schedule.set", "human.request"].includes(method); }
|
|
869
1044
|
function asRecord(value) { if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
870
1045
|
throw new Error("RPC params must be an object"); return value; }
|
|
1046
|
+
function boundedJson(value, maxChars = 2_000) { const text = typeof value === "string" ? value : JSON.stringify(value); return text.length <= maxChars ? text : `${text.slice(0, maxChars)}…`; }
|
|
1047
|
+
function recoveryRef(turnId) { return `recovery:${turnId}`; }
|
|
1048
|
+
function recoveryScheduleId(turnId, attempt) { return `${recoveryRef(turnId)}:${attempt}`; }
|
|
1049
|
+
function childRetryExhaustedRef(turnId) { return `child-retry-exhausted:${turnId}`; }
|
|
1050
|
+
function childRetryExhaustedTurnId(triggerRef) { return triggerRef.startsWith("child-retry-exhausted:") ? triggerRef.slice("child-retry-exhausted:".length) || null : null; }
|
|
1051
|
+
function parseRecoveryTrigger(triggerRef) { if (!triggerRef.startsWith("recovery:"))
|
|
1052
|
+
return null; const [turnId, rawAttempt] = triggerRef.slice("recovery:".length).split("@")[0].split(":"); if (!turnId)
|
|
1053
|
+
return null; const attempt = rawAttempt === undefined ? 0 : Number(rawAttempt); return Number.isInteger(attempt) && attempt >= 0 ? { turnId, attempt } : null; }
|
|
1054
|
+
function mailIdFromTriggerRef(triggerRef) { if (!triggerRef.startsWith("mail:"))
|
|
1055
|
+
return null; return triggerRef.slice("mail:".length).split("@redelivery:")[0] || null; }
|
|
1056
|
+
function mailDeliveryAttempt(triggerRef, base) { if (triggerRef === base)
|
|
1057
|
+
return 0; const prefix = `${base}@redelivery:`; if (!triggerRef.startsWith(prefix))
|
|
1058
|
+
return null; const attempt = Number(triggerRef.slice(prefix.length)); return Number.isInteger(attempt) && attempt > 0 ? attempt : null; }
|
|
871
1059
|
function numberArray(value) { if (!Array.isArray(value) || value.some((item) => typeof item !== "number"))
|
|
872
1060
|
throw new Error("RPC evidence must be a number array"); return value; }
|
|
1061
|
+
function isSameNumbers(left, right) { return left.length === right.length && left.every((value, index) => value === right[index]); }
|
|
873
1062
|
function asChildGoal(value) {
|
|
874
1063
|
const input = asRecord(value ?? null);
|
|
875
1064
|
return { id: String(input.id), objective: String(input.objective), observationMethod: String(input.observationMethod), verificationMethod: String(input.verificationMethod), owner: String(input.owner) };
|
|
876
1065
|
}
|
|
877
1066
|
function defaultCapabilities(role) {
|
|
878
1067
|
if (role === "ceo")
|
|
879
|
-
return ["ledger.search", "mail.send", "schedule.set", "
|
|
880
|
-
if (role === "verifier")
|
|
881
|
-
return ["ledger.search", "mail.send", "memory.append"
|
|
882
|
-
|
|
883
|
-
return ["ledger.search", "mail.send", "memory.append", "audit.write"];
|
|
884
|
-
return ["ledger.search", "mail.send", "schedule.set", "action.submit", "audit.ack", "goal.get", "work_record.list", "work_record.read", "work_record.history", "work_record.diff", "work_record.search", "work_record.update"];
|
|
1068
|
+
return ["ledger.search", "mail.send", "schedule.set", "team.list", "goal.get", "goal.create", "goal.work", "goal.delegate", "goal.reassign", "goal.revise", "goal.pause", "goal.resume", "goal.complete", "human.request", "work_record.list", "work_record.read", "work_record.history", "work_record.diff", "work_record.search", "work_record.update"];
|
|
1069
|
+
if (role === "verifier" || role === "audit")
|
|
1070
|
+
return ["ledger.search", "mail.send", "memory.append"];
|
|
1071
|
+
return ["ledger.search", "mail.send", "schedule.set", "goal.get", "work_record.list", "work_record.read", "work_record.history", "work_record.diff", "work_record.search", "work_record.update"];
|
|
885
1072
|
}
|
|
886
1073
|
export * from "./verification.js";
|
|
887
1074
|
export * from "./roles.js";
|