@osolmaz/pi-workflows 0.16.5 → 0.16.6

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.
Files changed (171) hide show
  1. package/README.md +5 -5
  2. package/dist/builtins/autoimplement.workflow.d.ts +5 -1
  3. package/dist/builtins/autoimplement.workflow.js +29 -10
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/client/client.js +3 -1
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/client/protocol.d.ts +1 -1
  8. package/dist/client/protocol.js +18 -1
  9. package/dist/client/protocol.js.map +1 -1
  10. package/dist/client/view.d.ts +7 -5
  11. package/dist/extension/index.d.ts +2 -1
  12. package/dist/extension/index.js +115 -165
  13. package/dist/extension/index.js.map +1 -1
  14. package/dist/extension/recorder.d.ts +1 -1
  15. package/dist/extension/recorder.js +2 -2
  16. package/dist/extension/recorder.js.map +1 -1
  17. package/dist/extension/step-message.d.ts +1 -1
  18. package/dist/extension/step-message.js +4 -4
  19. package/dist/extension/step-message.js.map +1 -1
  20. package/dist/extension/workflow-message-coordinator.d.ts +12 -1
  21. package/dist/extension/workflow-message-coordinator.js +65 -54
  22. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  23. package/dist/resource-managers/index.d.ts +1 -2
  24. package/dist/resource-managers/index.js +1 -2
  25. package/dist/resource-managers/index.js.map +1 -1
  26. package/dist/resource-managers/sqlite.d.ts +2 -295
  27. package/dist/resource-managers/sqlite.js +71 -1528
  28. package/dist/resource-managers/sqlite.js.map +1 -1
  29. package/dist/resource-managers/types.d.ts +2 -7
  30. package/dist/server/resource-runner-entry.js +2 -6
  31. package/dist/server/resource-runner-entry.js.map +1 -1
  32. package/dist/server/rpc-bridge.js +1 -1
  33. package/dist/server/rpc-bridge.js.map +1 -1
  34. package/dist/server/rpc-executor.js +2 -4
  35. package/dist/server/rpc-executor.js.map +1 -1
  36. package/dist/server/server-entry.js +3 -0
  37. package/dist/server/server-entry.js.map +1 -1
  38. package/dist/server/server.d.ts +9 -8
  39. package/dist/server/server.js +364 -541
  40. package/dist/server/server.js.map +1 -1
  41. package/dist/server/state.d.ts +16 -29
  42. package/dist/server/state.js +105 -319
  43. package/dist/server/state.js.map +1 -1
  44. package/dist/server/view.d.ts +4 -5
  45. package/dist/server/view.js +51 -51
  46. package/dist/server/view.js.map +1 -1
  47. package/dist/server/workflow-runner-entry.d.ts +1 -1
  48. package/dist/server/workflow-runner-entry.js +1 -49
  49. package/dist/server/workflow-runner-entry.js.map +1 -1
  50. package/dist/server/workflow-runner-protocol.d.ts +2 -7
  51. package/dist/server/workflow-runner-protocol.js +5 -10
  52. package/dist/server/workflow-runner-protocol.js.map +1 -1
  53. package/dist/server/workflow-runner-store.d.ts +5 -6
  54. package/dist/server/workflow-runner-store.js +7 -14
  55. package/dist/server/workflow-runner-store.js.map +1 -1
  56. package/dist/state/attempt-time.d.ts +21 -0
  57. package/dist/state/attempt-time.js +95 -0
  58. package/dist/state/attempt-time.js.map +1 -0
  59. package/dist/state/json.d.ts +2 -1
  60. package/dist/state/json.js.map +1 -1
  61. package/dist/state/project-store.d.ts +39 -0
  62. package/dist/state/project-store.js +154 -0
  63. package/dist/state/project-store.js.map +1 -0
  64. package/dist/state/prune.js +0 -7
  65. package/dist/state/prune.js.map +1 -1
  66. package/dist/state/schema.js +23 -23
  67. package/dist/state/workflow-messages.d.ts +1 -1
  68. package/dist/state/workflow-messages.js +3 -1
  69. package/dist/state/workflow-messages.js.map +1 -1
  70. package/dist/viewer/backup.js +5 -0
  71. package/dist/viewer/backup.js.map +1 -1
  72. package/dist/workflows/diagnostics.d.ts +8 -0
  73. package/dist/workflows/diagnostics.js +61 -0
  74. package/dist/workflows/diagnostics.js.map +1 -0
  75. package/dist/workflows/engine.d.ts +2 -15
  76. package/dist/workflows/engine.js +125 -245
  77. package/dist/workflows/engine.js.map +1 -1
  78. package/dist/workflows/human-decision.d.ts +3 -7
  79. package/dist/workflows/human-decision.js +2 -56
  80. package/dist/workflows/human-decision.js.map +1 -1
  81. package/dist/workflows/index.d.ts +2 -1
  82. package/dist/workflows/index.js +1 -0
  83. package/dist/workflows/index.js.map +1 -1
  84. package/dist/workflows/queue.d.ts +267 -0
  85. package/dist/workflows/queue.js +1248 -0
  86. package/dist/workflows/queue.js.map +1 -0
  87. package/dist/workflows/requests.d.ts +47 -0
  88. package/dist/workflows/requests.js +158 -0
  89. package/dist/workflows/requests.js.map +1 -0
  90. package/dist/workflows/schema.js +2 -4
  91. package/dist/workflows/schema.js.map +1 -1
  92. package/dist/workflows/store.d.ts +11 -16
  93. package/dist/workflows/store.js +174 -169
  94. package/dist/workflows/store.js.map +1 -1
  95. package/dist/workflows/tool-input.d.ts +6 -9
  96. package/dist/workflows/tool-input.js +15 -12
  97. package/dist/workflows/tool-input.js.map +1 -1
  98. package/dist/workflows/transitions.d.ts +38 -0
  99. package/dist/workflows/transitions.js +276 -0
  100. package/dist/workflows/transitions.js.map +1 -0
  101. package/dist/workflows/types.d.ts +9 -42
  102. package/dist/workflows/workflow-message-content.d.ts +7 -1
  103. package/dist/workflows/workflow-message-content.js +22 -4
  104. package/dist/workflows/workflow-message-content.js.map +1 -1
  105. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  106. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  107. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  108. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  109. package/docs/DEFERRED_TURNS.md +84 -105
  110. package/docs/HUMAN_DECISIONS.md +17 -19
  111. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  112. package/docs/SQLITE_STATE.md +68 -29
  113. package/docs/WORKFLOW_SERVER.md +21 -10
  114. package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
  115. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  116. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  117. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  118. package/docs/workflows.md +94 -45
  119. package/examples/workflows/autoresearch.workflow.ts +0 -2
  120. package/examples/workflows/branch.workflow.ts +0 -4
  121. package/examples/workflows/echo.workflow.ts +0 -2
  122. package/examples/workflows/live-settings.workflow.ts +0 -2
  123. package/examples/workflows/two-turn.workflow.ts +0 -2
  124. package/herdr-plugin.toml +1 -1
  125. package/package.json +1 -1
  126. package/protocol/fixtures/client-v1.json +1 -1
  127. package/skills/pi-workflows/SKILL.md +8 -7
  128. package/src/builtins/autoimplement.workflow.ts +31 -10
  129. package/src/client/client.ts +3 -1
  130. package/src/client/protocol.ts +21 -1
  131. package/src/client/view.ts +9 -5
  132. package/src/extension/index.ts +117 -189
  133. package/src/extension/recorder.ts +2 -2
  134. package/src/extension/step-message.ts +4 -4
  135. package/src/extension/workflow-message-coordinator.ts +78 -50
  136. package/src/resource-managers/index.ts +1 -9
  137. package/src/resource-managers/sqlite.ts +330 -2655
  138. package/src/resource-managers/types.ts +2 -3
  139. package/src/server/resource-runner-entry.ts +2 -7
  140. package/src/server/rpc-bridge.ts +1 -1
  141. package/src/server/rpc-executor.ts +4 -9
  142. package/src/server/server-entry.ts +3 -0
  143. package/src/server/server.ts +416 -641
  144. package/src/server/state.ts +122 -466
  145. package/src/server/view.ts +56 -60
  146. package/src/server/workflow-runner-entry.ts +2 -55
  147. package/src/server/workflow-runner-protocol.ts +5 -15
  148. package/src/server/workflow-runner-store.ts +12 -27
  149. package/src/state/attempt-time.ts +122 -0
  150. package/src/state/json.ts +2 -1
  151. package/src/state/project-store.ts +218 -0
  152. package/src/state/prune.ts +0 -13
  153. package/src/state/schema.ts +23 -23
  154. package/src/state/workflow-messages.ts +4 -2
  155. package/src/viewer/backup.ts +7 -0
  156. package/src/workflows/diagnostics.ts +78 -0
  157. package/src/workflows/engine.ts +133 -291
  158. package/src/workflows/human-decision.ts +3 -81
  159. package/src/workflows/index.ts +8 -2
  160. package/src/workflows/queue.ts +2091 -0
  161. package/src/workflows/requests.ts +283 -0
  162. package/src/workflows/schema.ts +2 -6
  163. package/src/workflows/store.ts +223 -231
  164. package/src/workflows/tool-input.ts +20 -12
  165. package/src/workflows/transitions.ts +330 -0
  166. package/src/workflows/types.ts +9 -46
  167. package/src/workflows/workflow-message-content.ts +28 -4
  168. package/dist/resource-managers/runtime.d.ts +0 -59
  169. package/dist/resource-managers/runtime.js +0 -423
  170. package/dist/resource-managers/runtime.js.map +0 -1
  171. package/src/resource-managers/runtime.ts +0 -576
@@ -86,33 +86,22 @@ export function parseWorkflowArgs(args) {
86
86
  return { kind: "status", runId };
87
87
  }
88
88
  if (trimmed === "answer" || trimmed.startsWith("answer ")) {
89
- let rest = trimmed === "answer" ? "" : trimmed.slice("answer".length).trim();
90
- if (rest.length === 0) {
91
- throw new Error('answer requires a JSON value or text, e.g. /workflow answer {"approved":true}');
92
- }
93
- let runId;
89
+ const rest = trimmed === "answer" ? "" : trimmed.slice("answer".length).trim();
94
90
  const firstSpace = rest.search(/\s/);
95
- if (firstSpace > 0) {
96
- const candidate = rest.slice(0, firstSpace);
97
- const remainder = rest.slice(firstSpace).trim();
98
- if (validRunId(candidate) &&
99
- remainder.length > 0 &&
100
- (remainder.startsWith("{") || remainder.startsWith("["))) {
101
- runId = candidate;
102
- rest = remainder;
103
- }
91
+ if (firstSpace <= 0 || rest.slice(firstSpace).trim().length === 0) {
92
+ throw new Error('answer requires an exact request id and a response: /workflow answer REQUEST_ID {"choice":"approve"}');
104
93
  }
94
+ const requestId = rest.slice(0, firstSpace);
95
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(requestId))
96
+ throw new Error("answer requires a valid request id");
97
+ const text = rest.slice(firstSpace).trim();
105
98
  try {
106
- return {
107
- kind: "answer",
108
- input: JSON.parse(rest),
109
- ...(runId === undefined ? {} : { runId }),
110
- };
99
+ return { kind: "answer", requestId, input: JSON.parse(text) };
111
100
  }
112
101
  catch {
113
- if (runId !== undefined)
114
- throw new Error("answer with a run id requires a JSON value");
115
- return { kind: "answer", input: { answer: rest } };
102
+ if (/^[{["]/.test(text))
103
+ throw new Error("answer contains invalid JSON");
104
+ return { kind: "answer", requestId, input: { answer: text } };
116
105
  }
117
106
  }
118
107
  const spaceIndex = trimmed.search(/\s/);
@@ -143,6 +132,7 @@ export default function piWorkflows(pi) {
143
132
  const sessionView = new SessionWorkflowView();
144
133
  const sessionRecorders = new Map();
145
134
  let agentRunning = false;
135
+ let lastStopReason = "completed";
146
136
  let activeRecorder = null;
147
137
  let activeRecorderMessageId = null;
148
138
  const ensureRecorder = async (message, ctx) => {
@@ -163,7 +153,7 @@ export default function piWorkflows(pi) {
163
153
  if (message === undefined || activeRecorderMessageId === message.workflowMessageId)
164
154
  return;
165
155
  const contract = agentContractForWorkflowMessage(message);
166
- if (contract === undefined && message.kind !== "terminal" && message.kind !== "followUp") {
156
+ if (contract === undefined && message.kind !== "followUp") {
167
157
  return;
168
158
  }
169
159
  const recorder = await ensureRecorder(message, ctx);
@@ -172,7 +162,7 @@ export default function piWorkflows(pi) {
172
162
  return;
173
163
  }
174
164
  if (contract === undefined) {
175
- recorder.beginWorkflowMessage(message.workflowMessageId, message.kind);
165
+ recorder.beginFollowUp(message.workflowMessageId);
176
166
  }
177
167
  else {
178
168
  recorder.beginAttempt(contract);
@@ -188,7 +178,28 @@ export default function piWorkflows(pi) {
188
178
  });
189
179
  await prior;
190
180
  try {
191
- await workflowMessages.synchronize(pi, client, ctx);
181
+ const finishRecording = async (message) => {
182
+ const recorder = sessionRecorders.get(message.runId);
183
+ if (recorder === undefined)
184
+ return;
185
+ await recorder.record(ctx).catch((error) => {
186
+ ctx.ui.notify(`Workflow conversation recording failed: ${errorMessage(error)}`, "warning");
187
+ });
188
+ await recorder.finish();
189
+ sessionRecorders.delete(message.runId);
190
+ if (activeRecorder === recorder)
191
+ activeRecorder = null;
192
+ };
193
+ await workflowMessages.synchronize(pi, client, ctx, {
194
+ beforeTurnEnd: async (message, end) => {
195
+ if (end.stopReason === "completed") {
196
+ await submitVisibleAssistantResponse(client, ctx, message, end.responseSessionEntryId);
197
+ }
198
+ if (message.kind === "followUp")
199
+ await finishRecording(message);
200
+ },
201
+ terminalDelivered: finishRecording,
202
+ });
192
203
  await activateRecorder(ctx);
193
204
  }
194
205
  finally {
@@ -412,7 +423,7 @@ export default function piWorkflows(pi) {
412
423
  "List, start, restart, inspect, change settings, queue or remove follow-ups, pause, resume, cancel, answer ordinary checkpoints, update, or complete hosted workflow runs.",
413
424
  "Protected human decisions cannot be answered with this model-facing tool.",
414
425
  "When the user asks to continue or resume the active workflow, call workflow resume immediately.",
415
- "Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
426
+ "Use update or submit only when a workflow step contract asks for it, and pass its exact requestId.",
416
427
  "Do not start repeated work without the user's request.",
417
428
  ].join(" "),
418
429
  parameters: WorkflowToolParameters,
@@ -420,28 +431,16 @@ export default function piWorkflows(pi) {
420
431
  return await runToolInOrder(async () => {
421
432
  const params = parseWorkflowToolInput(rawParams);
422
433
  if (params.action === "update" || params.action === "submit") {
423
- const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
424
- if (interaction === undefined)
425
- throw new Error("No workflow step is waiting for output");
426
- const contract = agentContract(interaction);
427
- if (contract === undefined)
428
- throw new Error("The pending interaction is not an agent step");
429
- if (contract.nodeId !== params.step || contract.attemptId !== params.attempt) {
430
- throw new Error("Workflow step or attempt does not match the durable request");
431
- }
432
434
  let response;
433
435
  try {
434
436
  response = await requestAccepted(client, {
435
437
  operation: params.action === "update" ? "interaction.update" : "interaction.submit",
436
438
  requestId: `${params.action}-${toolCallId}-${randomUUID()}`,
437
439
  idempotencyKey: toolCallId,
438
- runId: interaction.runId,
439
- expectedRevision: interaction.revision,
440
440
  payload: jsonValue({
441
- requestId: interaction.requestId,
441
+ ...sessionCommandPayload(ctx),
442
+ requestId: params.requestId,
442
443
  submissionId: toolCallId,
443
- step: params.step,
444
- attempt: params.attempt,
445
444
  value: params.action === "update"
446
445
  ? { update: params.update }
447
446
  : { output: params.output },
@@ -592,35 +591,16 @@ export default function piWorkflows(pi) {
592
591
  });
593
592
  pi.on("agent_start", async (_event, ctx) => {
594
593
  agentRunning = true;
594
+ lastStopReason = "completed";
595
595
  activeRecorder = null;
596
596
  activeRecorderMessageId = null;
597
597
  workflowMessages.startTurn();
598
598
  await presentInOrder(ctx).catch(() => undefined);
599
599
  });
600
- pi.on("agent_end", async (event, ctx) => {
601
- try {
602
- await submitVisibleAssistantResponse(client, ctx);
603
- }
604
- catch (error) {
605
- ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
606
- }
607
- const finishedMessage = workflowMessages.activeTurnMessage();
600
+ pi.on("agent_end", (event, ctx) => {
608
601
  agentRunning = false;
609
- activeRecorderMessageId = null;
610
- workflowMessages.endTurn(workflowTurnStopReason(event.messages, ctx.signal?.aborted === true), responseEntryId(ctx.sessionManager.getBranch()));
611
- if (activeRecorder !== null &&
612
- finishedMessage !== undefined &&
613
- (finishedMessage.kind === "terminal" || finishedMessage.kind === "followUp")) {
614
- const finishedRecorder = activeRecorder;
615
- activeRecorder = null;
616
- await finishedRecorder.finish();
617
- if (sessionRecorders.get(finishedMessage.runId) === finishedRecorder) {
618
- sessionRecorders.delete(finishedMessage.runId);
619
- }
620
- }
621
- await presentInOrder(ctx).catch((error) => {
622
- ctx.ui.notify(`Could not record workflow model activity: ${errorMessage(error)}`, "warning");
623
- });
602
+ lastStopReason = workflowTurnStopReason(event.messages, ctx.signal?.aborted === true);
603
+ // Pi can retry after agent_end. Only agent_settled completes the owned turn.
624
604
  });
625
605
  pi.on("turn_start", (event) => {
626
606
  activeRecorder?.handleTurnStart(event);
@@ -648,13 +628,11 @@ export default function piWorkflows(pi) {
648
628
  });
649
629
  pi.on("agent_settled", async (_event, ctx) => {
650
630
  activeRecorder?.settleAttempt();
651
- try {
652
- await submitVisibleAssistantResponse(client, ctx);
653
- }
654
- catch (error) {
655
- ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
656
- }
657
- await presentInOrder(ctx).catch(() => undefined);
631
+ workflowMessages.endTurn(lastStopReason, responseEntryId(ctx.sessionManager.getBranch()));
632
+ activeRecorderMessageId = null;
633
+ await presentInOrder(ctx).catch((error) => {
634
+ ctx.ui.notify(`Could not record workflow model activity: ${errorMessage(error)}`, "warning");
635
+ });
658
636
  });
659
637
  pi.on("session_shutdown", async (_event, ctx) => {
660
638
  sessionGeneration += 1;
@@ -788,17 +766,16 @@ async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(
788
766
  if (runId === undefined)
789
767
  throw new Error("No workflow terminal result is available to restart");
790
768
  const session = sessionCommandPayload(ctx);
791
- const marker = terminalMessageForSession(ctx, runId);
769
+ const expectedRevision = command.expectedRevision ?? (await client.getRun(runId))?.runRevision;
770
+ if (expectedRevision === undefined)
771
+ throw new Error("Workflow run revision is unavailable");
792
772
  const response = await requestAccepted(client, {
793
773
  operation: "run.restart",
794
774
  requestId: `restart-${idempotencyKey}`,
795
775
  idempotencyKey,
796
776
  runId,
797
- payload: {
798
- ...session,
799
- workflowMessageId: marker.workflowMessageId,
800
- ...(marker.workflowTurnId === undefined ? {} : { workflowTurnId: marker.workflowTurnId }),
801
- },
777
+ expectedRevision,
778
+ payload: session,
802
779
  });
803
780
  return {
804
781
  message: `Restarted workflow ${runId}.`,
@@ -906,11 +883,30 @@ async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(
906
883
  };
907
884
  }
908
885
  case "answer": {
909
- const interaction = pendingDecision(ctx.sessionManager.getSessionId(), command.runId);
910
- if (interaction !== undefined) {
911
- if (authority !== "human") {
912
- throw new Error("Protected human decisions cannot be answered by the workflow tool");
913
- }
886
+ if (authority === "model") {
887
+ const response = await requestAccepted(client, {
888
+ operation: "checkpoint.answer",
889
+ requestId: `checkpoint-${idempotencyKey}`,
890
+ idempotencyKey,
891
+ payload: {
892
+ ...sessionCommandPayload(ctx),
893
+ requestId: command.requestId,
894
+ submissionId: idempotencyKey,
895
+ input: jsonValue(command.input),
896
+ },
897
+ });
898
+ return {
899
+ message: `Answered checkpoint ${command.requestId}.`,
900
+ details: { action: "answer", response: response.receipt ?? null },
901
+ };
902
+ }
903
+ const interaction = sessionSnapshots
904
+ .get(ctx.sessionManager.getSessionId())
905
+ ?.pendingInteractions.map(parseInteractiveRequest)
906
+ .find((request) => request?.requestId === command.requestId);
907
+ if (interaction === undefined)
908
+ throw new Error("No matching checkpoint request is waiting in this session");
909
+ if (interaction.kind === "decision") {
914
910
  const response = await requestAccepted(client, {
915
911
  operation: "decision.answer",
916
912
  requestId: `answer-${idempotencyKey}`,
@@ -918,6 +914,7 @@ async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(
918
914
  runId: interaction.runId,
919
915
  expectedRevision: interaction.revision,
920
916
  payload: {
917
+ ...sessionCommandPayload(ctx),
921
918
  requestId: interaction.requestId,
922
919
  submissionId: idempotencyKey,
923
920
  response: decisionResponse(command.input),
@@ -932,32 +929,26 @@ async function executeCommand(client, ctx, command, idempotencyKey = randomUUID(
932
929
  },
933
930
  };
934
931
  }
935
- const parent = sessionRun(ctx, command.runId);
936
- if (parent === undefined)
937
- throw new Error("No checkpoint is waiting in this session");
938
- const continuationRunId = createRunId(parent.workflowName);
932
+ if (interaction.kind !== "checkpoint")
933
+ throw new Error("Only an ordinary checkpoint accepts an answer");
939
934
  const response = await requestAccepted(client, {
940
935
  operation: "checkpoint.answer",
941
936
  requestId: `checkpoint-${idempotencyKey}`,
942
937
  idempotencyKey,
943
- runId: parent.runId,
938
+ runId: interaction.runId,
939
+ expectedRevision: interaction.revision,
944
940
  payload: {
945
- continuationRunId,
941
+ ...sessionCommandPayload(ctx),
942
+ requestId: interaction.requestId,
943
+ submissionId: idempotencyKey,
946
944
  input: jsonValue(command.input),
947
945
  },
948
946
  });
949
- const receipt = isRecord(response.receipt) ? response.receipt : undefined;
950
- const actualRunId = receipt !== undefined && typeof receipt.runId === "string"
951
- ? receipt.runId
952
- : continuationRunId;
953
947
  return {
954
- message: receipt?.alreadyAnswered === true
955
- ? `Checkpoint ${parent.runId} was already answered; continuation ${actualRunId} exists.`
956
- : `Answered checkpoint ${parent.runId}; continuation ${actualRunId} started.`,
948
+ message: `Answered checkpoint ${interaction.requestId}; run ${interaction.runId} can continue.`,
957
949
  details: {
958
950
  action: "answer",
959
- parentRunId: parent.runId,
960
- runId: actualRunId,
951
+ runId: interaction.runId,
961
952
  response: response.receipt ?? null,
962
953
  },
963
954
  };
@@ -1023,50 +1014,36 @@ async function executeResourceManagerCommand(client, ctx, command) {
1023
1014
  details: { action: command.kind, resource: response.receipt ?? null },
1024
1015
  };
1025
1016
  }
1026
- async function submitVisibleAssistantResponse(client, ctx) {
1027
- const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
1028
- if (interaction === undefined ||
1029
- interaction.kind !== "assistant" ||
1030
- workflowRunPaused(interaction.runId)) {
1017
+ async function submitVisibleAssistantResponse(client, ctx, message, settledResponseEntryId) {
1018
+ const contract = agentContractForWorkflowMessage(message);
1019
+ if (contract?.completion !== "assistant" || workflowRunPaused(message.runId))
1031
1020
  return;
1032
- }
1033
- const contract = agentContract(interaction);
1034
- if (contract === undefined)
1021
+ if (settledResponseEntryId === null)
1035
1022
  return;
1036
- const submission = recoverAssistantStep(ctx.sessionManager.getBranch(), contract);
1023
+ const branch = ctx.sessionManager.getBranch();
1024
+ const responseIndex = branch.findIndex((entry) => entry.id === settledResponseEntryId);
1025
+ if (responseIndex < 0)
1026
+ return;
1027
+ const submission = recoverAssistantStep(branch.slice(0, responseIndex + 1), contract);
1037
1028
  if (submission === undefined)
1038
1029
  return;
1039
1030
  const responseId = submission.conversation?.lastEntryId ?? submission.assistantMessage?.entryId;
1040
- if (responseId === undefined)
1031
+ if (responseId !== settledResponseEntryId)
1041
1032
  return;
1042
- await requestAccepted(client, {
1043
- operation: "interaction.submit",
1044
- requestId: `assistant-${interaction.requestId}-${responseId}`,
1045
- idempotencyKey: `assistant-${interaction.requestId}-${responseId}`,
1046
- runId: interaction.runId,
1047
- expectedRevision: interaction.revision,
1033
+ const response = await client.request({
1034
+ operation: "interaction.assistant",
1035
+ requestId: `assistant-${message.sourceId}-${responseId}`,
1036
+ idempotencyKey: `assistant-${message.sourceId}-${responseId}`,
1048
1037
  payload: {
1049
- requestId: interaction.requestId,
1038
+ ...sessionCommandPayload(ctx),
1039
+ requestId: message.sourceId,
1050
1040
  submissionId: `assistant-${responseId}`,
1051
- step: contract.nodeId,
1052
- attempt: contract.attemptId,
1053
1041
  value: submission,
1054
1042
  },
1055
1043
  });
1056
- }
1057
- function pendingInteractionForSession(sessionId) {
1058
- return sessionSnapshots
1059
- .get(sessionId)
1060
- ?.pendingInteractions.map(parseInteractiveRequest)
1061
- .find((request) => request !== undefined);
1062
- }
1063
- function pendingDecision(sessionId, runId) {
1064
- return sessionSnapshots
1065
- .get(sessionId)
1066
- ?.pendingInteractions.map(parseInteractiveRequest)
1067
- .find((request) => request !== undefined &&
1068
- request.kind === "decision" &&
1069
- (runId === undefined || request.runId === runId));
1044
+ if (response.outcome !== "accepted" && response.outcome !== "adopted") {
1045
+ ctx.ui.notify(`Workflow response was rejected: ${response.error ?? response.outcome}`, "error");
1046
+ }
1070
1047
  }
1071
1048
  function workflowRunPaused(runId) {
1072
1049
  for (const session of sessionSnapshots.values()) {
@@ -1089,21 +1066,6 @@ function sessionCommandPayload(ctx) {
1089
1066
  }
1090
1067
  return { targetSessionId, coordinatorEpoch: session.coordinatorEpoch };
1091
1068
  }
1092
- function terminalMessageForSession(ctx, runId) {
1093
- const session = sessionSnapshots.get(ctx.sessionManager.getSessionId());
1094
- const message = session?.workflowMessages
1095
- .filter((item) => item.kind === "terminal" && item.runId === runId && item.status === "sent")
1096
- .sort((left, right) => right.order - left.order)[0];
1097
- if (message === undefined)
1098
- throw new Error(`Workflow run ${runId} has no current terminal result`);
1099
- const turn = session?.openWorkflowTurn;
1100
- return {
1101
- workflowMessageId: message.workflowMessageId,
1102
- ...(turn?.workflowMessageId === message.workflowMessageId
1103
- ? { workflowTurnId: turn.workflowTurnId }
1104
- : {}),
1105
- };
1106
- }
1107
1069
  function sessionRun(ctx, runId) {
1108
1070
  const session = sessionSnapshots.get(ctx.sessionManager.getSessionId());
1109
1071
  if (session?.run === null || session?.run === undefined || !isRecord(session.run.queue)) {
@@ -1132,7 +1094,7 @@ function toolInputToCommand(params) {
1132
1094
  case "start":
1133
1095
  return { kind: "run", ref: params.workflow, input: params.input ?? {} };
1134
1096
  case "restart":
1135
- return { kind: "restart", runId: params.runId };
1097
+ return { kind: "restart", runId: params.runId, expectedRevision: params.expectedRevision };
1136
1098
  case "change-settings":
1137
1099
  return {
1138
1100
  kind: "change-settings",
@@ -1168,8 +1130,8 @@ function toolInputToCommand(params) {
1168
1130
  case "answer":
1169
1131
  return {
1170
1132
  kind: "answer",
1171
- input: params.input ?? {},
1172
- ...(params.runId === undefined ? {} : { runId: params.runId }),
1133
+ requestId: params.requestId,
1134
+ input: params.input,
1173
1135
  };
1174
1136
  case "update":
1175
1137
  case "submit":
@@ -1186,28 +1148,13 @@ async function requestAccepted(client, options) {
1186
1148
  }
1187
1149
  return response;
1188
1150
  }
1189
- function interactionContract(interaction) {
1190
- if (!isRecord(interaction.contract))
1191
- throw new Error("Stored interaction contract is invalid");
1192
- return interaction.contract;
1193
- }
1194
- function agentContract(interaction) {
1195
- const value = interactionContract(interaction).contract;
1196
- if (!isRecord(value) ||
1197
- typeof value.runId !== "string" ||
1198
- typeof value.workflowName !== "string" ||
1199
- typeof value.nodeId !== "string" ||
1200
- typeof value.attemptId !== "string" ||
1201
- (value.completion !== "submit" && value.completion !== "assistant")) {
1202
- return undefined;
1203
- }
1204
- return value;
1205
- }
1206
1151
  function agentContractForWorkflowMessage(message) {
1207
1152
  if (message.kind !== "step" || !isRecord(message.content.details))
1208
1153
  return undefined;
1209
1154
  const value = message.content.details.contract;
1210
1155
  if (!isRecord(value) ||
1156
+ value.requestId !== message.sourceId ||
1157
+ value.runId !== message.runId ||
1211
1158
  typeof value.runId !== "string" ||
1212
1159
  typeof value.workflowName !== "string" ||
1213
1160
  typeof value.nodeId !== "string" ||
@@ -1303,7 +1250,10 @@ function parseInteractiveRequest(value) {
1303
1250
  typeof value.runId !== "string" ||
1304
1251
  typeof value.targetSessionId !== "string" ||
1305
1252
  typeof value.revision !== "number" ||
1306
- (value.kind !== "agent" && value.kind !== "assistant" && value.kind !== "decision")) {
1253
+ (value.kind !== "agent" &&
1254
+ value.kind !== "assistant" &&
1255
+ value.kind !== "checkpoint" &&
1256
+ value.kind !== "decision")) {
1307
1257
  return undefined;
1308
1258
  }
1309
1259
  return value;