@goah/cli 0.13.3 → 0.13.5
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/{chunk-7E7V6OIU.js → chunk-24NAHA3D.js} +16 -6
- package/dist/{chunk-YH6YJ2IB.js → chunk-CDELLMIJ.js} +37 -10
- package/dist/{chunk-D2DVAH37.js → chunk-J32EKOQ4.js} +1 -1
- package/dist/{chunk-KJ4HJCTK.js → chunk-ORAANUAR.js} +18 -25
- package/dist/{chunk-QBD6EJ2Z.js → chunk-RDUUXBZ2.js} +114 -51
- package/dist/cli.js +38 -18
- package/dist/console/assets/{index-B1vGMXKr.js → index-DYdJZskp.js} +5 -5
- package/dist/console/assets/{index-B1vGMXKr.js.map → index-DYdJZskp.js.map} +1 -1
- package/dist/console/index.html +1 -1
- package/dist/{dist-KNR2XL6J.js → dist-EYYNPOYG.js} +1 -1
- package/dist/execution.d.ts +29 -6
- package/dist/faux-runner-worker.js +13 -6
- package/dist/index.d.ts +29 -6
- package/dist/index.js +5 -5
- package/dist/pi-worker.js +59 -17
- package/dist/runner-pi.d.ts +26 -9
- package/dist/runner-pi.js +1 -1
- package/dist/{runner-registry-GD4BYCQC.js → runner-registry-XCGUS2P3.js} +2 -2
- package/dist/sqlite.d.ts +4 -2
- package/dist/sqlite.js +1 -1
- package/dist/supervisor.d.ts +27 -5
- package/dist/supervisor.js +1 -1
- package/dist/testkit.d.ts +26 -7
- package/dist/testkit.js +5 -1
- package/package.json +1 -1
|
@@ -124,18 +124,23 @@ var PiRunnerAdapter = class {
|
|
|
124
124
|
}
|
|
125
125
|
async #run(request) {
|
|
126
126
|
const runnerSession = await this.driver.createRunnerSession(request);
|
|
127
|
+
let messageSequence = 0;
|
|
127
128
|
try {
|
|
128
129
|
while (true) {
|
|
129
130
|
const step = await runnerSession.step();
|
|
130
131
|
for (const trace of step.trace ?? [])
|
|
131
132
|
request.emit(trace);
|
|
132
|
-
if (step.response)
|
|
133
|
-
|
|
133
|
+
if (step.response) {
|
|
134
|
+
const finalMessageId = `adapter:${request.execution.id}:attempt:${request.execution.attempt}:message:${++messageSequence}`;
|
|
135
|
+
request.emit({ type: "message.assistant.completed", data: { message: { id: finalMessageId, role: "assistant", content: [{ type: "text", text: step.response.content }] }, commitState: "committed" } });
|
|
136
|
+
return { outcome: "completed", finalMessageId };
|
|
137
|
+
}
|
|
134
138
|
if (step.handoff) {
|
|
135
|
-
const
|
|
139
|
+
const messageItemId = `adapter:${request.execution.id}:attempt:${request.execution.attempt}:message:${++messageSequence}`;
|
|
140
|
+
const validation = await request.rpc?.("goal.handoff.validate", { handoff: step.handoff.handoff, candidateMessageId: messageItemId, candidateMessage: step.handoff.response.content });
|
|
136
141
|
if (!validation)
|
|
137
142
|
return { outcome: "abnormal", reason: "Handoff validation is unavailable" };
|
|
138
|
-
request.emit({ type: "message.assistant.completed", data: { message: { id:
|
|
143
|
+
request.emit({ type: "message.assistant.completed", data: { message: { id: messageItemId, role: "assistant", content: [{ type: "text", text: step.handoff.response.content }], stopReason: "toolUse" }, commitState: "provisional" } });
|
|
139
144
|
if (!validation.accepted) {
|
|
140
145
|
if (validation.fatal)
|
|
141
146
|
return { outcome: "abnormal", reason: validation.issues.map((issue) => issue.message).join("; ") };
|
|
@@ -143,9 +148,11 @@ var PiRunnerAdapter = class {
|
|
|
143
148
|
request.emit({ type: "runner.handoff_rejected", data: { attemptId: validation.attemptId, issues: validation.issues } });
|
|
144
149
|
continue;
|
|
145
150
|
}
|
|
151
|
+
if (validation.messageItemId !== messageItemId)
|
|
152
|
+
return { outcome: "abnormal", reason: "Handoff validation returned a different assistant message identity" };
|
|
146
153
|
const output = { validationAttemptId: validation.attemptId, validationToken: validation.token, handoff: step.handoff.handoff };
|
|
147
154
|
assertTurnOutput(output);
|
|
148
|
-
return { outcome: "completed",
|
|
155
|
+
return { outcome: "completed", finalMessageId: messageItemId, handoff: output };
|
|
149
156
|
}
|
|
150
157
|
if (step.stopped)
|
|
151
158
|
return { outcome: "abnormal", reason: request.turn.goalCommitment ? "runner stopped without a readable response and valid handoff" : "runner stopped without a response" };
|
|
@@ -221,6 +228,8 @@ var ProcessRunner = class {
|
|
|
221
228
|
const message = JSON.parse(line);
|
|
222
229
|
if (message.type === "trace")
|
|
223
230
|
request.emit(message.event);
|
|
231
|
+
else if (message.type === "live")
|
|
232
|
+
request.emitLive?.(message.event);
|
|
224
233
|
else if (message.type === "steer_ack") {
|
|
225
234
|
const pending = pendingSteering.get(message.id);
|
|
226
235
|
if (pending) {
|
|
@@ -390,7 +399,8 @@ async function runProcessWorker(run) {
|
|
|
390
399
|
deliverSteer(message.id, message.message);
|
|
391
400
|
} };
|
|
392
401
|
const result = await run(start.request, (event) => process.stdout.write(`${JSON.stringify({ type: "trace", event })}
|
|
393
|
-
`), rpc, controls)
|
|
402
|
+
`), rpc, controls, (event) => process.stdout.write(`${JSON.stringify({ type: "live", event })}
|
|
403
|
+
`));
|
|
394
404
|
process.stdout.write(`${JSON.stringify({ type: "result", result })}
|
|
395
405
|
`);
|
|
396
406
|
input.close();
|
|
@@ -751,9 +751,25 @@ var SqliteLedger = class {
|
|
|
751
751
|
return this.#transaction(() => this.#repairOpenTurnItems(id, reason, now, actor));
|
|
752
752
|
}
|
|
753
753
|
finishTurn(id, status, error, now, actor, mailIds = []) {
|
|
754
|
+
if (status === "completed")
|
|
755
|
+
throw new Error("successful Turn completion requires commitTurnResponse or commitHandoff");
|
|
754
756
|
const current = this.#requiredTurn(id);
|
|
755
757
|
return this.#transaction(() => this.#finishTurn(current, status, error, now, actor, mailIds));
|
|
756
758
|
}
|
|
759
|
+
commitTurnResponse(id, responseItemId, now, actor, mailIds = []) {
|
|
760
|
+
return this.#transaction(() => {
|
|
761
|
+
const current = this.#requiredTurn(id);
|
|
762
|
+
if (current.goalId !== null)
|
|
763
|
+
throw new Error("Goal-bound Turn response requires commitHandoff");
|
|
764
|
+
const responseItem = this.turnItems(id).find((item) => item.id === responseItemId);
|
|
765
|
+
const text = responseItem && responseItem.type === "assistant_message" && responseItem.status === "completed" ? normalizeAssistantText(String(responseItem.data.text ?? "")) : "";
|
|
766
|
+
if (!responseItem || !text)
|
|
767
|
+
throw new Error("Turn response does not match a readable Assistant Item");
|
|
768
|
+
const streamId = `turn:${id}`;
|
|
769
|
+
this.#insertEvent({ streamId, ts: now, actor, type: "response.committed", data: { messageItemId: responseItem.id, text } });
|
|
770
|
+
return this.#finishTurn(current, "completed", null, now, actor, mailIds);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
757
773
|
releaseTurnProcess(id, actor) {
|
|
758
774
|
if (actor !== "supervisor")
|
|
759
775
|
throw new Error("only supervisor may release Runner process ownership");
|
|
@@ -1451,25 +1467,36 @@ var SqliteLedger = class {
|
|
|
1451
1467
|
} else {
|
|
1452
1468
|
if (event.type !== `turn.${turn.status}` || turn.attempt !== current.attempt || turn.runnerPid !== current.runnerPid)
|
|
1453
1469
|
throw new Error(`turn event ${event.seq} has an invalid terminal transition`);
|
|
1454
|
-
if (turn.status === "completed"
|
|
1455
|
-
|
|
1470
|
+
if (turn.status === "completed") {
|
|
1471
|
+
if (turn.goalId !== null)
|
|
1472
|
+
this.#assertReplayGoalCompletion(turn, event);
|
|
1473
|
+
else
|
|
1474
|
+
this.#assertReplayResponseCompletion(turn, event);
|
|
1475
|
+
}
|
|
1456
1476
|
}
|
|
1457
1477
|
}
|
|
1478
|
+
#assertReplayResponseCompletion(turn, event) {
|
|
1479
|
+
const responseRow = this.db.prepare("SELECT data FROM events WHERE stream_id=? AND type='response.committed' AND seq<? ORDER BY seq DESC LIMIT 1").get(`turn:${turn.id}`, event.seq);
|
|
1480
|
+
if (!responseRow)
|
|
1481
|
+
throw new Error(`turn event ${event.seq} completes without a committed response`);
|
|
1482
|
+
const response = JSON.parse(responseRow.data);
|
|
1483
|
+
const responseItem = this.turnItems(turn.id).find((item) => item.id === response.messageItemId && item.type === "assistant_message" && item.status === "completed");
|
|
1484
|
+
if (!responseItem || normalizeAssistantText(String(responseItem.data.text ?? "")) !== response.text)
|
|
1485
|
+
throw new Error(`turn event ${event.seq} has a mismatched committed response`);
|
|
1486
|
+
return responseItem;
|
|
1487
|
+
}
|
|
1458
1488
|
#assertReplayGoalCompletion(turn, event) {
|
|
1459
1489
|
if (turn.goalId === null || turn.goalRevision === null)
|
|
1460
1490
|
throw new Error(`turn event ${event.seq} has no Goal commitment`);
|
|
1461
1491
|
const items = this.turnItems(turn.id);
|
|
1462
|
-
|
|
1492
|
+
this.#assertReplayResponseCompletion(turn, event);
|
|
1463
1493
|
const handoffItem = items.findLast((item) => item.type === "handoff" && item.status === "completed");
|
|
1464
|
-
const responseRow = this.db.prepare("SELECT data FROM events WHERE stream_id=? AND type='response.committed' AND seq<? ORDER BY seq DESC LIMIT 1").get(`turn:${turn.id}`, event.seq);
|
|
1465
1494
|
const handoffRow = this.db.prepare("SELECT data FROM events WHERE stream_id=? AND type='handoff.recorded' AND seq<? ORDER BY seq DESC LIMIT 1").get(`turn:${turn.id}`, event.seq);
|
|
1466
|
-
if (!
|
|
1467
|
-
throw new Error(`turn event ${event.seq} completes a Goal without
|
|
1468
|
-
const response = JSON.parse(responseRow.data);
|
|
1495
|
+
if (!handoffRow || !handoffItem)
|
|
1496
|
+
throw new Error(`turn event ${event.seq} completes a Goal without Handoff`);
|
|
1469
1497
|
const handoff = JSON.parse(handoffRow.data);
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
throw new Error(`turn event ${event.seq} has mismatched response or Handoff facts`);
|
|
1498
|
+
if (!isDeepStrictEqual(handoffItem.data, handoff))
|
|
1499
|
+
throw new Error(`turn event ${event.seq} has mismatched Handoff facts`);
|
|
1473
1500
|
assertHandoff(handoff);
|
|
1474
1501
|
const record = this.workRecord(turn.goalId);
|
|
1475
1502
|
if (handoff.goalId !== turn.goalId || handoff.goalRevision !== turn.goalRevision || record?.recordRevision !== handoff.recordRevision)
|
|
@@ -3,7 +3,7 @@ const require = __goahCreateRequire(import.meta.url);
|
|
|
3
3
|
import {
|
|
4
4
|
createPiProcessRunner,
|
|
5
5
|
piRunnerConfigurator
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-24NAHA3D.js";
|
|
7
7
|
|
|
8
8
|
// node_modules/.dist-original/runner-registry.js
|
|
9
9
|
var plugins = /* @__PURE__ */ new Map([
|
|
@@ -2,30 +2,27 @@ import { createRequire as __goahCreateRequire } from "node:module";
|
|
|
2
2
|
const require = __goahCreateRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
runnerPlugin
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-J32EKOQ4.js";
|
|
6
6
|
import {
|
|
7
7
|
SQLITE_SCHEMA_VERSION,
|
|
8
8
|
SqliteLedger
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-CDELLMIJ.js";
|
|
10
10
|
import {
|
|
11
11
|
RunnerRouter,
|
|
12
12
|
Supervisor,
|
|
13
13
|
deriveRecoveryViews
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-RDUUXBZ2.js";
|
|
15
15
|
import {
|
|
16
16
|
ProcessRunner,
|
|
17
17
|
piWorkerPath,
|
|
18
18
|
resolveEnvSpec
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-24NAHA3D.js";
|
|
20
20
|
import {
|
|
21
21
|
createPiModel
|
|
22
22
|
} from "./chunk-6M3OOCKO.js";
|
|
23
23
|
import {
|
|
24
24
|
replayTranscript
|
|
25
25
|
} from "./chunk-R6I3VKME.js";
|
|
26
|
-
import {
|
|
27
|
-
normalizeAssistantText
|
|
28
|
-
} from "./chunk-TJON22J4.js";
|
|
29
26
|
|
|
30
27
|
// node_modules/.dist-original/index.js
|
|
31
28
|
import { accessSync, chmodSync as chmodSync3, constants, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, closeSync, readFileSync as readFileSync3, renameSync, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -340,13 +337,13 @@ async function dispatch(request, socket, supervisor, ledger, reloadRuntime, stop
|
|
|
340
337
|
if (request.op === "goal.interact") {
|
|
341
338
|
const accepted = await supervisor.startHumanGoalTurn(request.objective, request.id);
|
|
342
339
|
write(socket, { type: "accepted", turnId: accepted.turnId, value: accepted });
|
|
343
|
-
for await (const frame of turnFrames(accepted.turnId, ledger, () => !socket.destroyed))
|
|
340
|
+
for await (const frame of turnFrames(accepted.turnId, supervisor, ledger, () => !socket.destroyed))
|
|
344
341
|
write(socket, frame);
|
|
345
342
|
socket.end();
|
|
346
343
|
return;
|
|
347
344
|
}
|
|
348
345
|
if (request.op === "turn.attach") {
|
|
349
|
-
for await (const frame of turnFrames(request.turnId, ledger, () => !socket.destroyed))
|
|
346
|
+
for await (const frame of turnFrames(request.turnId, supervisor, ledger, () => !socket.destroyed))
|
|
350
347
|
write(socket, frame);
|
|
351
348
|
socket.end();
|
|
352
349
|
return;
|
|
@@ -416,10 +413,11 @@ async function* interactFrames(message, supervisor, ledger, isActive = () => tru
|
|
|
416
413
|
throw new Error("message is required");
|
|
417
414
|
const accepted = await supervisor.startHumanTurn(message);
|
|
418
415
|
yield { type: "accepted", turnId: accepted.turnId, value: accepted };
|
|
419
|
-
yield* turnFrames(accepted.turnId, ledger, isActive);
|
|
416
|
+
yield* turnFrames(accepted.turnId, supervisor, ledger, isActive);
|
|
420
417
|
}
|
|
421
|
-
async function* turnFrames(turnId, ledger, isActive) {
|
|
418
|
+
async function* turnFrames(turnId, supervisor, ledger, isActive) {
|
|
422
419
|
let nextStreamSeq = 1;
|
|
420
|
+
let liveRevision = 0;
|
|
423
421
|
const turn = ledger.turn(turnId);
|
|
424
422
|
if (!turn)
|
|
425
423
|
throw new Error("Turn not found");
|
|
@@ -431,19 +429,24 @@ async function* turnFrames(turnId, ledger, isActive) {
|
|
|
431
429
|
yield { type: "event", event };
|
|
432
430
|
}
|
|
433
431
|
};
|
|
432
|
+
const drainLive = async function* () {
|
|
433
|
+
const live = supervisor.liveTurnSnapshot(turnId, liveRevision);
|
|
434
|
+
if (!live)
|
|
435
|
+
return;
|
|
436
|
+
liveRevision = live.revision;
|
|
437
|
+
yield { type: "event", event: { type: "message.assistant.live", data: live } };
|
|
438
|
+
};
|
|
434
439
|
while (true) {
|
|
435
440
|
if (!isActive())
|
|
436
441
|
return;
|
|
437
442
|
yield* drain();
|
|
443
|
+
yield* drainLive();
|
|
438
444
|
const current = ledger.turn(turnId);
|
|
439
445
|
if (!current)
|
|
440
446
|
throw new Error("Turn disappeared");
|
|
441
447
|
if (current.status !== "in_progress") {
|
|
442
448
|
yield* drain();
|
|
443
|
-
|
|
444
|
-
const answerText = String(answer?.data?.text ?? "");
|
|
445
|
-
const streamed = answerText !== "" && ledger.readStream(`turn:${turnId}`).some((event) => event.type === "response.committed" ? event.data.text === answerText : event.type === "message.assistant.completed" && assistantEventText(event.data) === answerText);
|
|
446
|
-
yield { type: "result", value: { turn: { ...current, leaseToken: null }, ...answer && !streamed ? { response: { content: answerText } } : {} } };
|
|
449
|
+
yield { type: "result", value: { turn: { ...current, leaseToken: null } } };
|
|
447
450
|
return;
|
|
448
451
|
}
|
|
449
452
|
await new Promise((resolve3) => setTimeout(resolve3, 50));
|
|
@@ -461,16 +464,6 @@ function requiredGoal(ledger, id) {
|
|
|
461
464
|
throw new Error("goal not found");
|
|
462
465
|
return goal;
|
|
463
466
|
}
|
|
464
|
-
function assistantEventText(value) {
|
|
465
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || !value.message || typeof value.message !== "object" || Array.isArray(value.message))
|
|
466
|
-
return "";
|
|
467
|
-
const content = value.message.content;
|
|
468
|
-
if (typeof content === "string")
|
|
469
|
-
return normalizeAssistantText(content);
|
|
470
|
-
if (!Array.isArray(content))
|
|
471
|
-
return "";
|
|
472
|
-
return content.map((item) => item && typeof item === "object" && !Array.isArray(item) && item.type === "text" && typeof item.text === "string" ? normalizeAssistantText(item.text) : "").filter(Boolean).join("\n");
|
|
473
|
-
}
|
|
474
467
|
function isTurnPresentationEvent(type) {
|
|
475
468
|
return type.startsWith("message.") || type.startsWith("tool.") || type.startsWith("item.") || type.startsWith("turn.") || type === "response.committed" || type === "handoff.recorded" || type === "transcript.interrupted" || type === "transcript.completed";
|
|
476
469
|
}
|
|
@@ -92,14 +92,20 @@ function field(value, key) {
|
|
|
92
92
|
|
|
93
93
|
// node_modules/goah-supervisor/dist/roles.js
|
|
94
94
|
var prompts = {
|
|
95
|
-
child:
|
|
96
|
-
|
|
95
|
+
child: `Own the assigned Child Goal. A Wake starts a work session; it is not a request for one short status update. Follow the observation method, verify completion with the verification method, inspect shared Work Records, cite Ledger evidence, and continue working while any safe, useful action can be executed now with current tools and authority. Use the Work Record as a durable checkpoint whenever the semantic state changes; updating it does not end the Turn. Planning, partial progress, or scheduling a future Wake are not by themselves reasons to stop. Before Handoff, ask whether another useful action can be completed without new authority, unavailable data, another Agent's result, or the passage of time; if yes, do it now.
|
|
96
|
+
|
|
97
|
+
Handoff is declarative and ends this Turn. Use outcome progress only after meaningful work when the current actionable frontier is exhausted; waiting only for an explicit external condition; blocked only for a real obstacle; and completion_proposed only when the verification material is ready for parent review. Use mail.send to notify or escalate, setting its typed goalId route to the parent Goal when it should wake the parent's next committed Turn; use schedule.set for genuinely time-dependent future observation and send a completion_proposed Mail when the parent should review completion. You are not a task-only worker and cannot redefine or complete your own Goal.`,
|
|
98
|
+
ceo: `You are the user's sole operating interface and the durable CEO identity for this Goal tree. You organize work and execute directly wherever work is not delegated. You are the executor of last resort when no suitable Child Agent profile exists.
|
|
97
99
|
|
|
98
100
|
On the first wake for a root with no observation method, operationalize the goal before claiming measurable progress: inspect the current directory with read/edit/write/bash; read project documentation, configuration, code, scripts, local CLIs, and existing ledger evidence; clarify the objective, constraints, baseline, and meaning of success; identify the data source or qualitative inspection protocol; define freshness, cadence, time zone, sustain window, and missing-data behavior when relevant; list the exact access required; then propose a textual observation method to the human. Ask only for facts or permissions that cannot be discovered locally. Continue safe reversible exploration while waiting. Never invent a baseline, data source, permission, success criterion, or observation result.
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
During every Goal Turn: (1) orient from the root and descendants, each current observation method and revision, Team motion, incoming mail, handoffs, blockers, unknown Tool Calls, and recovery facts; (2) diagnose every active child's motion, ownership, and adherence to its authoritative observation method; (3) decide whether to keep, delegate, revise the objective/method pair, reassign, pause, resume, complete, or escalate; (4) apply organization changes only through the high-level atomic tools; (5) repair every idle_unplanned child and every child missing an observation method; (6) execute the current work frontier instead of stopping at planning; (7) before ending, leave active child motion plus a CEO review, an explicit wait/blocker, a human request, or a completion recommendation carrying evidence produced under the current method.
|
|
103
|
+
|
|
104
|
+
Use goal.delegate rather than separate goal/mail/schedule calls. Execute ambiguous or exploratory work yourself until a stream has a bounded objective, independent observation and verification methods, and a reviewable result; only then create a distinct Goal-owning Agent. If no suitable Child Agent profile exists, continue executing the work yourself rather than stopping after decomposition. Use team.list as the roster source of truth.
|
|
105
|
+
|
|
106
|
+
A Wake starts a work session; it does not imply that the Turn should be short. Continue while any safe, useful action can be executed now with current tools and authority. Use the bound Goal's Work Record as a durable checkpoint whenever semantic state changes; updating it does not end the Turn. Planning, partial progress, decomposition, or scheduling a future Wake are not by themselves reasons to stop. Before Handoff, ask whether another useful action can be completed without new authority, unavailable data, another Agent's result, or the passage of time; if yes, do it now. Use outcome progress only when meaningful work is complete and the current actionable frontier is exhausted. Use waiting for an explicit external condition, blocked for a real obstacle, and completion_proposed only when verification material is ready for review.
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
Handoff is declarative and ends the Turn: use mail.send for explicit organization communication, schedule.set only for genuinely time-dependent future motion, and human.request when Human authority is required. Never claim authority to confirm Root methods, complete, or materially change a Root Goal: request the Human instead.`,
|
|
103
109
|
verifier: "Verify one Turn's handoff claims against its trace and runner facts. Do not trust self-report. Persist concise findings with exact evidence sequences.",
|
|
104
110
|
audit: "Independently reconstruct outcomes from durable facts and external observations. Persist independent audit judgment with memory_append; it never substitutes for fresh evidence."
|
|
105
111
|
};
|
|
@@ -328,6 +334,7 @@ var Supervisor = class {
|
|
|
328
334
|
#goalExecutionBarriers = /* @__PURE__ */ new Map();
|
|
329
335
|
#handoffValidations = /* @__PURE__ */ new Map();
|
|
330
336
|
#handoffAttemptSeq = /* @__PURE__ */ new Map();
|
|
337
|
+
#liveTurns = /* @__PURE__ */ new Map();
|
|
331
338
|
#runner;
|
|
332
339
|
constructor(ledger, runner, clock, options = {}) {
|
|
333
340
|
this.ledger = ledger;
|
|
@@ -494,6 +501,7 @@ var Supervisor = class {
|
|
|
494
501
|
const turn = this.ledger.turn(turnId);
|
|
495
502
|
if (!turn || turn.status !== "in_progress")
|
|
496
503
|
throw new Error("active Turn not found");
|
|
504
|
+
this.#liveTurns.delete(turn.id);
|
|
497
505
|
this.#clearHandoffState(turn.id);
|
|
498
506
|
this.ledger.finishTurn(turn.id, "interrupted", { message: reason }, this.#now(), actor);
|
|
499
507
|
await this.#cleanupTerminalTurn(turn);
|
|
@@ -507,7 +515,7 @@ var Supervisor = class {
|
|
|
507
515
|
let renewal = null;
|
|
508
516
|
let processClean = true;
|
|
509
517
|
try {
|
|
510
|
-
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) });
|
|
518
|
+
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), emitLive: (trace) => this.#recordLiveTrace(initial.id, leaseToken, trace), rpc: (method, params) => this.#agentRpcForTurn(initial.id, agent, turnContext, method, params, sourceWake?.id) });
|
|
511
519
|
this.#handles.set(initial.id, handle);
|
|
512
520
|
if (handle.pid)
|
|
513
521
|
this.ledger.attachTurnProcess(initial.id, leaseToken, handle.pid);
|
|
@@ -550,27 +558,18 @@ var Supervisor = class {
|
|
|
550
558
|
this.#scheduleTurnRecovery(sourceWake, sourceWakeTriggers, current.id, agent);
|
|
551
559
|
return;
|
|
552
560
|
}
|
|
553
|
-
const
|
|
554
|
-
if (!response)
|
|
555
|
-
throw new Error("completed Turn requires a readable assistant response");
|
|
561
|
+
const responseItem = this.#finalAssistantItem(current.id, result.finalMessageId);
|
|
556
562
|
if (turnContext.goalCommitment) {
|
|
557
563
|
if (!result.handoff)
|
|
558
564
|
throw new Error("committed Turn requires Handoff");
|
|
559
|
-
this.#commitTurnGoalWork(current.id, agent, turnContext,
|
|
565
|
+
this.#commitTurnGoalWork(current.id, agent, turnContext, responseItem, result.handoff, sourceWake?.id ?? null, deliveredMailIds, recordRevisionAtStart);
|
|
560
566
|
return;
|
|
561
567
|
}
|
|
562
568
|
if (result.handoff)
|
|
563
569
|
throw new Error("uncommitted Turn cannot finish with Handoff");
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (traced !== response)
|
|
568
|
-
throw new Error("completed Turn response does not match its latest assistant trace");
|
|
569
|
-
if (this.#assistantCommitState(current.id, traced) !== "committed")
|
|
570
|
-
throw new Error("uncommitted Turn requires a committed assistant trace");
|
|
571
|
-
} else
|
|
572
|
-
this.#appendTurnItem(current.id, "assistant_message", { text: response }, agent);
|
|
573
|
-
this.ledger.finishTurn(current.id, "completed", null, this.#now(), "supervisor", deliveredMailIds);
|
|
570
|
+
if (this.#assistantCommitState(current.id, responseItem.id) !== "committed")
|
|
571
|
+
throw new Error("uncommitted Turn requires a committed assistant trace");
|
|
572
|
+
this.ledger.commitTurnResponse(current.id, responseItem.id, this.#now(), "supervisor", deliveredMailIds);
|
|
574
573
|
return;
|
|
575
574
|
} catch (error) {
|
|
576
575
|
if (renewal)
|
|
@@ -727,23 +726,37 @@ ${history}` } : {} };
|
|
|
727
726
|
const facts = this.ledger.readStream(`turn:${turnId}`).filter((event) => event.type === "turn.retry_started").map((event) => `${event.type}: ${JSON.stringify(event.data)}`);
|
|
728
727
|
return [...items, ...facts].join("\n");
|
|
729
728
|
}
|
|
730
|
-
#
|
|
731
|
-
|
|
732
|
-
const event = this.ledger.readStream(`turn:${turnId}`).findLast((candidate) => {
|
|
729
|
+
#assistantMessageEvent(turnId, messageItemId) {
|
|
730
|
+
return this.ledger.readStream(`turn:${turnId}`).findLast((candidate) => {
|
|
733
731
|
if (candidate.type !== "message.assistant.completed" || !candidate.data || typeof candidate.data !== "object" || Array.isArray(candidate.data))
|
|
734
732
|
return false;
|
|
735
733
|
const message = candidate.data.message;
|
|
736
|
-
return
|
|
734
|
+
return message?.id === messageItemId;
|
|
737
735
|
});
|
|
736
|
+
}
|
|
737
|
+
#assistantCommitState(turnId, messageItemId) {
|
|
738
|
+
const event = this.#assistantMessageEvent(turnId, messageItemId);
|
|
738
739
|
if (!event || !event.data || typeof event.data !== "object" || Array.isArray(event.data))
|
|
739
740
|
return null;
|
|
740
741
|
const state = event.data.commitState;
|
|
741
742
|
return state === "committed" || state === "provisional" ? state : null;
|
|
742
743
|
}
|
|
744
|
+
#finalAssistantItem(turnId, messageItemId) {
|
|
745
|
+
const id = messageItemId.trim();
|
|
746
|
+
const item = id ? this.ledger.turnItems(turnId).find((candidate) => candidate.id === id) : void 0;
|
|
747
|
+
const text = item && item.type === "assistant_message" && item.status === "completed" ? normalizeAssistantText(String(item.data.text ?? "")) : "";
|
|
748
|
+
const event = id ? this.#assistantMessageEvent(turnId, id) : void 0;
|
|
749
|
+
const turn = this.ledger.turn(turnId);
|
|
750
|
+
const retryStart = turn && turn.attempt > 1 ? this.ledger.readStream(`turn:${turnId}`).findLast((candidate) => candidate.type === "turn.retry_started" && candidate.data.attempt === turn.attempt) : void 0;
|
|
751
|
+
if (!item || !text || !event || retryStart && event.seq <= retryStart.seq)
|
|
752
|
+
throw new Error("completed Turn finalMessageId must reference a readable Assistant Item from the current attempt");
|
|
753
|
+
return item;
|
|
754
|
+
}
|
|
743
755
|
#failTurn(turnId, reason) {
|
|
744
756
|
const current = this.ledger.turn(turnId);
|
|
745
757
|
if (!current || current.status !== "in_progress")
|
|
746
758
|
return;
|
|
759
|
+
this.#liveTurns.delete(turnId);
|
|
747
760
|
this.#clearHandoffState(turnId);
|
|
748
761
|
this.ledger.finishTurn(turnId, "failed", { message: reason }, this.#now(), "supervisor");
|
|
749
762
|
}
|
|
@@ -751,6 +764,7 @@ ${history}` } : {} };
|
|
|
751
764
|
const turn = this.ledger.turns().find((candidate) => candidate.status === "in_progress" && candidate.goalId === goalId);
|
|
752
765
|
if (!turn)
|
|
753
766
|
return;
|
|
767
|
+
this.#liveTurns.delete(turn.id);
|
|
754
768
|
this.#clearHandoffState(turn.id);
|
|
755
769
|
this.ledger.finishTurn(turn.id, "interrupted", { message: reason }, this.#now(), "supervisor");
|
|
756
770
|
void this.#cleanupTerminalTurn(turn);
|
|
@@ -811,6 +825,10 @@ ${history}` } : {} };
|
|
|
811
825
|
return item;
|
|
812
826
|
}
|
|
813
827
|
#recordTurnTrace(turnId, leaseToken, type, data, actor = "ceo") {
|
|
828
|
+
if (type === "message.assistant.delta") {
|
|
829
|
+
this.#recordLiveTrace(turnId, leaseToken, { type, data });
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
814
832
|
if (type === "transcript.completed" || type === "transcript.interrupted")
|
|
815
833
|
return;
|
|
816
834
|
if (type === "transcript.started" && this.ledger.readStream(`turn:${turnId}`).some((event) => event.type === "transcript.started"))
|
|
@@ -824,26 +842,20 @@ ${history}` } : {} };
|
|
|
824
842
|
}
|
|
825
843
|
this.ledger.appendTurnEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor, type, data }, leaseToken);
|
|
826
844
|
if (type === "message.assistant.completed") {
|
|
845
|
+
this.#liveTurns.delete(turnId);
|
|
827
846
|
const value = data;
|
|
828
847
|
const text = messageTextContent(value.message?.content);
|
|
848
|
+
const reasoning = messageReasoningContent(value.message?.content);
|
|
849
|
+
const id = typeof value.message?.id === "string" ? value.message.id.trim() : "";
|
|
850
|
+
if ((text || reasoning) && !id)
|
|
851
|
+
throw new Error("Runner assistant completion is missing a message Item id");
|
|
852
|
+
if (reasoning)
|
|
853
|
+
this.#appendTurnItem(turnId, "reasoning", { text: reasoning }, actor, `${id}:reasoning`);
|
|
829
854
|
if (text)
|
|
830
|
-
this.#appendTurnItem(turnId, "assistant_message", { text }, actor);
|
|
855
|
+
this.#appendTurnItem(turnId, "assistant_message", { text }, actor, id);
|
|
831
856
|
} else if (type === "plan.updated")
|
|
832
857
|
this.#appendTurnItem(turnId, "plan", data, actor);
|
|
833
|
-
else if (type === "
|
|
834
|
-
const input = data;
|
|
835
|
-
const delta = input.delta;
|
|
836
|
-
if (delta?.type === "thinking_start" || delta?.type === "thinking_delta" || delta?.type === "thinking_end") {
|
|
837
|
-
const turn = this.ledger.turn(turnId);
|
|
838
|
-
const id = `${turnId}:attempt:${turn.attempt}:reasoning:${String(input.messageId ?? "message")}`;
|
|
839
|
-
const existing = this.ledger.turnItems(turnId).find((item) => item.id === id);
|
|
840
|
-
const text = `${String(existing?.data?.text ?? "")}${delta.type === "thinking_delta" ? String(delta.delta ?? "") : ""}`;
|
|
841
|
-
if (!existing)
|
|
842
|
-
this.#appendTurnItem(turnId, "reasoning", { text }, actor, id, delta.type === "thinking_end" ? "completed" : "in_progress");
|
|
843
|
-
else
|
|
844
|
-
this.ledger.putTurnItem({ ...existing, data: { text }, status: delta.type === "thinking_end" ? "completed" : "in_progress", completedAt: delta.type === "thinking_end" ? this.#now() : null }, actor);
|
|
845
|
-
}
|
|
846
|
-
} else if (type === "tool.called") {
|
|
858
|
+
else if (type === "tool.called") {
|
|
847
859
|
const input = data;
|
|
848
860
|
const turn = this.ledger.turn(turnId);
|
|
849
861
|
const id = `${turnId}:attempt:${turn.attempt}:tool:${String(input.callId)}`;
|
|
@@ -861,6 +873,37 @@ ${history}` } : {} };
|
|
|
861
873
|
this.#appendTurnItem(turnId, "tool_result", { callId: String(input.callId), result: input.result ?? null }, actor);
|
|
862
874
|
}
|
|
863
875
|
}
|
|
876
|
+
#recordLiveTrace(turnId, leaseToken, event) {
|
|
877
|
+
const turn = this.ledger.turn(turnId);
|
|
878
|
+
if (!turn || turn.status !== "in_progress" || turn.leaseToken !== leaseToken)
|
|
879
|
+
return;
|
|
880
|
+
const input = event.data;
|
|
881
|
+
const delta = input.delta;
|
|
882
|
+
let state = this.#liveTurns.get(turnId);
|
|
883
|
+
if (!state || state.messageId !== input.messageId || delta.type === "start") {
|
|
884
|
+
state = { revision: 0, messageId: input.messageId, text: /* @__PURE__ */ new Map(), thinking: /* @__PURE__ */ new Map(), thinkingActive: false };
|
|
885
|
+
this.#liveTurns.set(turnId, state);
|
|
886
|
+
}
|
|
887
|
+
const index = delta.contentIndex ?? 0;
|
|
888
|
+
if (delta.type === "text_start")
|
|
889
|
+
state.text.set(index, []);
|
|
890
|
+
else if (delta.type === "text_delta")
|
|
891
|
+
appendLiveChunk(state.text, index, delta.delta);
|
|
892
|
+
else if (delta.type === "thinking_start") {
|
|
893
|
+
state.thinking.set(index, []);
|
|
894
|
+
state.thinkingActive = true;
|
|
895
|
+
} else if (delta.type === "thinking_delta")
|
|
896
|
+
appendLiveChunk(state.thinking, index, delta.delta);
|
|
897
|
+
else if (delta.type === "thinking_end")
|
|
898
|
+
state.thinkingActive = false;
|
|
899
|
+
state.revision += 1;
|
|
900
|
+
}
|
|
901
|
+
liveTurnSnapshot(turnId, afterRevision = 0) {
|
|
902
|
+
const state = this.#liveTurns.get(turnId);
|
|
903
|
+
if (!state || state.revision <= afterRevision)
|
|
904
|
+
return null;
|
|
905
|
+
return { revision: state.revision, messageId: state.messageId, text: joinLiveChunks(state.text), thinking: joinLiveChunks(state.thinking), thinkingActive: state.thinkingActive };
|
|
906
|
+
}
|
|
864
907
|
async sendToCeo(body) {
|
|
865
908
|
const value = body && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
866
909
|
const message = String(value.message ?? "").trim();
|
|
@@ -1195,7 +1238,7 @@ ${history}` } : {} };
|
|
|
1195
1238
|
return agent === "ceo" && role === "ceo";
|
|
1196
1239
|
return role === "verifier" || role === "audit";
|
|
1197
1240
|
}
|
|
1198
|
-
#commitTurnGoalWork(turnId, agent, turn,
|
|
1241
|
+
#commitTurnGoalWork(turnId, agent, turn, responseItem, raw, sourceWakeId, mailIds, revisionAtStart) {
|
|
1199
1242
|
const binding = turn.goalCommitment;
|
|
1200
1243
|
const goal = this.#goal(binding.goalId);
|
|
1201
1244
|
if (goal.revision !== binding.goalRevision)
|
|
@@ -1206,11 +1249,9 @@ ${history}` } : {} };
|
|
|
1206
1249
|
assertTurnOutput(raw);
|
|
1207
1250
|
const validation = this.#handoffValidations.get(raw.validationToken);
|
|
1208
1251
|
const execution = this.ledger.turn(turnId);
|
|
1209
|
-
|
|
1252
|
+
const response = normalizeAssistantText(String(responseItem.data.text ?? ""));
|
|
1253
|
+
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.messageItemId !== responseItem.id || validation.message !== response || validation.handoff.outcome !== raw.handoff.outcome || !isSameNumbers(validation.handoff.evidence, raw.handoff.evidence))
|
|
1210
1254
|
throw new Error("Handoff validation token is stale or does not match this Turn result");
|
|
1211
|
-
const responseItem = [...this.ledger.turnItems(turnId)].reverse().find((item2) => item2.type === "assistant_message" && item2.status === "completed" && item2.data.text === validation.message);
|
|
1212
|
-
if (!responseItem)
|
|
1213
|
-
throw new Error("accepted Handoff response was not recorded in the Turn");
|
|
1214
1255
|
const output = this.#committedTurnOutput(raw, binding);
|
|
1215
1256
|
assertHandoff(output.handoff);
|
|
1216
1257
|
const now = this.#now();
|
|
@@ -1341,7 +1382,8 @@ ${workText}`, activeGoal: turn.activeGoal, goalCommitment: turn.goalCommitment,
|
|
|
1341
1382
|
const input = asRecord(params);
|
|
1342
1383
|
if (method === "goal.handoff.validate") {
|
|
1343
1384
|
const candidate = String(input.candidateMessage ?? "");
|
|
1344
|
-
|
|
1385
|
+
const candidateMessageId = String(input.candidateMessageId ?? "");
|
|
1386
|
+
this.ledger.appendEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor: agent, type: "rpc.goal.handoff.validate", data: { attemptId: handoffAttemptId, handoff: input.handoff ?? null, candidateMessageId, candidateMessagePresent: Boolean(candidate.trim()), candidateMessageChars: candidate.length }, ignorable: true });
|
|
1345
1387
|
return this.#validateHandoffDraft(turnId, handoffAttemptId, agent, execution, context, input);
|
|
1346
1388
|
}
|
|
1347
1389
|
const profile = this.#profiles.get(agent) ?? { agent, role: "child" };
|
|
@@ -1480,17 +1522,19 @@ ${workText}`, activeGoal: turn.activeGoal, goalCommitment: turn.goalCommitment,
|
|
|
1480
1522
|
#validateHandoffDraft(turnId, attemptId, agent, execution, context, input) {
|
|
1481
1523
|
if (!context.goalCommitment || execution.goalId === null)
|
|
1482
1524
|
return { accepted: false, fatal: true, attemptId, issues: [{ code: "turn_not_committed", message: "This Turn no longer has a Goal commitment." }] };
|
|
1483
|
-
const request = { handoff: input.handoff ?? null, candidateMessage: String(input.candidateMessage ?? "") };
|
|
1525
|
+
const request = { handoff: input.handoff ?? null, candidateMessageId: String(input.candidateMessageId ?? "").trim(), candidateMessage: String(input.candidateMessage ?? "") };
|
|
1484
1526
|
const issues = [];
|
|
1485
1527
|
try {
|
|
1486
1528
|
assertAgentHandoff(request.handoff);
|
|
1487
1529
|
} catch {
|
|
1488
1530
|
issues.push({ code: "handoff_invalid", message: "Handoff requires a valid outcome and at least one evidence event." });
|
|
1489
1531
|
}
|
|
1490
|
-
const
|
|
1491
|
-
const
|
|
1492
|
-
if (!message)
|
|
1493
|
-
issues.push({ code: "message_missing", message: "This committed Turn needs a readable assistant message before it can finish." });
|
|
1532
|
+
const message = normalizeAssistantText(request.candidateMessage);
|
|
1533
|
+
const existingMessage = request.candidateMessageId ? this.ledger.turnItems(turnId).find((item) => item.id === request.candidateMessageId) : void 0;
|
|
1534
|
+
if (!request.candidateMessageId || !message)
|
|
1535
|
+
issues.push({ code: "message_missing", message: "This committed Turn needs a readable assistant message Item before it can finish." });
|
|
1536
|
+
else if (existingMessage && (existingMessage.type !== "assistant_message" || existingMessage.status !== "completed" || normalizeAssistantText(String(existingMessage.data.text ?? "")) !== message))
|
|
1537
|
+
issues.push({ code: "message_mismatch", message: "Handoff message identity does not match its assistant Item." });
|
|
1494
1538
|
const goal = this.ledger.goal(context.goalCommitment.goalId);
|
|
1495
1539
|
if (!goal || goal.phase !== "active" || goal.owner !== agent || goal.revision !== context.goalCommitment.goalRevision)
|
|
1496
1540
|
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 commitment." }] };
|
|
@@ -1504,8 +1548,8 @@ ${workText}`, activeGoal: turn.activeGoal, goalCommitment: turn.goalCommitment,
|
|
|
1504
1548
|
if (issues.length)
|
|
1505
1549
|
return { accepted: false, fatal: false, attemptId, issues };
|
|
1506
1550
|
const token = randomUUID();
|
|
1507
|
-
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] } });
|
|
1508
|
-
return { accepted: true, fatal: false, attemptId, token, goalId: goal.id, goalRevision: goal.revision };
|
|
1551
|
+
this.#handoffValidations.set(token, { turnId, attemptId, agent, attempt: execution.attempt, leaseToken: execution.leaseToken, goalId: goal.id, goalRevision: goal.revision, messageItemId: request.candidateMessageId, message, handoff: { outcome: request.handoff.outcome, evidence: [...request.handoff.evidence] } });
|
|
1552
|
+
return { accepted: true, fatal: false, attemptId, token, goalId: goal.id, goalRevision: goal.revision, messageItemId: request.candidateMessageId };
|
|
1509
1553
|
}
|
|
1510
1554
|
#beginHandoffAttempt(turnId) {
|
|
1511
1555
|
this.#invalidateHandoffTokens(turnId);
|
|
@@ -1666,6 +1710,25 @@ function messageTextContent(content) {
|
|
|
1666
1710
|
return "";
|
|
1667
1711
|
return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && part.type === "text" ? normalizeAssistantText(String(part.text ?? "")) : "").filter(Boolean).join("\n");
|
|
1668
1712
|
}
|
|
1713
|
+
function messageReasoningContent(content) {
|
|
1714
|
+
if (!Array.isArray(content))
|
|
1715
|
+
return "";
|
|
1716
|
+
return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && part.type === "thinking" ? normalizeAssistantText(String(part.thinking ?? "")) : "").filter(Boolean).join("\n");
|
|
1717
|
+
}
|
|
1718
|
+
function appendLiveChunk(target, index, delta) {
|
|
1719
|
+
if (!delta)
|
|
1720
|
+
return;
|
|
1721
|
+
const chunks = target.get(index) ?? [];
|
|
1722
|
+
const last = chunks.at(-1);
|
|
1723
|
+
if (last !== void 0 && last.length < 4096)
|
|
1724
|
+
chunks[chunks.length - 1] = last + delta;
|
|
1725
|
+
else
|
|
1726
|
+
chunks.push(delta);
|
|
1727
|
+
target.set(index, chunks);
|
|
1728
|
+
}
|
|
1729
|
+
function joinLiveChunks(target) {
|
|
1730
|
+
return [...target.entries()].sort(([left], [right]) => left - right).map(([, chunks]) => chunks.join("")).join("\n");
|
|
1731
|
+
}
|
|
1669
1732
|
function goalBoundCapability(method) {
|
|
1670
1733
|
return ["goal.delegate", "goal.reassign", "goal.revise", "goal.put", "work_record.update", "schedule.set", "human.request"].includes(method);
|
|
1671
1734
|
}
|