@goah/cli 0.10.2 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +15 -5
- package/dist/cli.js.map +1 -1
- package/dist/console/assets/{index-BPOrMqEr.js → index-BJTGvlCH.js} +2 -2
- package/dist/console/assets/{index-BPOrMqEr.js.map → index-BJTGvlCH.js.map} +1 -1
- package/dist/console/index.html +1 -1
- package/dist/control.d.ts +16 -0
- package/dist/control.d.ts.map +1 -1
- package/dist/control.js +16 -5
- package/dist/control.js.map +1 -1
- package/dist/tui.d.ts +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +66 -13
- package/dist/tui.js.map +1 -1
- package/dist/update.js +1 -1
- package/dist/update.js.map +1 -1
- package/dist/web-console.js +1 -2
- package/dist/web-console.js.map +1 -1
- package/node_modules/goah-ledger-contract/dist/execution.d.ts +79 -2
- package/node_modules/goah-ledger-contract/dist/execution.d.ts.map +1 -1
- package/node_modules/goah-ledger-contract/dist/execution.js +13 -2
- package/node_modules/goah-ledger-contract/dist/execution.js.map +1 -1
- package/node_modules/goah-ledger-contract/dist/kernel.d.ts +2 -1
- package/node_modules/goah-ledger-contract/dist/kernel.d.ts.map +1 -1
- package/node_modules/goah-ledger-contract/dist/kernel.js +2 -1
- package/node_modules/goah-ledger-contract/dist/kernel.js.map +1 -1
- package/node_modules/goah-ledger-sqlite/dist/index.d.ts +9 -2
- package/node_modules/goah-ledger-sqlite/dist/index.d.ts.map +1 -1
- package/node_modules/goah-ledger-sqlite/dist/index.js +183 -25
- package/node_modules/goah-ledger-sqlite/dist/index.js.map +1 -1
- package/node_modules/goah-runner-pi/dist/index.d.ts +4 -2
- package/node_modules/goah-runner-pi/dist/index.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/index.js +4 -2
- package/node_modules/goah-runner-pi/dist/index.js.map +1 -1
- package/node_modules/goah-runner-pi/dist/pi-worker.d.ts.map +1 -1
- package/node_modules/goah-runner-pi/dist/pi-worker.js +66 -16
- package/node_modules/goah-runner-pi/dist/pi-worker.js.map +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 +1 -0
- package/node_modules/goah-runner-pi/dist/test-helpers.js.map +1 -1
- package/node_modules/goah-supervisor/dist/context-view.d.ts.map +1 -1
- package/node_modules/goah-supervisor/dist/context-view.js +1 -0
- package/node_modules/goah-supervisor/dist/context-view.js.map +1 -1
- package/node_modules/goah-supervisor/dist/index.d.ts +7 -2
- package/node_modules/goah-supervisor/dist/index.d.ts.map +1 -1
- package/node_modules/goah-supervisor/dist/index.js +185 -34
- package/node_modules/goah-supervisor/dist/index.js.map +1 -1
- package/node_modules/goah-supervisor/dist/roles.js +2 -2
- package/node_modules/goah-supervisor/dist/roles.js.map +1 -1
- package/node_modules/goah-testkit/dist/faux-runner-worker.js +34 -7
- package/node_modules/goah-testkit/dist/faux-runner-worker.js.map +1 -1
- package/node_modules/goah-testkit/dist/index.d.ts +1 -0
- package/node_modules/goah-testkit/dist/index.d.ts.map +1 -1
- package/node_modules/goah-testkit/dist/index.js +5 -5
- package/node_modules/goah-testkit/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -94,16 +94,32 @@ export class Supervisor {
|
|
|
94
94
|
}
|
|
95
95
|
get runner() { return this.#runner; }
|
|
96
96
|
createGoal(goal, actor = "human") { this.ledger.putGoal(goal, actor); }
|
|
97
|
-
|
|
97
|
+
createRootGoal(objective, id = randomUUID()) {
|
|
98
98
|
if (!objective.trim())
|
|
99
99
|
throw new Error("root objective is required");
|
|
100
|
-
|
|
100
|
+
if (this.ledger.goals().some((goal) => goal.parentId === null && goal.owner === "ceo" && goal.phase !== "complete"))
|
|
101
|
+
throw new Error("CEO already has an unfinished Root Goal; use work_on_goal");
|
|
102
|
+
const goal = { id, parentId: null, objective, observationMethod: null, verificationMethod: null, owner: "ceo", phase: "active", revision: 0 };
|
|
101
103
|
this.ledger.putGoal(goal, "human");
|
|
104
|
+
return this.#goal(id);
|
|
105
|
+
}
|
|
106
|
+
startGoal(objective, id = randomUUID()) {
|
|
107
|
+
const goal = this.createRootGoal(objective, id);
|
|
102
108
|
const wake = this.#enqueueTrigger("ceo", `root:${id}:created`);
|
|
103
109
|
if (!wake)
|
|
104
110
|
throw new Error("CEO wake was not admitted for an active root goal");
|
|
105
111
|
return { goal, wake };
|
|
106
112
|
}
|
|
113
|
+
interactWithCeo(message) {
|
|
114
|
+
if (!message.trim())
|
|
115
|
+
throw new Error("message is required");
|
|
116
|
+
const mail = { id: randomUUID(), to: "ceo", from: "human", level: "fyi", body: { type: "interaction", message }, readAt: null };
|
|
117
|
+
this.ledger.putMail(mail, "human");
|
|
118
|
+
const wake = this.#enqueueTrigger("ceo", `interaction:${mail.id}`);
|
|
119
|
+
if (!wake)
|
|
120
|
+
throw new Error("CEO interaction was not admitted");
|
|
121
|
+
return { mail, wake };
|
|
122
|
+
}
|
|
107
123
|
sendToCeo(body, level = "decision") {
|
|
108
124
|
const mail = { id: randomUUID(), to: "ceo", from: "human", level, body, readAt: null };
|
|
109
125
|
this.ledger.putMail(mail, "human");
|
|
@@ -116,15 +132,16 @@ export class Supervisor {
|
|
|
116
132
|
reassignGoal(request, actor = "ceo", wakeId) { return this.ledger.commitReassignment(request, actor, wakeId); }
|
|
117
133
|
teamList(now = this.#now()) { return deriveTeam(this.ledger, now); }
|
|
118
134
|
updateGoal(id, patch, actor = "human") {
|
|
119
|
-
if (patch.objective === undefined && patch.observationMethod === undefined && patch.owner === undefined)
|
|
120
|
-
throw new Error("goal update requires objective, observation method, or owner");
|
|
135
|
+
if (patch.objective === undefined && patch.observationMethod === undefined && patch.verificationMethod === undefined && patch.owner === undefined)
|
|
136
|
+
throw new Error("goal update requires objective, observation method, verification method, or owner");
|
|
121
137
|
const current = this.#goal(id);
|
|
122
|
-
if (patch.objective !== undefined && patch.objective !== current.objective && current.parentId !== null && patch.observationMethod === undefined)
|
|
123
|
-
throw new Error("child objective revision requires
|
|
138
|
+
if (patch.objective !== undefined && patch.objective !== current.objective && current.parentId !== null && (patch.observationMethod === undefined || patch.verificationMethod === undefined))
|
|
139
|
+
throw new Error("child objective revision requires replacement observation and verification methods");
|
|
124
140
|
const next = {
|
|
125
141
|
...current,
|
|
126
142
|
...patch,
|
|
127
143
|
...(patch.objective !== undefined && patch.objective !== current.objective && current.parentId === null && patch.observationMethod === undefined ? { observationMethod: null } : {}),
|
|
144
|
+
...(patch.objective !== undefined && patch.objective !== current.objective && current.parentId === null && patch.verificationMethod === undefined ? { verificationMethod: null } : {}),
|
|
128
145
|
revision: current.revision + 1,
|
|
129
146
|
};
|
|
130
147
|
this.ledger.putGoal(next, actor);
|
|
@@ -136,9 +153,9 @@ export class Supervisor {
|
|
|
136
153
|
const current = this.#goal(id);
|
|
137
154
|
if (current.parentId !== null)
|
|
138
155
|
throw new Error("human confirmation applies only to a root goal");
|
|
139
|
-
return this.updateGoal(id, { observationMethod }, "human");
|
|
156
|
+
return this.updateGoal(id, { observationMethod, verificationMethod: observationMethod }, "human");
|
|
140
157
|
}
|
|
141
|
-
reviseChildGoal(id, objective, observationMethod, actor, reason, evidence, wakeId) {
|
|
158
|
+
reviseChildGoal(id, objective, observationMethod, verificationMethod, actor, reason, evidence, wakeId) {
|
|
142
159
|
const current = this.#goal(id);
|
|
143
160
|
if (current.parentId === null)
|
|
144
161
|
throw new Error("CEO cannot revise a root goal");
|
|
@@ -147,8 +164,8 @@ export class Supervisor {
|
|
|
147
164
|
for (const seq of evidence)
|
|
148
165
|
if (!this.ledger.eventsSince(seq - 1).some((event) => event.seq === seq))
|
|
149
166
|
throw new Error(`evidence event does not exist: ${seq}`);
|
|
150
|
-
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, reason, evidence } });
|
|
151
|
-
return this.updateGoal(id, { objective, observationMethod }, actor);
|
|
167
|
+
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);
|
|
152
169
|
}
|
|
153
170
|
completeGoal(request, actor = "human", wakeId) {
|
|
154
171
|
const goal = this.ledger.completeGoal(request, actor, wakeId);
|
|
@@ -212,13 +229,17 @@ export class Supervisor {
|
|
|
212
229
|
let renewal;
|
|
213
230
|
try {
|
|
214
231
|
running = this.ledger.markWakeRunning(wake.id, this.#now(), leaseToken);
|
|
215
|
-
const
|
|
232
|
+
const turn = this.#turnContext(running);
|
|
233
|
+
this.ledger.appendRunnerEvent({ streamId: wakeStream(running.id), ts: this.#now(), actor: running.agent, type: "run.admitted", data: turn }, leaseToken);
|
|
234
|
+
const workRecordRevisionAtStart = turn.goalBinding ? this.ledger.workRecord(turn.goalBinding.goalId)?.recordRevision ?? -1 : -1;
|
|
235
|
+
const context = this.#loadContext(running, turn);
|
|
216
236
|
handle = this.runner.prepare({
|
|
217
237
|
wake: running,
|
|
238
|
+
turn,
|
|
218
239
|
context,
|
|
219
240
|
now: () => this.#now(),
|
|
220
241
|
emit: (trace) => this.ledger.appendRunnerEvent({ streamId: wakeStream(running.id), ts: this.#now(), actor: running.agent, type: trace.type, data: trace.data }, leaseToken),
|
|
221
|
-
rpc: (method, params) => this.#agentRpc(running, leaseToken, method, params),
|
|
242
|
+
rpc: (method, params) => this.#agentRpc(running, turn, leaseToken, method, params),
|
|
222
243
|
});
|
|
223
244
|
this.#handles.set(running.id, handle);
|
|
224
245
|
if (handle.pid)
|
|
@@ -243,14 +264,27 @@ export class Supervisor {
|
|
|
243
264
|
await this.#markAbnormal(running, result.reason);
|
|
244
265
|
return this.#wake(running.id);
|
|
245
266
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 }
|
|
250
284
|
: null;
|
|
251
|
-
const handoffEvent = this.ledger.commitHandoff({ agent: running.agent, wakeId: running.id, ts: this.#now(), output
|
|
285
|
+
const handoffEvent = this.ledger.commitHandoff({ agent: running.agent, wakeId: running.id, ts: this.#now(), output, outgoingMail, schedule });
|
|
252
286
|
this.ledger.finishWake(running.id, "done", this.#now());
|
|
253
|
-
if (this.#role(running.agent) !== "ceo" && (
|
|
287
|
+
if (this.#role(running.agent) !== "ceo" && handoffTriggersCeo(output.handoff) && this.#hasActiveRoot()) {
|
|
254
288
|
this.#enqueueTrigger("ceo", `child-handoff:${handoffEvent.seq}`);
|
|
255
289
|
}
|
|
256
290
|
if (this.#verifyMetricsAfterWake) {
|
|
@@ -416,10 +450,10 @@ export class Supervisor {
|
|
|
416
450
|
if (exact)
|
|
417
451
|
return exact;
|
|
418
452
|
const ownsLiveGoal = this.ledger.goalsForOwner(agent).some((goal) => goal.phase === "active" || goal.phase === "blocked");
|
|
419
|
-
const ceoInterrupt = agent === "ceo" && (triggerRef.startsWith("mail:") || triggerRef.startsWith("child-"));
|
|
453
|
+
const ceoInterrupt = agent === "ceo" && (triggerRef.startsWith("interaction:") || triggerRef.startsWith("mail:") || triggerRef.startsWith("child-"));
|
|
420
454
|
if (!ownsLiveGoal && !ceoInterrupt)
|
|
421
455
|
return null;
|
|
422
|
-
const queued = this.ledger.queuedWakeForAgent(agent);
|
|
456
|
+
const queued = triggerRef.startsWith("interaction:") ? null : this.ledger.queuedWakeForAgent(agent);
|
|
423
457
|
if (queued) {
|
|
424
458
|
this.ledger.appendEvent({ streamId: wakeStream(queued.id), ts: this.#now(), actor: "supervisor", type: "wake.trigger_coalesced", data: { wakeId: queued.id, triggerRef } });
|
|
425
459
|
return queued;
|
|
@@ -450,7 +484,7 @@ export class Supervisor {
|
|
|
450
484
|
const activeRoot = this.#hasActiveRoot();
|
|
451
485
|
const hasChildMotion = this.teamList().some((member) => member.agent !== "ceo" && !["idle_unplanned", "retired"].includes(member.status));
|
|
452
486
|
const hasReview = Boolean(output.nextWakeAt);
|
|
453
|
-
const hasBlocker =
|
|
487
|
+
const hasBlocker = handoffBlocked(output.handoff);
|
|
454
488
|
const asksHuman = output.mail.some((mail) => mail.to === "human" && (mail.level === "decision" || mail.level === "emergency"))
|
|
455
489
|
|| this.ledger.unreadMail("human").some((mail) => mail.from === "ceo" && (mail.level === "decision" || mail.level === "emergency"));
|
|
456
490
|
if (idle.length === 0 && missingObservationGoalIds.length === 0 && (!activeRoot || hasChildMotion || hasReview || hasBlocker || asksHuman))
|
|
@@ -463,10 +497,61 @@ export class Supervisor {
|
|
|
463
497
|
this.ledger.appendEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: "supervisor", type: "ceo.motion_invalid", data: violation });
|
|
464
498
|
throw new Error(`CEO motion invalid: ${violation.reason}${violation.idleAgents.length ? ` (${violation.idleAgents.join(", ")})` : ""}`);
|
|
465
499
|
}
|
|
466
|
-
#
|
|
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) {
|
|
510
|
+
const record = this.ledger.workRecord(binding.goalId);
|
|
511
|
+
if (!record)
|
|
512
|
+
throw new Error("Goal Work Record is missing");
|
|
513
|
+
const legacy = "goalId" in output.handoff ? null : output.handoff;
|
|
514
|
+
const handoff = {
|
|
515
|
+
goalId: binding.goalId,
|
|
516
|
+
goalRevision: binding.goalRevision,
|
|
517
|
+
recordRevision: record.recordRevision,
|
|
518
|
+
outcome: "goalId" in output.handoff ? output.handoff.outcome : legacy?.blocker ? "blocked" : legacy?.material ? "completion_proposed" : "progress",
|
|
519
|
+
evidence: "goalId" in output.handoff && output.handoff.evidence.length ? output.handoff.evidence : record.evidence,
|
|
520
|
+
};
|
|
521
|
+
return { ...output, handoff };
|
|
522
|
+
}
|
|
523
|
+
#turnContext(wake) {
|
|
524
|
+
if (interactionMailId(wake))
|
|
525
|
+
return { source: { kind: "human" } };
|
|
526
|
+
const goals = this.ledger.goalsForOwner(wake.agent).filter((goal) => goal.phase === "active");
|
|
527
|
+
const goal = goals.find((candidate) => candidate.parentId === null) ?? goals[0];
|
|
528
|
+
const humanMail = wake.triggerRef.startsWith("mail:") && this.ledger.mailbox().some((mail) => mail.id === wake.triggerRef.slice(5) && mail.from === "human");
|
|
529
|
+
return { source: humanMail ? { kind: "human" } : { kind: "goal_driver", round: goal ? (this.ledger.workRecord(goal.id)?.recordRevision ?? 0) + 1 : 1 }, ...(goal ? { goalBinding: { goalId: goal.id, goalRevision: goal.revision } } : {}) };
|
|
530
|
+
}
|
|
531
|
+
#loadContext(wake, turn) {
|
|
467
532
|
const profile = this.#profiles.get(wake.agent) ?? { agent: wake.agent, role: "child" };
|
|
468
533
|
const role = profile.role;
|
|
469
534
|
const capabilities = profile.capabilities ?? defaultCapabilities(role);
|
|
535
|
+
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
|
+
}
|
|
470
555
|
const goals = role === "ceo" ? this.ledger.goals() : this.ledger.goalsForOwner(wake.agent);
|
|
471
556
|
const mail = this.ledger.unreadMail(wake.agent);
|
|
472
557
|
const handoff = this.ledger.lastEvent(wake.agent, "handoff.recorded");
|
|
@@ -480,8 +565,14 @@ export class Supervisor {
|
|
|
480
565
|
const actions = this.ledger.actions().filter((action) => action.agent === wake.agent && (action.status === "unknown" || Boolean(action.auditAdvice && !action.adviceAcked)));
|
|
481
566
|
const revisionWarnings = goals.flatMap((goal) => this.#goalRevisionWarning(goal));
|
|
482
567
|
const workingMemory = selectWorkingMemory(this.ledger.readStream(memoryStream(wake.agent)), this.#memoryTailChars);
|
|
483
|
-
const
|
|
484
|
-
|
|
568
|
+
const active = composeActiveContext({ role, capabilities, systemPrompt: profile.systemPrompt ?? defaultRolePrompt(role), wake, goals, mail, actions, lastHandoff: handoff, teamHandoffs, team: role === "ceo" ? this.teamList() : [], revisionWarnings, recoveryEvents, workingMemory });
|
|
569
|
+
const records = this.ledger.workRecords();
|
|
570
|
+
const currentRecord = turn.goalBinding ? this.ledger.workRecord(turn.goalBinding.goalId) : null;
|
|
571
|
+
const currentGoal = turn.goalBinding ? this.ledger.goal(turn.goalBinding.goalId) : null;
|
|
572
|
+
const parentRecord = currentGoal?.parentId ? this.ledger.workRecord(currentGoal.parentId) : null;
|
|
573
|
+
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
|
+
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 } : {}) };
|
|
485
576
|
}
|
|
486
577
|
#requiredConnector(name) { const value = this.#connectors.get(name); if (!value)
|
|
487
578
|
throw new Error(`connector not registered: ${name}`); return value; }
|
|
@@ -507,7 +598,7 @@ export class Supervisor {
|
|
|
507
598
|
throw new Error(warnings.join(" "));
|
|
508
599
|
}
|
|
509
600
|
#now() { return this.clock.now().toISOString(); }
|
|
510
|
-
async #agentRpc(wake, leaseToken, method, params) {
|
|
601
|
+
async #agentRpc(wake, turn, leaseToken, method, params) {
|
|
511
602
|
const current = this.ledger.wake(wake.id);
|
|
512
603
|
if (!current || current.status !== "running" || current.leaseToken !== leaseToken || !current.leaseUntil || current.leaseUntil < this.#now())
|
|
513
604
|
throw new Error("stale runner RPC rejected");
|
|
@@ -515,12 +606,51 @@ export class Supervisor {
|
|
|
515
606
|
const allowed = new Set(profile.capabilities ?? defaultCapabilities(profile.role));
|
|
516
607
|
if (!allowed.has(method))
|
|
517
608
|
throw new Error(`${profile.role} agent is not allowed to call ${method}`);
|
|
609
|
+
if (!turn.goalBinding && goalBoundCapability(method))
|
|
610
|
+
throw new Error(`${method} requires a Goal-bound Turn`);
|
|
518
611
|
this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: `rpc.${method}`, data: params }, leaseToken);
|
|
519
612
|
const input = asRecord(params);
|
|
520
613
|
if (method === "ledger.search")
|
|
521
614
|
return this.ledger.searchEvents(String(input.query), Number(input.limit ?? 20));
|
|
522
615
|
if (method === "team.list")
|
|
523
616
|
return this.teamList();
|
|
617
|
+
if (method === "goal.get")
|
|
618
|
+
return (turn.goalBinding ? this.ledger.goal(turn.goalBinding.goalId) : null);
|
|
619
|
+
if (method === "goal.create") {
|
|
620
|
+
if (turn.source.kind !== "human" || turn.goalBinding)
|
|
621
|
+
throw new Error("a Root Goal can only be created from an unbound Human Turn");
|
|
622
|
+
if (wake.agent !== "ceo")
|
|
623
|
+
throw new Error("only CEO may translate Human intent into a Root Goal");
|
|
624
|
+
const goal = this.createRootGoal(String(input.objective), typeof input.id === "string" && input.id.trim() ? input.id : undefined);
|
|
625
|
+
turn.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
|
|
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." };
|
|
628
|
+
}
|
|
629
|
+
if (method === "goal.work") {
|
|
630
|
+
if (turn.source.kind !== "human" || turn.goalBinding)
|
|
631
|
+
throw new Error("work_on_goal requires an unbound Human Turn");
|
|
632
|
+
const goal = this.#goal(String(input.goalId));
|
|
633
|
+
if (goal.owner !== wake.agent || goal.phase !== "active")
|
|
634
|
+
throw new Error("work_on_goal requires an active Goal owned by this Agent");
|
|
635
|
+
turn.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
|
|
636
|
+
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);
|
|
637
|
+
return { goal, goalBinding: turn.goalBinding, instruction: "This Turn is now Goal-bound. Update its Work Record and finish with handoff." };
|
|
638
|
+
}
|
|
639
|
+
if (method === "work_record.list")
|
|
640
|
+
return this.ledger.workRecords();
|
|
641
|
+
if (method === "work_record.read")
|
|
642
|
+
return this.ledger.workRecord(String(input.goalId ?? turn.goalBinding?.goalId ?? ""));
|
|
643
|
+
if (method === "work_record.history")
|
|
644
|
+
return this.ledger.workRecordHistory(String(input.goalId ?? turn.goalBinding?.goalId ?? ""));
|
|
645
|
+
if (method === "work_record.diff")
|
|
646
|
+
return this.ledger.workRecordDiff(String(input.goalId ?? turn.goalBinding?.goalId ?? ""), Number(input.fromRevision), Number(input.toRevision));
|
|
647
|
+
if (method === "work_record.search")
|
|
648
|
+
return this.ledger.searchWorkRecords(String(input.query), Number(input.limit ?? 20));
|
|
649
|
+
if (method === "work_record.update") {
|
|
650
|
+
if (!turn.goalBinding)
|
|
651
|
+
throw new Error("work_record.update requires a Goal-bound Turn");
|
|
652
|
+
return this.ledger.updateWorkRecord({ goalId: turn.goalBinding.goalId, goalRevision: turn.goalBinding.goalRevision, expectedRevision: Number(input.expectedRevision), content: String(input.content), reason: String(input.reason), evidence: numberArray(input.evidence), turnId: wake.id, wakeId: wake.id }, wake.agent);
|
|
653
|
+
}
|
|
524
654
|
if (method === "goal.delegate")
|
|
525
655
|
return this.delegate({
|
|
526
656
|
id: String(input.id),
|
|
@@ -540,11 +670,28 @@ export class Supervisor {
|
|
|
540
670
|
evidence: numberArray(input.evidence),
|
|
541
671
|
}, wake.agent, wake.id);
|
|
542
672
|
if (method === "goal.revise")
|
|
543
|
-
return this.reviseChildGoal(String(input.goalId), String(input.objective), String(input.observationMethod), wake.agent, String(input.reason), numberArray(input.evidence), wake.id);
|
|
544
|
-
if (method === "goal.pause" || method === "goal.resume")
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
673
|
+
return this.reviseChildGoal(String(input.goalId), String(input.objective), String(input.observationMethod), String(input.verificationMethod), wake.agent, String(input.reason), numberArray(input.evidence), wake.id);
|
|
674
|
+
if (method === "goal.pause" || method === "goal.resume") {
|
|
675
|
+
const goalId = String(input.goalId);
|
|
676
|
+
const currentGoal = this.#goal(goalId);
|
|
677
|
+
const directHumanRoot = !turn.goalBinding && turn.source.kind === "human" && currentGoal.parentId === null;
|
|
678
|
+
if (!turn.goalBinding && !directHumanRoot)
|
|
679
|
+
throw new Error(`${method} requires a Goal-bound Turn`);
|
|
680
|
+
const goal = this.transitionGoal(goalId, method === "goal.pause" ? "paused" : "active", directHumanRoot ? "human" : wake.agent);
|
|
681
|
+
if (method === "goal.resume" && directHumanRoot) {
|
|
682
|
+
turn.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
|
|
683
|
+
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);
|
|
684
|
+
return { goal, goalBinding: turn.goalBinding, instruction: "This Turn is now Goal-bound. Update its Work Record and finish with handoff." };
|
|
685
|
+
}
|
|
686
|
+
return goal;
|
|
687
|
+
}
|
|
688
|
+
if (method === "goal.complete") {
|
|
689
|
+
const currentGoal = this.#goal(String(input.goalId));
|
|
690
|
+
const directHumanRoot = !turn.goalBinding && turn.source.kind === "human" && currentGoal.parentId === null;
|
|
691
|
+
if (!turn.goalBinding && !directHumanRoot)
|
|
692
|
+
throw new Error("goal.complete requires a Goal-bound Turn");
|
|
693
|
+
return this.completeGoal({ goalId: currentGoal.id, revision: Number(input.revision), reason: String(input.reason), evidence: numberArray(input.evidence) }, directHumanRoot ? "human" : wake.agent, wake.id);
|
|
694
|
+
}
|
|
548
695
|
if (method === "human.request") {
|
|
549
696
|
const evidence = numberArray(input.evidence);
|
|
550
697
|
for (const seq of evidence)
|
|
@@ -648,7 +795,7 @@ export function deriveTeam(ledger, now = new Date().toISOString()) {
|
|
|
648
795
|
const nextWakeAt = schedules.filter((schedule) => schedule.agent === agent && schedule.nextWakeAt > now).map((schedule) => schedule.nextWakeAt).sort()[0] ?? null;
|
|
649
796
|
const lastHandoff = [...handoffs].reverse().find((event) => event.actor === agent) ?? null;
|
|
650
797
|
const blocker = lastHandoff && typeof lastHandoff.data === "object" && lastHandoff.data !== null && !Array.isArray(lastHandoff.data)
|
|
651
|
-
? lastHandoff.data.blocker
|
|
798
|
+
? (lastHandoff.data.blocker ?? (lastHandoff.data.outcome === "blocked" ? "blocked" : null))
|
|
652
799
|
: null;
|
|
653
800
|
let status;
|
|
654
801
|
if (live.length === 0)
|
|
@@ -715,22 +862,26 @@ function minimalEnvironment(explicit = {}) {
|
|
|
715
862
|
return { ...env, ...explicit };
|
|
716
863
|
}
|
|
717
864
|
function escapeHtml(value) { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); }
|
|
865
|
+
function interactionMailId(wake) { return wake.triggerRef.startsWith("interaction:") ? wake.triggerRef.slice("interaction:".length) : null; }
|
|
866
|
+
function handoffBlocked(handoff) { return "goalId" in handoff ? handoff.outcome === "blocked" : Boolean(handoff.blocker); }
|
|
867
|
+
function handoffTriggersCeo(handoff) { return "goalId" in handoff ? handoff.outcome === "blocked" || handoff.outcome === "completion_proposed" : Boolean(handoff.material || handoff.blocker); }
|
|
868
|
+
function goalBoundCapability(method) { return ["goal.delegate", "goal.reassign", "goal.revise", "goal.put", "work_record.update", "mail.send", "schedule.set", "human.request"].includes(method); }
|
|
718
869
|
function asRecord(value) { if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
719
870
|
throw new Error("RPC params must be an object"); return value; }
|
|
720
871
|
function numberArray(value) { if (!Array.isArray(value) || value.some((item) => typeof item !== "number"))
|
|
721
872
|
throw new Error("RPC evidence must be a number array"); return value; }
|
|
722
873
|
function asChildGoal(value) {
|
|
723
874
|
const input = asRecord(value ?? null);
|
|
724
|
-
return { id: String(input.id), objective: String(input.objective), observationMethod: String(input.observationMethod), owner: String(input.owner) };
|
|
875
|
+
return { id: String(input.id), objective: String(input.objective), observationMethod: String(input.observationMethod), verificationMethod: String(input.verificationMethod), owner: String(input.owner) };
|
|
725
876
|
}
|
|
726
877
|
function defaultCapabilities(role) {
|
|
727
878
|
if (role === "ceo")
|
|
728
|
-
return ["ledger.search", "mail.send", "schedule.set", "action.submit", "audit.ack", "
|
|
879
|
+
return ["ledger.search", "mail.send", "schedule.set", "action.submit", "audit.ack", "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"];
|
|
729
880
|
if (role === "verifier")
|
|
730
881
|
return ["ledger.search", "mail.send", "memory.append", "audit.write"];
|
|
731
882
|
if (role === "audit")
|
|
732
883
|
return ["ledger.search", "mail.send", "memory.append", "audit.write"];
|
|
733
|
-
return ["ledger.search", "mail.send", "schedule.set", "action.submit", "audit.ack", "
|
|
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"];
|
|
734
885
|
}
|
|
735
886
|
export * from "./verification.js";
|
|
736
887
|
export * from "./roles.js";
|