@goah/cli 0.13.3 → 0.13.4

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.
@@ -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-R6GKCNTB.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-SQJZ5OFH.js";
15
15
  import {
16
16
  ProcessRunner,
17
17
  piWorkerPath,
18
18
  resolveEnvSpec
19
- } from "./chunk-7E7V6OIU.js";
19
+ } from "./chunk-EK6BTCFG.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";
@@ -440,10 +437,7 @@ async function* turnFrames(turnId, ledger, isActive) {
440
437
  throw new Error("Turn disappeared");
441
438
  if (current.status !== "in_progress") {
442
439
  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 } } : {} } };
440
+ yield { type: "result", value: { turn: { ...current, leaseToken: null } } };
447
441
  return;
448
442
  }
449
443
  await new Promise((resolve3) => setTimeout(resolve3, 50));
@@ -461,16 +455,6 @@ function requiredGoal(ledger, id) {
461
455
  throw new Error("goal not found");
462
456
  return goal;
463
457
  }
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
458
  function isTurnPresentationEvent(type) {
475
459
  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
460
  }
@@ -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)
@@ -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" };
@@ -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-EK6BTCFG.js";
7
7
 
8
8
  // node_modules/.dist-original/runner-registry.js
9
9
  var plugins = /* @__PURE__ */ new Map([
@@ -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
  };
@@ -550,27 +556,18 @@ var Supervisor = class {
550
556
  this.#scheduleTurnRecovery(sourceWake, sourceWakeTriggers, current.id, agent);
551
557
  return;
552
558
  }
553
- const response = normalizeAssistantText(result.response.content);
554
- if (!response)
555
- throw new Error("completed Turn requires a readable assistant response");
559
+ const responseItem = this.#finalAssistantItem(current.id, result.finalMessageId);
556
560
  if (turnContext.goalCommitment) {
557
561
  if (!result.handoff)
558
562
  throw new Error("committed Turn requires Handoff");
559
- this.#commitTurnGoalWork(current.id, agent, turnContext, response, result.handoff, sourceWake?.id ?? null, deliveredMailIds, recordRevisionAtStart);
563
+ this.#commitTurnGoalWork(current.id, agent, turnContext, responseItem, result.handoff, sourceWake?.id ?? null, deliveredMailIds, recordRevisionAtStart);
560
564
  return;
561
565
  }
562
566
  if (result.handoff)
563
567
  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);
568
+ if (this.#assistantCommitState(current.id, responseItem.id) !== "committed")
569
+ throw new Error("uncommitted Turn requires a committed assistant trace");
570
+ this.ledger.commitTurnResponse(current.id, responseItem.id, this.#now(), "supervisor", deliveredMailIds);
574
571
  return;
575
572
  } catch (error) {
576
573
  if (renewal)
@@ -727,19 +724,32 @@ ${history}` } : {} };
727
724
  const facts = this.ledger.readStream(`turn:${turnId}`).filter((event) => event.type === "turn.retry_started").map((event) => `${event.type}: ${JSON.stringify(event.data)}`);
728
725
  return [...items, ...facts].join("\n");
729
726
  }
730
- #assistantCommitState(turnId, text) {
731
- const canonical = normalizeAssistantText(text);
732
- const event = this.ledger.readStream(`turn:${turnId}`).findLast((candidate) => {
727
+ #assistantMessageEvent(turnId, messageItemId) {
728
+ return this.ledger.readStream(`turn:${turnId}`).findLast((candidate) => {
733
729
  if (candidate.type !== "message.assistant.completed" || !candidate.data || typeof candidate.data !== "object" || Array.isArray(candidate.data))
734
730
  return false;
735
731
  const message = candidate.data.message;
736
- return messageTextContent(message?.content) === canonical;
732
+ return message?.id === messageItemId;
737
733
  });
734
+ }
735
+ #assistantCommitState(turnId, messageItemId) {
736
+ const event = this.#assistantMessageEvent(turnId, messageItemId);
738
737
  if (!event || !event.data || typeof event.data !== "object" || Array.isArray(event.data))
739
738
  return null;
740
739
  const state = event.data.commitState;
741
740
  return state === "committed" || state === "provisional" ? state : null;
742
741
  }
742
+ #finalAssistantItem(turnId, messageItemId) {
743
+ const id = messageItemId.trim();
744
+ const item = id ? this.ledger.turnItems(turnId).find((candidate) => candidate.id === id) : void 0;
745
+ const text = item && item.type === "assistant_message" && item.status === "completed" ? normalizeAssistantText(String(item.data.text ?? "")) : "";
746
+ const event = id ? this.#assistantMessageEvent(turnId, id) : void 0;
747
+ const turn = this.ledger.turn(turnId);
748
+ 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;
749
+ if (!item || !text || !event || retryStart && event.seq <= retryStart.seq)
750
+ throw new Error("completed Turn finalMessageId must reference a readable Assistant Item from the current attempt");
751
+ return item;
752
+ }
743
753
  #failTurn(turnId, reason) {
744
754
  const current = this.ledger.turn(turnId);
745
755
  if (!current || current.status !== "in_progress")
@@ -826,8 +836,12 @@ ${history}` } : {} };
826
836
  if (type === "message.assistant.completed") {
827
837
  const value = data;
828
838
  const text = messageTextContent(value.message?.content);
829
- if (text)
830
- this.#appendTurnItem(turnId, "assistant_message", { text }, actor);
839
+ if (text) {
840
+ const id = typeof value.message?.id === "string" ? value.message.id.trim() : "";
841
+ if (!id)
842
+ throw new Error("Runner assistant completion is missing a message Item id");
843
+ this.#appendTurnItem(turnId, "assistant_message", { text }, actor, id);
844
+ }
831
845
  } else if (type === "plan.updated")
832
846
  this.#appendTurnItem(turnId, "plan", data, actor);
833
847
  else if (type === "message.assistant.delta") {
@@ -1195,7 +1209,7 @@ ${history}` } : {} };
1195
1209
  return agent === "ceo" && role === "ceo";
1196
1210
  return role === "verifier" || role === "audit";
1197
1211
  }
1198
- #commitTurnGoalWork(turnId, agent, turn, response, raw, sourceWakeId, mailIds, revisionAtStart) {
1212
+ #commitTurnGoalWork(turnId, agent, turn, responseItem, raw, sourceWakeId, mailIds, revisionAtStart) {
1199
1213
  const binding = turn.goalCommitment;
1200
1214
  const goal = this.#goal(binding.goalId);
1201
1215
  if (goal.revision !== binding.goalRevision)
@@ -1206,11 +1220,9 @@ ${history}` } : {} };
1206
1220
  assertTurnOutput(raw);
1207
1221
  const validation = this.#handoffValidations.get(raw.validationToken);
1208
1222
  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))
1223
+ const response = normalizeAssistantText(String(responseItem.data.text ?? ""));
1224
+ 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
1225
  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
1226
  const output = this.#committedTurnOutput(raw, binding);
1215
1227
  assertHandoff(output.handoff);
1216
1228
  const now = this.#now();
@@ -1341,7 +1353,8 @@ ${workText}`, activeGoal: turn.activeGoal, goalCommitment: turn.goalCommitment,
1341
1353
  const input = asRecord(params);
1342
1354
  if (method === "goal.handoff.validate") {
1343
1355
  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 });
1356
+ const candidateMessageId = String(input.candidateMessageId ?? "");
1357
+ 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
1358
  return this.#validateHandoffDraft(turnId, handoffAttemptId, agent, execution, context, input);
1346
1359
  }
1347
1360
  const profile = this.#profiles.get(agent) ?? { agent, role: "child" };
@@ -1480,17 +1493,19 @@ ${workText}`, activeGoal: turn.activeGoal, goalCommitment: turn.goalCommitment,
1480
1493
  #validateHandoffDraft(turnId, attemptId, agent, execution, context, input) {
1481
1494
  if (!context.goalCommitment || execution.goalId === null)
1482
1495
  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 ?? "") };
1496
+ const request = { handoff: input.handoff ?? null, candidateMessageId: String(input.candidateMessageId ?? "").trim(), candidateMessage: String(input.candidateMessage ?? "") };
1484
1497
  const issues = [];
1485
1498
  try {
1486
1499
  assertAgentHandoff(request.handoff);
1487
1500
  } catch {
1488
1501
  issues.push({ code: "handoff_invalid", message: "Handoff requires a valid outcome and at least one evidence event." });
1489
1502
  }
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." });
1503
+ const message = normalizeAssistantText(request.candidateMessage);
1504
+ const existingMessage = request.candidateMessageId ? this.ledger.turnItems(turnId).find((item) => item.id === request.candidateMessageId) : void 0;
1505
+ if (!request.candidateMessageId || !message)
1506
+ issues.push({ code: "message_missing", message: "This committed Turn needs a readable assistant message Item before it can finish." });
1507
+ else if (existingMessage && (existingMessage.type !== "assistant_message" || existingMessage.status !== "completed" || normalizeAssistantText(String(existingMessage.data.text ?? "")) !== message))
1508
+ issues.push({ code: "message_mismatch", message: "Handoff message identity does not match its assistant Item." });
1494
1509
  const goal = this.ledger.goal(context.goalCommitment.goalId);
1495
1510
  if (!goal || goal.phase !== "active" || goal.owner !== agent || goal.revision !== context.goalCommitment.goalRevision)
1496
1511
  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 +1519,8 @@ ${workText}`, activeGoal: turn.activeGoal, goalCommitment: turn.goalCommitment,
1504
1519
  if (issues.length)
1505
1520
  return { accepted: false, fatal: false, attemptId, issues };
1506
1521
  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 };
1522
+ 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] } });
1523
+ return { accepted: true, fatal: false, attemptId, token, goalId: goal.id, goalRevision: goal.revision, messageItemId: request.candidateMessageId };
1509
1524
  }
1510
1525
  #beginHandoffAttempt(turnId) {
1511
1526
  this.#invalidateHandoffTokens(turnId);
package/dist/cli.js CHANGED
@@ -25,14 +25,14 @@ import {
25
25
  streamEvents,
26
26
  updateWorkspaceRunnerProfile,
27
27
  writeDefaultConfig
28
- } from "./chunk-KJ4HJCTK.js";
28
+ } from "./chunk-5PZFP5CO.js";
29
29
  import {
30
30
  runnerManifests,
31
31
  runnerPlugin
32
- } from "./chunk-D2DVAH37.js";
33
- import "./chunk-YH6YJ2IB.js";
34
- import "./chunk-QBD6EJ2Z.js";
35
- import "./chunk-7E7V6OIU.js";
32
+ } from "./chunk-R6GKCNTB.js";
33
+ import "./chunk-CDELLMIJ.js";
34
+ import "./chunk-SQJZ5OFH.js";
35
+ import "./chunk-EK6BTCFG.js";
36
36
  import "./chunk-6M3OOCKO.js";
37
37
  import "./chunk-K26BVR6O.js";
38
38
  import "./chunk-IIJQ3DW4.js";
@@ -12670,6 +12670,9 @@ var ConversationView = class {
12670
12670
  this.entries.push({ kind: "component", component });
12671
12671
  }
12672
12672
  addText(content) {
12673
+ const previous = this.entries.at(-1);
12674
+ if (previous?.kind === "text" && previous.content === content)
12675
+ return;
12673
12676
  this.entries.push({ kind: "text", content });
12674
12677
  this.trim();
12675
12678
  }
@@ -12677,15 +12680,17 @@ var ConversationView = class {
12677
12680
  this.entries.push({ kind: "user", content });
12678
12681
  this.trim();
12679
12682
  }
12680
- addMarkdown(content) {
12683
+ addMarkdown(content, messageId) {
12681
12684
  this.liveMarkdown = "";
12682
12685
  content = content.trim();
12683
12686
  if (!content)
12684
12687
  return;
12688
+ if (messageId && this.entries.some((entry) => entry.kind === "markdown" && entry.messageId === messageId))
12689
+ return;
12685
12690
  const previous = this.entries.at(-1);
12686
12691
  if (previous?.kind === "markdown" && previous.content === content)
12687
12692
  return;
12688
- this.entries.push({ kind: "markdown", content });
12693
+ this.entries.push({ kind: "markdown", content, ...messageId ? { messageId } : {} });
12689
12694
  this.trim();
12690
12695
  }
12691
12696
  appendLiveMarkdown(content) {
@@ -12990,14 +12995,14 @@ async function runGoahTui(configPath, stateDir, initialMessage) {
12990
12995
  transcriptView.appendLiveMarkdown(text);
12991
12996
  tui.requestRender();
12992
12997
  };
12993
- const commitLive = (text) => {
12998
+ const commitLive = (text, messageId) => {
12994
12999
  transcriptView.clearLiveMarkdown();
12995
13000
  if (text)
12996
- transcriptView.addMarkdown(text);
13001
+ transcriptView.addMarkdown(text, messageId);
12997
13002
  tui.requestRender();
12998
13003
  };
12999
- const pushResponse = (text) => {
13000
- transcriptView.addMarkdown(text);
13004
+ const pushResponse = (text, messageId) => {
13005
+ transcriptView.addMarkdown(text, messageId);
13001
13006
  tui.requestRender();
13002
13007
  };
13003
13008
  const updateTool = (activity) => {
@@ -13457,15 +13462,16 @@ function renderFrame(frame, push, appendLive = push, commitLive = push, pushResp
13457
13462
  return;
13458
13463
  }
13459
13464
  const text = messageText(message.content);
13465
+ const messageId = typeof message.id === "string" ? message.id : void 0;
13460
13466
  if (data.commitState === "provisional") {
13461
13467
  clearLive();
13462
13468
  return;
13463
13469
  }
13464
13470
  if (text)
13465
- commitLive(text);
13471
+ commitLive(text, messageId);
13466
13472
  } else if (record.type === "response.committed") {
13467
13473
  if (typeof data.text === "string" && data.text.trim())
13468
- commitLive(data.text.trim());
13474
+ commitLive(data.text.trim(), typeof data.messageItemId === "string" ? data.messageItemId : void 0);
13469
13475
  } else if (record.type === "handoff.recorded") {
13470
13476
  if (typeof data.goalId === "string")
13471
13477
  push(`${data.outcome === "blocked" ? tuiTheme.error("goal blocked") : tuiTheme.success("goal saved")} ${tuiTheme.muted(`${String(data.outcome).replaceAll("_", " ")} \xB7 record r${String(data.recordRevision)}`)}`);
@@ -13955,7 +13961,7 @@ async function main() {
13955
13961
  console.log(JSON.stringify({ goal }, null, 2));
13956
13962
  } else if (command === "dashboard") {
13957
13963
  const path4 = option("--output") ?? join7(config.stateDir, "status.html");
13958
- writeFileSync(path4, (await import("./dist-KNR2XL6J.js")).renderDashboard(ledger));
13964
+ writeFileSync(path4, (await import("./dist-QW77PKUJ.js")).renderDashboard(ledger));
13959
13965
  console.log(path4);
13960
13966
  } else
13961
13967
  throw new Error(`unknown command: ${command}`);
@@ -13965,7 +13971,7 @@ async function main() {
13965
13971
  }
13966
13972
  }
13967
13973
  async function run(supervisor, signal) {
13968
- const { runSupervisorDaemon } = await import("./dist-KNR2XL6J.js");
13974
+ const { runSupervisorDaemon } = await import("./dist-QW77PKUJ.js");
13969
13975
  await runSupervisorDaemon(supervisor, { signal });
13970
13976
  }
13971
13977
  async function waitForConsole(stateDir) {
@@ -14200,7 +14206,7 @@ async function runRunnerCommand(command, commandArgs, configPath) {
14200
14206
  async function runRunnerEarly(configPath) {
14201
14207
  const action = args[1] ?? "list";
14202
14208
  if (action === "list") {
14203
- for (const manifest of (await import("./runner-registry-GD4BYCQC.js")).runnerManifests())
14209
+ for (const manifest of (await import("./runner-registry-3WSOMXRW.js")).runnerManifests())
14204
14210
  console.log(`${manifest.id.padEnd(16)} ${manifest.description}`);
14205
14211
  return;
14206
14212
  }
@@ -15,7 +15,7 @@ import {
15
15
  renderDashboard,
16
16
  runSupervisorDaemon,
17
17
  selectRecoveryEvents
18
- } from "./chunk-QBD6EJ2Z.js";
18
+ } from "./chunk-SQJZ5OFH.js";
19
19
  import "./chunk-4KA63VNZ.js";
20
20
  import "./chunk-KAO7WIXG.js";
21
21
  import "./chunk-R6I3VKME.js";
@@ -304,6 +304,7 @@ type HandoffValidationResult = {
304
304
  token: string;
305
305
  goalId: string;
306
306
  goalRevision: number;
307
+ messageItemId: string;
307
308
  } | {
308
309
  accepted: false;
309
310
  fatal: boolean;
@@ -312,6 +313,7 @@ type HandoffValidationResult = {
312
313
  };
313
314
  interface HandoffValidationRequest {
314
315
  handoff: AgentHandoff;
316
+ candidateMessageId: string;
315
317
  candidateMessage: string;
316
318
  }
317
319
  interface TurnOutput {
@@ -418,9 +420,6 @@ interface TurnContext {
418
420
  activeGoal: GoalSnapshot | null;
419
421
  goalCommitment: GoalCommitment | null;
420
422
  }
421
- interface AssistantResponse {
422
- content: string;
423
- }
424
423
  /** Canonical user-visible assistant prose. Raw provider events remain unchanged. */
425
424
  declare function normalizeAssistantText(text: string): string;
426
425
  interface RunRequest {
@@ -436,7 +435,7 @@ interface RunRequest {
436
435
  }
437
436
  type RunnerCandidateResult = {
438
437
  outcome: "completed";
439
- response: AssistantResponse;
438
+ finalMessageId: string;
440
439
  handoff?: TurnOutput;
441
440
  } | {
442
441
  outcome: "abnormal";
@@ -514,7 +513,8 @@ interface Ledger extends EventStore {
514
513
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
515
514
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
516
515
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
517
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
516
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
517
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
518
518
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
519
519
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
520
520
  putMails(mail: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
@@ -554,4 +554,4 @@ declare function assertTurnOutput(value: TurnOutput): void;
554
554
  declare function assertGoalSnapshot(value: GoalSnapshot): void;
555
555
 
556
556
  export { assertAgentHandoff, assertGoalSnapshot, assertGoalTransition, assertHandoff, assertTurnOutput, assertWakeTransition, goalAutomaticTarget, goalCommitment, goalRoute, humanInboxRoute, humanRequestRoute, noGoalCommitment, normalizeAssistantText, specialistAutomaticTarget, specialistInboxRoute };
557
- export type { AgentCapability, AgentHandoff, AgentProfile, AgentRole, AssistantResponse, AutomaticTarget, CommittedTurnOutput, DelegationRequest, DelegationResult, GoalChangeAuthority, GoalChangeMetadata, GoalChangeOperation, GoalChangedData, GoalCommitment, GoalCompletionRequest, GoalHandoff, GoalOutcome, GoalPhase, GoalSnapshot, Handoff, HandoffCommit, HandoffValidationIssue, HandoffValidationRequest, HandoffValidationResult, HumanTurnAdmissionRequest, HumanTurnAdmissionResult, Ledger, MailLevel, MailRoute, MailSnapshot, ReassignmentRequest, ReassignmentResult, RunRequest, Runner, RunnerCandidateResult, RunnerChoice, RunnerCommandResult, RunnerConfigurator, RunnerControlMethod, RunnerDiagnostic, RunnerHandle, RunnerManifest, RunnerProfile, RunnerRpcMethod, RunnerSetupInteraction, RunnerSetupProgress, RunnerSetupTransaction, RunnerTraceEvent, ScheduleSnapshot, ScheduleStatus, SpecialistRole, TeamMemberMotion, TeamMemberView, ThreadSnapshot, TurnContext, TurnItemSnapshot, TurnItemStatus, TurnItemType, TurnOutput, TurnSnapshot, TurnStatus, TurnTrigger, TurnTriggerKind, WakeSnapshot, WakeStatus, WakeTriggerSnapshot, WakeTriggerStatus, WorkRecordDiff, WorkRecordSnapshot, WorkRecordUpdateRequest };
557
+ export type { AgentCapability, AgentHandoff, AgentProfile, AgentRole, AutomaticTarget, CommittedTurnOutput, DelegationRequest, DelegationResult, GoalChangeAuthority, GoalChangeMetadata, GoalChangeOperation, GoalChangedData, GoalCommitment, GoalCompletionRequest, GoalHandoff, GoalOutcome, GoalPhase, GoalSnapshot, Handoff, HandoffCommit, HandoffValidationIssue, HandoffValidationRequest, HandoffValidationResult, HumanTurnAdmissionRequest, HumanTurnAdmissionResult, Ledger, MailLevel, MailRoute, MailSnapshot, ReassignmentRequest, ReassignmentResult, RunRequest, Runner, RunnerCandidateResult, RunnerChoice, RunnerCommandResult, RunnerConfigurator, RunnerControlMethod, RunnerDiagnostic, RunnerHandle, RunnerManifest, RunnerProfile, RunnerRpcMethod, RunnerSetupInteraction, RunnerSetupProgress, RunnerSetupTransaction, RunnerTraceEvent, ScheduleSnapshot, ScheduleStatus, SpecialistRole, TeamMemberMotion, TeamMemberView, ThreadSnapshot, TurnContext, TurnItemSnapshot, TurnItemStatus, TurnItemType, TurnOutput, TurnSnapshot, TurnStatus, TurnTrigger, TurnTriggerKind, WakeSnapshot, WakeStatus, WakeTriggerSnapshot, WakeTriggerStatus, WorkRecordDiff, WorkRecordSnapshot, WorkRecordUpdateRequest };
@@ -2,7 +2,7 @@ import { createRequire as __goahCreateRequire } from "node:module";
2
2
  const require = __goahCreateRequire(import.meta.url);
3
3
  import {
4
4
  runProcessWorker
5
- } from "./chunk-7E7V6OIU.js";
5
+ } from "./chunk-EK6BTCFG.js";
6
6
  import "./chunk-6M3OOCKO.js";
7
7
  import "./chunk-K26BVR6O.js";
8
8
  import "./chunk-IIJQ3DW4.js";
@@ -32,6 +32,7 @@ await runProcessWorker(async (request, emit, rpc) => {
32
32
  const steps = triggerSteps ?? byAgent[request.agent] ?? JSON.parse(process.env.GOAH_FAUX_STEPS ?? "[]");
33
33
  let goalCommitment = request.turn?.goalCommitment;
34
34
  let recordUpdated = false;
35
+ let messageSequence = 0;
35
36
  for (const step of steps) {
36
37
  if (step.write) {
37
38
  const path = join(process.cwd(), step.write.path);
@@ -54,8 +55,11 @@ await runProcessWorker(async (request, emit, rpc) => {
54
55
  await new Promise((resolve) => setTimeout(resolve, step.delayMs));
55
56
  if (step.hang)
56
57
  await new Promise(() => void 0);
57
- if (step.response !== void 0)
58
- return { outcome: "completed", response: { content: step.response } };
58
+ if (step.response !== void 0) {
59
+ const finalMessageId = `faux:${request.execution.id}:attempt:${request.execution.attempt}:message:${++messageSequence}`;
60
+ emit({ type: "message.assistant.completed", data: { message: { id: finalMessageId, role: "assistant", content: [{ type: "text", text: step.response }] }, commitState: "committed" } });
61
+ return { outcome: "completed", finalMessageId };
62
+ }
59
63
  if (step.handoff) {
60
64
  const draft = step.handoff;
61
65
  if (goalCommitment && !recordUpdated) {
@@ -64,16 +68,19 @@ await runProcessWorker(async (request, emit, rpc) => {
64
68
  const evidence = sourceSeqs(request.context);
65
69
  await rpc("work_record.update", { expectedRevision: Number(record.recordRevision ?? 0), content: handoffRecord(draft.handoff, request.execution.id), reason: "record faux Goal progress", evidence: evidence.length ? [Math.max(...evidence)] : [] });
66
70
  }
67
- const validation = await rpc("goal.handoff.validate", { handoff: draft.handoff, candidateMessage: draft.response.content });
68
- emit({ type: "message.assistant.completed", data: { message: { id: `faux:${request.execution.id}:handoff:${validation.attemptId}`, role: "assistant", content: [{ type: "text", text: draft.response.content }], stopReason: "toolUse" }, commitState: "provisional" } });
71
+ const messageItemId = `faux:${request.execution.id}:attempt:${request.execution.attempt}:message:${++messageSequence}`;
72
+ const validation = await rpc("goal.handoff.validate", { handoff: draft.handoff, candidateMessageId: messageItemId, candidateMessage: draft.response.content });
73
+ emit({ type: "message.assistant.completed", data: { message: { id: messageItemId, role: "assistant", content: [{ type: "text", text: draft.response.content }], stopReason: "toolUse" }, commitState: "provisional" } });
69
74
  if (!validation.accepted) {
70
75
  emit({ type: "runner.handoff_rejected", data: { attemptId: validation.attemptId, issues: validation.issues } });
71
76
  if (validation.fatal)
72
77
  return { outcome: "abnormal", reason: validation.issues.map((issue) => issue.message).join("; ") };
73
78
  continue;
74
79
  }
80
+ if (validation.messageItemId !== messageItemId)
81
+ return { outcome: "abnormal", reason: "Handoff validation returned a different assistant message identity" };
75
82
  const output = { validationAttemptId: validation.attemptId, validationToken: validation.token, handoff: draft.handoff };
76
- return { outcome: "completed", response: draft.response, handoff: output };
83
+ return { outcome: "completed", finalMessageId: messageItemId, handoff: output };
77
84
  }
78
85
  }
79
86
  return { outcome: "abnormal", reason: "faux worker stopped without handoff" };
package/dist/index.d.ts CHANGED
@@ -337,9 +337,6 @@ interface TurnContext {
337
337
  activeGoal: GoalSnapshot | null;
338
338
  goalCommitment: GoalCommitment | null;
339
339
  }
340
- interface AssistantResponse {
341
- content: string;
342
- }
343
340
  interface RunRequest {
344
341
  agent: string;
345
342
  execution: TurnSnapshot;
@@ -353,7 +350,7 @@ interface RunRequest {
353
350
  }
354
351
  type RunnerCandidateResult = {
355
352
  outcome: "completed";
356
- response: AssistantResponse;
353
+ finalMessageId: string;
357
354
  handoff?: TurnOutput;
358
355
  } | {
359
356
  outcome: "abnormal";
@@ -431,7 +428,8 @@ interface Ledger extends EventStore {
431
428
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
432
429
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
433
430
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
434
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
431
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
432
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
435
433
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
436
434
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
437
435
  putMails(mail: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
@@ -518,7 +516,8 @@ declare class SqliteLedger implements Ledger {
518
516
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
519
517
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
520
518
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
521
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
519
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
520
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
522
521
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
523
522
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
524
523
  putMails(mails: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
package/dist/index.js CHANGED
@@ -31,11 +31,11 @@ import {
31
31
  updateWorkspaceRunnerProfile,
32
32
  writeDefaultConfig,
33
33
  writeDefaultRunnerProfile
34
- } from "./chunk-KJ4HJCTK.js";
35
- import "./chunk-D2DVAH37.js";
36
- import "./chunk-YH6YJ2IB.js";
37
- import "./chunk-QBD6EJ2Z.js";
38
- import "./chunk-7E7V6OIU.js";
34
+ } from "./chunk-5PZFP5CO.js";
35
+ import "./chunk-R6GKCNTB.js";
36
+ import "./chunk-CDELLMIJ.js";
37
+ import "./chunk-SQJZ5OFH.js";
38
+ import "./chunk-EK6BTCFG.js";
39
39
  import "./chunk-6M3OOCKO.js";
40
40
  import "./chunk-K26BVR6O.js";
41
41
  import "./chunk-IIJQ3DW4.js";
package/dist/pi-worker.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-XALZLRYK.js";
6
6
  import {
7
7
  runProcessWorker
8
- } from "./chunk-7E7V6OIU.js";
8
+ } from "./chunk-EK6BTCFG.js";
9
9
  import {
10
10
  createPiModel,
11
11
  fauxAssistantMessage,
@@ -35,6 +35,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
35
35
  import { delimiter, dirname, join, resolve, sep } from "node:path";
36
36
  import { homedir, tmpdir } from "node:os";
37
37
  import { fileURLToPath } from "node:url";
38
+ var GOAL_TURN_POLICY = `A Goal commitment starts a sustained work session, not a short status round. 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, 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.
39
+
40
+ Call handoff exactly once, only at a genuine control boundary: the current actionable frontier is exhausted after meaningful work, an explicit external wait is required, a real blocker prevents safe progress, or completion evidence is ready for review. Use outcome progress only for the first case, waiting only for the second, blocked only for the third, and completion_proposed only for the fourth. Write a concise human-readable assistant message describing what happened, and keep Handoff machine-readable with only outcome and evidence. Treat the supplied context as authoritative.`;
38
41
  function compactMessages(messages, maxRecent = 8) {
39
42
  if (messages.length <= maxRecent + 1)
40
43
  return messages;
@@ -100,8 +103,10 @@ Follow the current Goal and explicit tools.
100
103
  `;
101
104
  const finalResponse = fauxAssistantMessage([fauxText(String(handoff.message ?? `Goal work recorded with outcome ${outcome}.`)), fauxToolCall("handoff", { outcome, evidence: handoffEvidence })], { stopReason: "toolUse" });
102
105
  const recordResponse = fauxAssistantMessage(fauxToolCall("work_record_update", { expectedRevision: Number(current.recordRevision ?? 0), content: record, reason: "record faux Goal progress", evidence: evidence.length ? [Math.max(...evidence)] : [] }), { stopReason: "toolUse" });
106
+ const narratedScheduleResponse = fauxAssistantMessage([fauxText(String(handoff.message ?? "Work Record updated; the next wake is scheduled.")), fauxToolCall("schedule_wake", { at: new Date(Date.parse(request.execution.startedAt) + 6e4).toISOString(), reason: "continue Goal work" })], { stopReason: "toolUse" });
107
+ const toolOnlyHandoff = fauxAssistantMessage(fauxToolCall("handoff", { outcome, evidence: handoffEvidence }), { stopReason: "toolUse" });
103
108
  const fatalBatch = fauxAssistantMessage([fauxText("Stale completion attempt."), fauxToolCall("handoff", { outcome, evidence: handoffEvidence }), fauxToolCall("write", { path: "fatal.txt", content: "must not be written" })], { stopReason: "toolUse" });
104
- faux.setResponses(process.env.GOAH_PI_FAUX_FATAL_BATCH === "1" ? [fatalBatch] : process.env.GOAH_PI_FAUX_HANDOFF_REPAIR === "1" ? [fauxAssistantMessage([fauxText("Premature completion attempt."), fauxToolCall("handoff", { outcome, evidence: handoffEvidence })], { stopReason: "toolUse" }), recordResponse, finalResponse] : [recordResponse, finalResponse]);
109
+ faux.setResponses(process.env.GOAH_PI_FAUX_FATAL_BATCH === "1" ? [fatalBatch] : process.env.GOAH_PI_FAUX_HANDOFF_REPAIR === "1" ? [fauxAssistantMessage([fauxText("Premature completion attempt."), fauxToolCall("handoff", { outcome, evidence: handoffEvidence })], { stopReason: "toolUse" }), recordResponse, finalResponse] : process.env.GOAH_PI_FAUX_TOOL_ONLY_HANDOFF === "1" ? [recordResponse, narratedScheduleResponse, toolOnlyHandoff] : [recordResponse, finalResponse]);
105
110
  } else if (!goalState.bound) {
106
111
  faux.setResponses([fauxAssistantMessage([fauxText(process.env.GOAH_PI_FAUX_RESPONSE ?? "Hello from Goah.")])]);
107
112
  } else {
@@ -113,7 +118,7 @@ Follow the current Goal and explicit tools.
113
118
  let output = null;
114
119
  let acceptedHandoff = null;
115
120
  let revokedReason = "";
116
- let response = "";
121
+ let lastReadableMessage = null;
117
122
  let responseFailure = "";
118
123
  let compactions = 0;
119
124
  let messageCounter = 0;
@@ -125,7 +130,7 @@ Follow the current Goal and explicit tools.
125
130
  const existing = messageIds.get(message);
126
131
  if (existing)
127
132
  return existing;
128
- const id = `message:${++messageCounter}`;
133
+ const id = `${request.execution.id}:attempt:${request.execution.attempt}:message:${++messageCounter}`;
129
134
  messageIds.set(message, id);
130
135
  return id;
131
136
  };
@@ -145,8 +150,11 @@ Follow the current Goal and explicit tools.
145
150
  const sourceSeqs = Array.isArray(contextRecord.sourceSeqs) ? contextRecord.sourceSeqs.filter((value) => Number.isInteger(value)) : [];
146
151
  const basePrompt = process.env.GOAH_PI_SYSTEM_PROMPT ?? suppliedPrompt ?? (request.turn?.trigger.kind === "user_message" ? "You are Goah's primary Agent. Respond naturally to the Human." : `You are Goah Agent ${request.agent}. Inspect the supplied context and respond appropriately.`);
147
152
  const systemPrompt = goalState.bound ? `${basePrompt}
148
- This Turn has a Goal commitment. You must update its Work Record, write a concise human-readable assistant message describing what happened, and call handoff exactly once with only machine-readable outcome and evidence. The message and Handoff are separate outputs. Treat the supplied context as authoritative.` : request.turn?.trigger.kind === "user_message" ? `${basePrompt}
149
- The active Goal, when supplied, is context rather than an assignment. Finish with an ordinary response unless a Goal tool establishes a commitment during this Turn. After commitment, update that Goal's Work Record, write a concise human-readable assistant message, and call handoff exactly once with separate machine-readable outcome and evidence. Treat the supplied context as authoritative.` : `${basePrompt}
153
+
154
+ ${GOAL_TURN_POLICY}` : request.turn?.trigger.kind === "user_message" ? `${basePrompt}
155
+ The active Goal, when supplied, is context rather than an assignment. Finish with an ordinary response unless a Goal tool establishes a commitment during this Turn. If a Goal commitment is established, follow this policy for the rest of the Turn:
156
+
157
+ ${GOAL_TURN_POLICY}` : `${basePrompt}
150
158
  This Turn has no Goal commitment. Finish with an ordinary response and do not call handoff. Treat the supplied context as authoritative.`;
151
159
  const agent = new Agent({
152
160
  initialState: {
@@ -196,10 +204,16 @@ This Turn has no Goal commitment. Finish with an ordinary response and do not ca
196
204
  acceptedHandoff = null;
197
205
  output = null;
198
206
  const handoff = args;
207
+ const currentText = assistantResponseText(assistantMessage);
208
+ const candidate = currentText ? { id: idFor(assistantMessage), text: currentText } : lastReadableMessage;
199
209
  try {
200
- const result = await rpc("goal.handoff.validate", { handoff, candidateMessage: assistantResponseText(assistantMessage) });
210
+ const result = await rpc("goal.handoff.validate", { handoff, candidateMessageId: candidate?.id ?? "", candidateMessage: candidate?.text ?? "" });
201
211
  if (result.accepted) {
202
- acceptedHandoff = { attemptId: result.attemptId, token: result.token, handoff };
212
+ if (!candidate || result.messageItemId !== candidate.id) {
213
+ revokedReason = "Handoff validation returned a different assistant message identity";
214
+ return { block: true, reason: revokedReason, terminate: true };
215
+ }
216
+ acceptedHandoff = { attemptId: result.attemptId, token: result.token, messageItemId: result.messageItemId, handoff };
203
217
  return;
204
218
  }
205
219
  const reason = result.issues.map((issue) => `- ${issue.message}`).join("\n");
@@ -234,11 +248,14 @@ ${reason}`, terminate: result.fatal };
234
248
  } else if (event.type === "message_update") {
235
249
  emit({ type: "message.assistant.delta", data: { messageId: idFor(event.message), delta: JSON.parse(JSON.stringify(event.assistantMessageEvent)) } });
236
250
  } else if (event.type === "message_end" && event.message.role === "assistant") {
237
- response = assistantResponseText(event.message);
251
+ const messageId = idFor(event.message);
252
+ const text = assistantResponseText(event.message);
253
+ if (text)
254
+ lastReadableMessage = { id: messageId, text };
238
255
  if ((event.message.stopReason === "error" || event.message.stopReason === "aborted") && event.message.errorMessage)
239
256
  responseFailure = event.message.errorMessage;
240
257
  const handoffIntent = hasAgentToolCall(event.message, "handoff");
241
- emit({ type: "message.assistant.completed", data: { message: transcriptMessage(event.message, idFor(event.message)), commitState: handoffIntent ? "provisional" : "committed" } });
258
+ emit({ type: "message.assistant.completed", data: { message: transcriptMessage(event.message, messageId), commitState: handoffIntent ? "provisional" : "committed" } });
242
259
  } else if (event.type === "tool_execution_start")
243
260
  emit({ type: "tool.called", data: { callId: event.toolCallId, name: event.toolName, arguments: JSON.parse(JSON.stringify(event.args)) } });
244
261
  else if (event.type === "tool_execution_end")
@@ -262,13 +279,17 @@ Runner root: ${root}. Manage local files directly when the goal requires them.`)
262
279
  }
263
280
  if (revokedReason)
264
281
  return { outcome: "abnormal", reason: revokedReason };
282
+ const finalMessage = lastReadableMessage;
265
283
  if (!goalState.bound)
266
- return response ? { outcome: "completed", response: { content: response } } : { outcome: "abnormal", reason: responseFailure || "Pi worker exited without a response" };
284
+ return finalMessage ? { outcome: "completed", finalMessageId: finalMessage.id } : { outcome: "abnormal", reason: responseFailure || "Pi worker exited without a response" };
267
285
  if (responseFailure)
268
286
  return { outcome: "abnormal", reason: responseFailure };
269
287
  if (!output)
270
288
  return { outcome: "abnormal", reason: "Pi worker exited without an accepted Handoff" };
271
- return { outcome: "completed", response: { content: response }, handoff: output };
289
+ const finalHandoff = acceptedHandoff;
290
+ if (!finalHandoff)
291
+ return { outcome: "abnormal", reason: "Pi worker exited without an accepted Handoff message" };
292
+ return { outcome: "completed", finalMessageId: finalHandoff.messageItemId, handoff: output };
272
293
  });
273
294
  }
274
295
  function snapshotModelConfig(options) {
@@ -294,7 +315,7 @@ function createTools(root, handoff, rpc, capabilities, goalState, protectedPaths
294
315
  const handoffTool = {
295
316
  name: "handoff",
296
317
  label: "Handoff",
297
- description: "Record a structured handoff and end a Turn with a Goal commitment.",
318
+ description: "End the current Goal Turn at a genuine control boundary. Do not call while another safe and useful action can be completed now. Planning, partial progress, a Work Record update, or a scheduled Wake does not by itself justify Handoff.",
298
319
  parameters: typebox_exports.Object({
299
320
  outcome: typebox_exports.Union([typebox_exports.Literal("progress"), typebox_exports.Literal("waiting"), typebox_exports.Literal("blocked"), typebox_exports.Literal("completion_proposed")]),
300
321
  evidence: typebox_exports.Array(typebox_exports.Number())
@@ -488,7 +509,7 @@ function createRpcTools(rpc, allowed, onResult) {
488
509
  ["ledger.search", tool("ledger_search", "Search durable ledger facts.", "ledger.search", typebox_exports.Object({ query: typebox_exports.String(), limit: typebox_exports.Optional(typebox_exports.Number()) }))],
489
510
  ["memory.append", tool("memory_append", "Append a durable working-memory note that is injected into your future wakes. Record procedural knowledge, active hypotheses, and abandoned approaches with the reason; keep notes concise.", "memory.append", typebox_exports.Object({ note: typebox_exports.String() }))],
490
511
  ["mail.send", tool("send_mail", "Send durable Mail to another Goal owner. goalId is required and routes the Mail into that owner's next committed Turn. Human communication must use request_human.", "mail.send", typebox_exports.Object({ to: typebox_exports.String(), goalId: typebox_exports.String(), level: typebox_exports.Union([typebox_exports.Literal("fyi"), typebox_exports.Literal("decision"), typebox_exports.Literal("emergency")]), body: typebox_exports.Any() }))],
491
- ["schedule.set", tool("schedule_wake", "Schedule this agent's next wake.", "schedule.set", typebox_exports.Object({ at: typebox_exports.String(), reason: typebox_exports.String() }))],
512
+ ["schedule.set", tool("schedule_wake", "Schedule genuinely time-dependent future observation. Scheduling does not end the current Turn; continue all useful work that can be completed before waiting.", "schedule.set", typebox_exports.Object({ at: typebox_exports.String(), reason: typebox_exports.String() }))],
492
513
  ["team.list", tool("team_list", "Read the ledger-derived team roster and liveness state.", "team.list", typebox_exports.Object({}))],
493
514
  ["goal.get", tool("get_goal", "Read the active Goal visible to this Turn. Visibility does not establish a Goal commitment.", "goal.get", typebox_exports.Object({}))],
494
515
  ["goal.create", tool("create_goal", "Create a Root Goal from durable Human intent and bind this Turn. Do not use for greetings, questions, or routine single-turn work.", "goal.create", typebox_exports.Object({ objective: typebox_exports.String(), id: typebox_exports.Optional(typebox_exports.String()) }))],
@@ -87,6 +87,7 @@ type HandoffValidationResult = {
87
87
  token: string;
88
88
  goalId: string;
89
89
  goalRevision: number;
90
+ messageItemId: string;
90
91
  } | {
91
92
  accepted: false;
92
93
  fatal: boolean;
@@ -181,9 +182,6 @@ interface TurnContext {
181
182
  activeGoal: GoalSnapshot | null;
182
183
  goalCommitment: GoalCommitment | null;
183
184
  }
184
- interface AssistantResponse {
185
- content: string;
186
- }
187
185
  interface RunRequest {
188
186
  agent: string;
189
187
  execution: TurnSnapshot;
@@ -197,7 +195,7 @@ interface RunRequest {
197
195
  }
198
196
  type RunnerCandidateResult = {
199
197
  outcome: "completed";
200
- response: AssistantResponse;
198
+ finalMessageId: string;
201
199
  handoff?: TurnOutput;
202
200
  } | {
203
201
  outcome: "abnormal";
@@ -267,14 +265,17 @@ interface EnvSpecSource {
267
265
  */
268
266
  declare function resolveEnvSpec(spec: Record<string, string> | undefined, source: EnvSpecSource | undefined): Record<string, string>;
269
267
 
268
+ interface PiAssistantResponse {
269
+ content: string;
270
+ }
270
271
  interface PiStep {
271
272
  trace?: Array<{
272
273
  type: string;
273
274
  data: JsonValue;
274
275
  }>;
275
- response?: AssistantResponse;
276
+ response?: PiAssistantResponse;
276
277
  handoff?: {
277
- response: AssistantResponse;
278
+ response: PiAssistantResponse;
278
279
  handoff: AgentHandoff;
279
280
  };
280
281
  stopped?: boolean;
@@ -339,4 +340,4 @@ type WorkerRun = (request: WorkerRequest, emit: (event: {
339
340
  declare function runProcessWorker(run: WorkerRun): Promise<void>;
340
341
 
341
342
  export { LOCAL_PROVIDERS, PiRunnerAdapter, ProcessRunner, createPiProcessRunner, defaultAuthFile, modelCatalog, piConfig, piEnvironment, piRunnerConfigurator, piWorkerPath, providerCatalog, resolveEnvSpec, runProcessWorker, verificationWorkerPath };
342
- export type { ModelSummary, PiDriver, PiRunnerConfig, PiRunnerSession, PiStep, ProcessRunnerOptions, ProviderSummary, WorkerControls, WorkerRpc, WorkerRun };
343
+ export type { ModelSummary, PiAssistantResponse, PiDriver, PiRunnerConfig, PiRunnerSession, PiStep, ProcessRunnerOptions, ProviderSummary, WorkerControls, WorkerRpc, WorkerRun };
package/dist/runner-pi.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  resolveEnvSpec,
12
12
  runProcessWorker,
13
13
  verificationWorkerPath
14
- } from "./chunk-7E7V6OIU.js";
14
+ } from "./chunk-EK6BTCFG.js";
15
15
  import {
16
16
  LOCAL_PROVIDERS,
17
17
  defaultAuthFile,
@@ -3,8 +3,8 @@ const require = __goahCreateRequire(import.meta.url);
3
3
  import {
4
4
  runnerManifests,
5
5
  runnerPlugin
6
- } from "./chunk-D2DVAH37.js";
7
- import "./chunk-7E7V6OIU.js";
6
+ } from "./chunk-R6GKCNTB.js";
7
+ import "./chunk-EK6BTCFG.js";
8
8
  import "./chunk-6M3OOCKO.js";
9
9
  import "./chunk-K26BVR6O.js";
10
10
  import "./chunk-IIJQ3DW4.js";
package/dist/sqlite.d.ts CHANGED
@@ -320,7 +320,8 @@ interface Ledger extends EventStore {
320
320
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
321
321
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
322
322
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
323
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
323
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
324
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
324
325
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
325
326
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
326
327
  putMails(mail: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
@@ -408,7 +409,8 @@ declare class SqliteLedger implements Ledger {
408
409
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
409
410
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
410
411
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
411
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
412
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
413
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
412
414
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
413
415
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
414
416
  putMails(mails: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
package/dist/sqlite.js CHANGED
@@ -3,7 +3,7 @@ const require = __goahCreateRequire(import.meta.url);
3
3
  import {
4
4
  SQLITE_SCHEMA_VERSION,
5
5
  SqliteLedger
6
- } from "./chunk-YH6YJ2IB.js";
6
+ } from "./chunk-CDELLMIJ.js";
7
7
  import "./chunk-4KA63VNZ.js";
8
8
  import "./chunk-KAO7WIXG.js";
9
9
  import "./chunk-R6I3VKME.js";
@@ -304,9 +304,6 @@ interface TurnContext {
304
304
  activeGoal: GoalSnapshot | null;
305
305
  goalCommitment: GoalCommitment | null;
306
306
  }
307
- interface AssistantResponse {
308
- content: string;
309
- }
310
307
  interface RunRequest {
311
308
  agent: string;
312
309
  execution: TurnSnapshot;
@@ -320,7 +317,7 @@ interface RunRequest {
320
317
  }
321
318
  type RunnerCandidateResult = {
322
319
  outcome: "completed";
323
- response: AssistantResponse;
320
+ finalMessageId: string;
324
321
  handoff?: TurnOutput;
325
322
  } | {
326
323
  outcome: "abnormal";
@@ -398,7 +395,8 @@ interface Ledger extends EventStore {
398
395
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
399
396
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
400
397
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
401
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
398
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
399
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
402
400
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
403
401
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
404
402
  putMails(mail: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
@@ -15,7 +15,7 @@ import {
15
15
  renderDashboard,
16
16
  runSupervisorDaemon,
17
17
  selectRecoveryEvents
18
- } from "./chunk-QBD6EJ2Z.js";
18
+ } from "./chunk-SQJZ5OFH.js";
19
19
  import "./chunk-4KA63VNZ.js";
20
20
  import "./chunk-KAO7WIXG.js";
21
21
  import "./chunk-R6I3VKME.js";
package/dist/testkit.d.ts CHANGED
@@ -269,6 +269,7 @@ type HandoffValidationResult = {
269
269
  token: string;
270
270
  goalId: string;
271
271
  goalRevision: number;
272
+ messageItemId: string;
272
273
  } | {
273
274
  accepted: false;
274
275
  fatal: boolean;
@@ -296,9 +297,6 @@ interface TurnContext {
296
297
  activeGoal: GoalSnapshot | null;
297
298
  goalCommitment: GoalCommitment | null;
298
299
  }
299
- interface AssistantResponse {
300
- content: string;
301
- }
302
300
  interface RunRequest {
303
301
  agent: string;
304
302
  execution: TurnSnapshot;
@@ -370,7 +368,8 @@ interface Ledger extends EventStore {
370
368
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
371
369
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
372
370
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
373
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
371
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
372
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
374
373
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
375
374
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
376
375
  putMails(mail: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
@@ -457,7 +456,8 @@ declare class SqliteLedger implements Ledger {
457
456
  renewTurnLease(id: string, leaseToken: string, leaseUntil: string, now: string): TurnSnapshot;
458
457
  appendTurnEvent(input: EventInput, leaseToken: string): EventRecord;
459
458
  repairTurnAttempt(id: string, reason: string, now: string, actor: string): TurnItemSnapshot[];
460
- finishTurn(id: string, status: "completed" | "failed" | "interrupted", error: JsonValue | null, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
459
+ finishTurn(id: string, status: "failed" | "interrupted", error: JsonValue, now: string, actor: string): TurnSnapshot;
460
+ commitTurnResponse(id: string, responseItemId: string, now: string, actor: string, mailIds?: string[]): TurnSnapshot;
461
461
  releaseTurnProcess(id: string, actor: string): TurnSnapshot;
462
462
  putMail(mail: MailSnapshot, actor: string, wakeId?: string): EventRecord;
463
463
  putMails(mails: MailSnapshot[], actor: string, wakeId?: string): EventRecord[];
@@ -491,14 +491,17 @@ declare class SqliteLedger implements Ledger {
491
491
  rebuildProjections(): void;
492
492
  }
493
493
 
494
+ interface PiAssistantResponse {
495
+ content: string;
496
+ }
494
497
  interface PiStep {
495
498
  trace?: Array<{
496
499
  type: string;
497
500
  data: JsonValue;
498
501
  }>;
499
- response?: AssistantResponse;
502
+ response?: PiAssistantResponse;
500
503
  handoff?: {
501
- response: AssistantResponse;
504
+ response: PiAssistantResponse;
502
505
  handoff: AgentHandoff;
503
506
  };
504
507
  stopped?: boolean;
package/dist/testkit.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire as __goahCreateRequire } from "node:module";
2
2
  const require = __goahCreateRequire(import.meta.url);
3
3
  import {
4
4
  SqliteLedger
5
- } from "./chunk-YH6YJ2IB.js";
5
+ } from "./chunk-CDELLMIJ.js";
6
6
  import "./chunk-4KA63VNZ.js";
7
7
  import "./chunk-KAO7WIXG.js";
8
8
  import "./chunk-R6I3VKME.js";
@@ -107,6 +107,10 @@ function assertLedgerConformance(create) {
107
107
  throw new Error("ledger conformance: consumed Wake did not link its Turn");
108
108
  if (ledger.wakeTriggers("z").some((trigger) => trigger.status !== "resolved"))
109
109
  throw new Error("ledger conformance: consumed Wake retained pending triggers");
110
+ ledger.putTurnItem({ id: "turn:a:answer", turnId: turn.id, ordinal: 1, type: "assistant_message", status: "completed", data: { text: "verified" }, createdAt: clock.now().toISOString(), completedAt: clock.now().toISOString() }, "a");
111
+ ledger.commitTurnResponse(turn.id, "turn:a:answer", clock.now().toISOString(), "supervisor");
112
+ if (ledger.turn(turn.id)?.status !== "completed" || !ledger.readStream(`turn:${turn.id}`).some((item) => item.type === "response.committed" && item.data.messageItemId === "turn:a:answer"))
113
+ throw new Error("ledger conformance: canonical Assistant response was not committed");
110
114
  if (first.event.ts !== clock.now().toISOString())
111
115
  throw new Error("ledger conformance: injected clock was ignored");
112
116
  const informational = ledger.appendEvent({ streamId: "conformance:events", ts: clock.now().toISOString(), actor: "a", type: "transcript.conformance_info", data: {}, ignorable: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goah/cli",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",