@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.
@@ -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
- return { outcome: "completed", response: step.response };
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 validation = await request.rpc?.("goal.handoff.validate", { handoff: step.handoff.handoff, candidateMessage: step.handoff.response.content });
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: `adapter:${request.execution.id}:handoff:${validation.attemptId}`, role: "assistant", content: [{ type: "text", text: step.handoff.response.content }], stopReason: "toolUse" }, commitState: "provisional" } });
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", response: step.handoff.response, handoff: output };
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" && turn.goalId !== null)
1455
- this.#assertReplayGoalCompletion(turn, event);
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
- const responses = items.filter((item) => item.type === "assistant_message" && item.status === "completed" && typeof item.data.text === "string" && Boolean(item.data.text.trim()));
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 (!responseRow || !handoffRow || !handoffItem)
1467
- throw new Error(`turn event ${event.seq} completes a Goal without response and Handoff`);
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
- const responseItem = responses.find((item) => item.id === response.messageItemId);
1471
- if (!responseItem || responseItem.data.text !== response.text || !isDeepStrictEqual(handoffItem.data, handoff))
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-7E7V6OIU.js";
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-D2DVAH37.js";
5
+ } from "./chunk-J32EKOQ4.js";
6
6
  import {
7
7
  SQLITE_SCHEMA_VERSION,
8
8
  SqliteLedger
9
- } from "./chunk-YH6YJ2IB.js";
9
+ } from "./chunk-CDELLMIJ.js";
10
10
  import {
11
11
  RunnerRouter,
12
12
  Supervisor,
13
13
  deriveRecoveryViews
14
- } from "./chunk-QBD6EJ2Z.js";
14
+ } from "./chunk-RDUUXBZ2.js";
15
15
  import {
16
16
  ProcessRunner,
17
17
  piWorkerPath,
18
18
  resolveEnvSpec
19
- } from "./chunk-7E7V6OIU.js";
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
- const answer = ledger.turnItems(turnId).filter((item) => item.type === "assistant_message").at(-1);
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: "Own the assigned Child Goal. Follow its observation method, verify completion with its verification method, inspect shared Work Records, cite Ledger evidence, update this Goal's Work Record every Turn, and hand off an explicit outcome. Handoff is declarative: 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 to request future motion 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.",
96
- ceo: `You are the user's sole operating interface and the durable CEO identity for this Goal tree. You organize work; you do not impersonate child execution.
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
- On every wake: (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 before handoff; (6) close with 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.
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
- 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. Use team.list as the roster source of truth. Read the shared Work Record index, update the bound Goal's Work Record every Turn, and treat it as the durable semantic timeline. Handoff is declarative: use mail.send for explicit organization communication, schedule.set for 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.`,
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 response = normalizeAssistantText(result.response.content);
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, response, result.handoff, sourceWake?.id ?? null, deliveredMailIds, recordRevisionAtStart);
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
- const latestMessage = this.ledger.turnItems(current.id).findLast((item) => item.type === "assistant_message" && item.status === "completed");
565
- if (latestMessage) {
566
- const traced = normalizeAssistantText(String(latestMessage.data.text ?? ""));
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
- #assistantCommitState(turnId, text) {
731
- const canonical = normalizeAssistantText(text);
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 messageTextContent(message?.content) === canonical;
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 === "message.assistant.delta") {
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, response, raw, sourceWakeId, mailIds, revisionAtStart) {
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
- 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.message !== response || validation.handoff.outcome !== raw.handoff.outcome || !isSameNumbers(validation.handoff.evidence, raw.handoff.evidence))
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
- 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 });
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 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()));
1491
- const message = normalizeAssistantText(request.candidateMessage) || normalizeAssistantText(String(existingMessage?.data?.text ?? ""));
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
  }