@osolmaz/pi-workflows 0.16.4 → 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 (173) 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 +22 -8
  39. package/dist/server/server.js +498 -543
  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.d.ts +28 -0
  65. package/dist/state/prune.js +278 -83
  66. package/dist/state/prune.js.map +1 -1
  67. package/dist/state/schema.js +23 -23
  68. package/dist/state/workflow-messages.d.ts +1 -1
  69. package/dist/state/workflow-messages.js +3 -1
  70. package/dist/state/workflow-messages.js.map +1 -1
  71. package/dist/viewer/backup.js +5 -0
  72. package/dist/viewer/backup.js.map +1 -1
  73. package/dist/workflows/diagnostics.d.ts +8 -0
  74. package/dist/workflows/diagnostics.js +61 -0
  75. package/dist/workflows/diagnostics.js.map +1 -0
  76. package/dist/workflows/engine.d.ts +2 -15
  77. package/dist/workflows/engine.js +125 -245
  78. package/dist/workflows/engine.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +3 -7
  80. package/dist/workflows/human-decision.js +2 -56
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +2 -1
  83. package/dist/workflows/index.js +1 -0
  84. package/dist/workflows/index.js.map +1 -1
  85. package/dist/workflows/queue.d.ts +267 -0
  86. package/dist/workflows/queue.js +1248 -0
  87. package/dist/workflows/queue.js.map +1 -0
  88. package/dist/workflows/requests.d.ts +47 -0
  89. package/dist/workflows/requests.js +158 -0
  90. package/dist/workflows/requests.js.map +1 -0
  91. package/dist/workflows/schema.js +2 -4
  92. package/dist/workflows/schema.js.map +1 -1
  93. package/dist/workflows/store.d.ts +11 -16
  94. package/dist/workflows/store.js +174 -169
  95. package/dist/workflows/store.js.map +1 -1
  96. package/dist/workflows/tool-input.d.ts +6 -9
  97. package/dist/workflows/tool-input.js +15 -12
  98. package/dist/workflows/tool-input.js.map +1 -1
  99. package/dist/workflows/transitions.d.ts +38 -0
  100. package/dist/workflows/transitions.js +276 -0
  101. package/dist/workflows/transitions.js.map +1 -0
  102. package/dist/workflows/types.d.ts +9 -42
  103. package/dist/workflows/workflow-message-content.d.ts +7 -1
  104. package/dist/workflows/workflow-message-content.js +22 -4
  105. package/dist/workflows/workflow-message-content.js.map +1 -1
  106. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  107. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  108. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  109. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  110. package/docs/DEFERRED_TURNS.md +84 -105
  111. package/docs/HUMAN_DECISIONS.md +17 -19
  112. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  113. package/docs/SQLITE_STATE.md +87 -30
  114. package/docs/WORKFLOW_SERVER.md +50 -11
  115. package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
  116. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  117. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  118. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  119. package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
  120. package/docs/workflows.md +109 -44
  121. package/examples/workflows/autoresearch.workflow.ts +0 -2
  122. package/examples/workflows/branch.workflow.ts +0 -4
  123. package/examples/workflows/echo.workflow.ts +0 -2
  124. package/examples/workflows/live-settings.workflow.ts +0 -2
  125. package/examples/workflows/two-turn.workflow.ts +0 -2
  126. package/herdr-plugin.toml +1 -1
  127. package/package.json +1 -1
  128. package/protocol/fixtures/client-v1.json +1 -1
  129. package/skills/pi-workflows/SKILL.md +8 -7
  130. package/src/builtins/autoimplement.workflow.ts +31 -10
  131. package/src/client/client.ts +3 -1
  132. package/src/client/protocol.ts +21 -1
  133. package/src/client/view.ts +9 -5
  134. package/src/extension/index.ts +117 -189
  135. package/src/extension/recorder.ts +2 -2
  136. package/src/extension/step-message.ts +4 -4
  137. package/src/extension/workflow-message-coordinator.ts +78 -50
  138. package/src/resource-managers/index.ts +1 -9
  139. package/src/resource-managers/sqlite.ts +330 -2655
  140. package/src/resource-managers/types.ts +2 -3
  141. package/src/server/resource-runner-entry.ts +2 -7
  142. package/src/server/rpc-bridge.ts +1 -1
  143. package/src/server/rpc-executor.ts +4 -9
  144. package/src/server/server-entry.ts +3 -0
  145. package/src/server/server.ts +568 -644
  146. package/src/server/state.ts +122 -466
  147. package/src/server/view.ts +56 -60
  148. package/src/server/workflow-runner-entry.ts +2 -55
  149. package/src/server/workflow-runner-protocol.ts +5 -15
  150. package/src/server/workflow-runner-store.ts +12 -27
  151. package/src/state/attempt-time.ts +122 -0
  152. package/src/state/json.ts +2 -1
  153. package/src/state/project-store.ts +218 -0
  154. package/src/state/prune.ts +408 -95
  155. package/src/state/schema.ts +23 -23
  156. package/src/state/workflow-messages.ts +4 -2
  157. package/src/viewer/backup.ts +7 -0
  158. package/src/workflows/diagnostics.ts +78 -0
  159. package/src/workflows/engine.ts +133 -291
  160. package/src/workflows/human-decision.ts +3 -81
  161. package/src/workflows/index.ts +8 -2
  162. package/src/workflows/queue.ts +2091 -0
  163. package/src/workflows/requests.ts +283 -0
  164. package/src/workflows/schema.ts +2 -6
  165. package/src/workflows/store.ts +223 -231
  166. package/src/workflows/tool-input.ts +20 -12
  167. package/src/workflows/transitions.ts +330 -0
  168. package/src/workflows/types.ts +9 -46
  169. package/src/workflows/workflow-message-content.ts +28 -4
  170. package/dist/resource-managers/runtime.d.ts +0 -59
  171. package/dist/resource-managers/runtime.js +0 -423
  172. package/dist/resource-managers/runtime.js.map +0 -1
  173. package/src/resource-managers/runtime.ts +0 -576
@@ -67,7 +67,7 @@ export type ParsedWorkflowArgs =
67
67
  | { kind: "resume" }
68
68
  | { kind: "status"; runId?: string }
69
69
  | { kind: "clear"; runId?: string }
70
- | { kind: "restart"; runId?: string }
70
+ | { kind: "restart"; runId?: string; expectedRevision?: number }
71
71
  | {
72
72
  kind: "change-settings";
73
73
  patch: unknown;
@@ -77,7 +77,7 @@ export type ParsedWorkflowArgs =
77
77
  }
78
78
  | { kind: "queue-follow-up"; prompt: string; runId?: string }
79
79
  | { kind: "remove-follow-up"; followUpId: string; runId?: string }
80
- | { kind: "answer"; input: unknown; runId?: string | undefined }
80
+ | { kind: "answer"; requestId: string; input: unknown }
81
81
  | { kind: "run"; ref: string; input: unknown };
82
82
 
83
83
  /** Parse `/workflow` arguments. Exported for tests. */
@@ -131,35 +131,22 @@ export function parseWorkflowArgs(args: string): ParsedWorkflowArgs {
131
131
  return { kind: "status", runId };
132
132
  }
133
133
  if (trimmed === "answer" || trimmed.startsWith("answer ")) {
134
- let rest = trimmed === "answer" ? "" : trimmed.slice("answer".length).trim();
135
- if (rest.length === 0) {
134
+ const rest = trimmed === "answer" ? "" : trimmed.slice("answer".length).trim();
135
+ const firstSpace = rest.search(/\s/);
136
+ if (firstSpace <= 0 || rest.slice(firstSpace).trim().length === 0) {
136
137
  throw new Error(
137
- 'answer requires a JSON value or text, e.g. /workflow answer {"approved":true}',
138
+ 'answer requires an exact request id and a response: /workflow answer REQUEST_ID {"choice":"approve"}',
138
139
  );
139
140
  }
140
- let runId: string | undefined;
141
- const firstSpace = rest.search(/\s/);
142
- if (firstSpace > 0) {
143
- const candidate = rest.slice(0, firstSpace);
144
- const remainder = rest.slice(firstSpace).trim();
145
- if (
146
- validRunId(candidate) &&
147
- remainder.length > 0 &&
148
- (remainder.startsWith("{") || remainder.startsWith("["))
149
- ) {
150
- runId = candidate;
151
- rest = remainder;
152
- }
153
- }
141
+ const requestId = rest.slice(0, firstSpace);
142
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(requestId))
143
+ throw new Error("answer requires a valid request id");
144
+ const text = rest.slice(firstSpace).trim();
154
145
  try {
155
- return {
156
- kind: "answer",
157
- input: JSON.parse(rest) as unknown,
158
- ...(runId === undefined ? {} : { runId }),
159
- };
146
+ return { kind: "answer", requestId, input: JSON.parse(text) as unknown };
160
147
  } catch {
161
- if (runId !== undefined) throw new Error("answer with a run id requires a JSON value");
162
- return { kind: "answer", input: { answer: rest } };
148
+ if (/^[{["]/.test(text)) throw new Error("answer contains invalid JSON");
149
+ return { kind: "answer", requestId, input: { answer: text } };
163
150
  }
164
151
  }
165
152
  const spaceIndex = trimmed.search(/\s/);
@@ -190,6 +177,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
190
177
  const sessionView = new SessionWorkflowView();
191
178
  const sessionRecorders = new Map<string, SessionRecorder>();
192
179
  let agentRunning = false;
180
+ let lastStopReason: ReturnType<typeof workflowTurnStopReason> = "completed";
193
181
  let activeRecorder: SessionRecorder | null = null;
194
182
  let activeRecorderMessageId: string | null = null;
195
183
 
@@ -216,7 +204,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
216
204
  const message = workflowMessages.activeTurnMessage();
217
205
  if (message === undefined || activeRecorderMessageId === message.workflowMessageId) return;
218
206
  const contract = agentContractForWorkflowMessage(message);
219
- if (contract === undefined && message.kind !== "terminal" && message.kind !== "followUp") {
207
+ if (contract === undefined && message.kind !== "followUp") {
220
208
  return;
221
209
  }
222
210
  const recorder = await ensureRecorder(message, ctx);
@@ -227,10 +215,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
227
215
  return;
228
216
  }
229
217
  if (contract === undefined) {
230
- recorder.beginWorkflowMessage(
231
- message.workflowMessageId,
232
- message.kind as "terminal" | "followUp",
233
- );
218
+ recorder.beginFollowUp(message.workflowMessageId);
234
219
  } else {
235
220
  recorder.beginAttempt(contract);
236
221
  }
@@ -246,7 +231,28 @@ export default function piWorkflows(pi: ExtensionAPI): void {
246
231
  });
247
232
  await prior;
248
233
  try {
249
- await workflowMessages.synchronize(pi, client, ctx);
234
+ const finishRecording = async (message: WorkflowMessage): Promise<void> => {
235
+ const recorder = sessionRecorders.get(message.runId);
236
+ if (recorder === undefined) return;
237
+ await recorder.record(ctx).catch((error) => {
238
+ ctx.ui.notify(
239
+ `Workflow conversation recording failed: ${errorMessage(error)}`,
240
+ "warning",
241
+ );
242
+ });
243
+ await recorder.finish();
244
+ sessionRecorders.delete(message.runId);
245
+ if (activeRecorder === recorder) activeRecorder = null;
246
+ };
247
+ await workflowMessages.synchronize(pi, client, ctx, {
248
+ beforeTurnEnd: async (message, end) => {
249
+ if (end.stopReason === "completed") {
250
+ await submitVisibleAssistantResponse(client, ctx, message, end.responseSessionEntryId);
251
+ }
252
+ if (message.kind === "followUp") await finishRecording(message);
253
+ },
254
+ terminalDelivered: finishRecording,
255
+ });
250
256
  await activateRecorder(ctx);
251
257
  } finally {
252
258
  sessionView.refresh(ctx);
@@ -495,7 +501,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
495
501
  "List, start, restart, inspect, change settings, queue or remove follow-ups, pause, resume, cancel, answer ordinary checkpoints, update, or complete hosted workflow runs.",
496
502
  "Protected human decisions cannot be answered with this model-facing tool.",
497
503
  "When the user asks to continue or resume the active workflow, call workflow resume immediately.",
498
- "Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
504
+ "Use update or submit only when a workflow step contract asks for it, and pass its exact requestId.",
499
505
  "Do not start repeated work without the user's request.",
500
506
  ].join(" "),
501
507
  parameters: WorkflowToolParameters,
@@ -503,27 +509,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
503
509
  return await runToolInOrder(async () => {
504
510
  const params = parseWorkflowToolInput(rawParams);
505
511
  if (params.action === "update" || params.action === "submit") {
506
- const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
507
- if (interaction === undefined) throw new Error("No workflow step is waiting for output");
508
- const contract = agentContract(interaction);
509
- if (contract === undefined)
510
- throw new Error("The pending interaction is not an agent step");
511
- if (contract.nodeId !== params.step || contract.attemptId !== params.attempt) {
512
- throw new Error("Workflow step or attempt does not match the durable request");
513
- }
514
512
  let response: ClientResponse;
515
513
  try {
516
514
  response = await requestAccepted(client, {
517
515
  operation: params.action === "update" ? "interaction.update" : "interaction.submit",
518
516
  requestId: `${params.action}-${toolCallId}-${randomUUID()}`,
519
517
  idempotencyKey: toolCallId,
520
- runId: interaction.runId,
521
- expectedRevision: interaction.revision,
522
518
  payload: jsonValue({
523
- requestId: interaction.requestId,
519
+ ...sessionCommandPayload(ctx),
520
+ requestId: params.requestId,
524
521
  submissionId: toolCallId,
525
- step: params.step,
526
- attempt: params.attempt,
527
522
  value:
528
523
  params.action === "update"
529
524
  ? { update: params.update }
@@ -692,40 +687,17 @@ export default function piWorkflows(pi: ExtensionAPI): void {
692
687
 
693
688
  pi.on("agent_start", async (_event, ctx) => {
694
689
  agentRunning = true;
690
+ lastStopReason = "completed";
695
691
  activeRecorder = null;
696
692
  activeRecorderMessageId = null;
697
693
  workflowMessages.startTurn();
698
694
  await presentInOrder(ctx).catch(() => undefined);
699
695
  });
700
696
 
701
- pi.on("agent_end", async (event, ctx) => {
702
- try {
703
- await submitVisibleAssistantResponse(client, ctx);
704
- } catch (error) {
705
- ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
706
- }
707
- const finishedMessage = workflowMessages.activeTurnMessage();
697
+ pi.on("agent_end", (event, ctx) => {
708
698
  agentRunning = false;
709
- activeRecorderMessageId = null;
710
- workflowMessages.endTurn(
711
- workflowTurnStopReason(event.messages, ctx.signal?.aborted === true),
712
- responseEntryId(ctx.sessionManager.getBranch()),
713
- );
714
- if (
715
- activeRecorder !== null &&
716
- finishedMessage !== undefined &&
717
- (finishedMessage.kind === "terminal" || finishedMessage.kind === "followUp")
718
- ) {
719
- const finishedRecorder = activeRecorder;
720
- activeRecorder = null;
721
- await finishedRecorder.finish();
722
- if (sessionRecorders.get(finishedMessage.runId) === finishedRecorder) {
723
- sessionRecorders.delete(finishedMessage.runId);
724
- }
725
- }
726
- await presentInOrder(ctx).catch((error) => {
727
- ctx.ui.notify(`Could not record workflow model activity: ${errorMessage(error)}`, "warning");
728
- });
699
+ lastStopReason = workflowTurnStopReason(event.messages, ctx.signal?.aborted === true);
700
+ // Pi can retry after agent_end. Only agent_settled completes the owned turn.
729
701
  });
730
702
 
731
703
  pi.on("turn_start", (event) => {
@@ -762,12 +734,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
762
734
 
763
735
  pi.on("agent_settled", async (_event, ctx) => {
764
736
  activeRecorder?.settleAttempt();
765
- try {
766
- await submitVisibleAssistantResponse(client, ctx);
767
- } catch (error) {
768
- ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
769
- }
770
- await presentInOrder(ctx).catch(() => undefined);
737
+ workflowMessages.endTurn(lastStopReason, responseEntryId(ctx.sessionManager.getBranch()));
738
+ activeRecorderMessageId = null;
739
+ await presentInOrder(ctx).catch((error) => {
740
+ ctx.ui.notify(`Could not record workflow model activity: ${errorMessage(error)}`, "warning");
741
+ });
771
742
  });
772
743
 
773
744
  pi.on("session_shutdown", async (_event, ctx) => {
@@ -920,17 +891,16 @@ async function executeCommand(
920
891
  if (runId === undefined)
921
892
  throw new Error("No workflow terminal result is available to restart");
922
893
  const session = sessionCommandPayload(ctx);
923
- const marker = terminalMessageForSession(ctx, runId);
894
+ const expectedRevision =
895
+ command.expectedRevision ?? (await client.getRun(runId))?.runRevision;
896
+ if (expectedRevision === undefined) throw new Error("Workflow run revision is unavailable");
924
897
  const response = await requestAccepted(client, {
925
898
  operation: "run.restart",
926
899
  requestId: `restart-${idempotencyKey}`,
927
900
  idempotencyKey,
928
901
  runId,
929
- payload: {
930
- ...session,
931
- workflowMessageId: marker.workflowMessageId,
932
- ...(marker.workflowTurnId === undefined ? {} : { workflowTurnId: marker.workflowTurnId }),
933
- },
902
+ expectedRevision,
903
+ payload: session,
934
904
  });
935
905
  return {
936
906
  message: `Restarted workflow ${runId}.`,
@@ -1033,11 +1003,30 @@ async function executeCommand(
1033
1003
  };
1034
1004
  }
1035
1005
  case "answer": {
1036
- const interaction = pendingDecision(ctx.sessionManager.getSessionId(), command.runId);
1037
- if (interaction !== undefined) {
1038
- if (authority !== "human") {
1039
- throw new Error("Protected human decisions cannot be answered by the workflow tool");
1040
- }
1006
+ if (authority === "model") {
1007
+ const response = await requestAccepted(client, {
1008
+ operation: "checkpoint.answer",
1009
+ requestId: `checkpoint-${idempotencyKey}`,
1010
+ idempotencyKey,
1011
+ payload: {
1012
+ ...sessionCommandPayload(ctx),
1013
+ requestId: command.requestId,
1014
+ submissionId: idempotencyKey,
1015
+ input: jsonValue(command.input),
1016
+ },
1017
+ });
1018
+ return {
1019
+ message: `Answered checkpoint ${command.requestId}.`,
1020
+ details: { action: "answer", response: response.receipt ?? null },
1021
+ };
1022
+ }
1023
+ const interaction = sessionSnapshots
1024
+ .get(ctx.sessionManager.getSessionId())
1025
+ ?.pendingInteractions.map(parseInteractiveRequest)
1026
+ .find((request) => request?.requestId === command.requestId);
1027
+ if (interaction === undefined)
1028
+ throw new Error("No matching checkpoint request is waiting in this session");
1029
+ if (interaction.kind === "decision") {
1041
1030
  const response = await requestAccepted(client, {
1042
1031
  operation: "decision.answer",
1043
1032
  requestId: `answer-${idempotencyKey}`,
@@ -1045,6 +1034,7 @@ async function executeCommand(
1045
1034
  runId: interaction.runId,
1046
1035
  expectedRevision: interaction.revision,
1047
1036
  payload: {
1037
+ ...sessionCommandPayload(ctx),
1048
1038
  requestId: interaction.requestId,
1049
1039
  submissionId: idempotencyKey,
1050
1040
  response: decisionResponse(command.input),
@@ -1059,33 +1049,26 @@ async function executeCommand(
1059
1049
  },
1060
1050
  };
1061
1051
  }
1062
- const parent = sessionRun(ctx, command.runId);
1063
- if (parent === undefined) throw new Error("No checkpoint is waiting in this session");
1064
- const continuationRunId = createRunId(parent.workflowName);
1052
+ if (interaction.kind !== "checkpoint")
1053
+ throw new Error("Only an ordinary checkpoint accepts an answer");
1065
1054
  const response = await requestAccepted(client, {
1066
1055
  operation: "checkpoint.answer",
1067
1056
  requestId: `checkpoint-${idempotencyKey}`,
1068
1057
  idempotencyKey,
1069
- runId: parent.runId,
1058
+ runId: interaction.runId,
1059
+ expectedRevision: interaction.revision,
1070
1060
  payload: {
1071
- continuationRunId,
1061
+ ...sessionCommandPayload(ctx),
1062
+ requestId: interaction.requestId,
1063
+ submissionId: idempotencyKey,
1072
1064
  input: jsonValue(command.input),
1073
1065
  },
1074
1066
  });
1075
- const receipt = isRecord(response.receipt) ? response.receipt : undefined;
1076
- const actualRunId =
1077
- receipt !== undefined && typeof receipt.runId === "string"
1078
- ? receipt.runId
1079
- : continuationRunId;
1080
1067
  return {
1081
- message:
1082
- receipt?.alreadyAnswered === true
1083
- ? `Checkpoint ${parent.runId} was already answered; continuation ${actualRunId} exists.`
1084
- : `Answered checkpoint ${parent.runId}; continuation ${actualRunId} started.`,
1068
+ message: `Answered checkpoint ${interaction.requestId}; run ${interaction.runId} can continue.`,
1085
1069
  details: {
1086
1070
  action: "answer",
1087
- parentRunId: parent.runId,
1088
- runId: actualRunId,
1071
+ runId: interaction.runId,
1089
1072
  response: response.receipt ?? null,
1090
1073
  },
1091
1074
  };
@@ -1162,54 +1145,33 @@ async function executeResourceManagerCommand(
1162
1145
  async function submitVisibleAssistantResponse(
1163
1146
  client: WorkflowClient,
1164
1147
  ctx: ExtensionContext,
1148
+ message: WorkflowMessage,
1149
+ settledResponseEntryId: string | null,
1165
1150
  ): Promise<void> {
1166
- const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
1167
- if (
1168
- interaction === undefined ||
1169
- interaction.kind !== "assistant" ||
1170
- workflowRunPaused(interaction.runId)
1171
- ) {
1172
- return;
1173
- }
1174
- const contract = agentContract(interaction);
1175
- if (contract === undefined) return;
1176
- const submission = recoverAssistantStep(ctx.sessionManager.getBranch(), contract);
1151
+ const contract = agentContractForWorkflowMessage(message);
1152
+ if (contract?.completion !== "assistant" || workflowRunPaused(message.runId)) return;
1153
+ if (settledResponseEntryId === null) return;
1154
+ const branch = ctx.sessionManager.getBranch();
1155
+ const responseIndex = branch.findIndex((entry) => entry.id === settledResponseEntryId);
1156
+ if (responseIndex < 0) return;
1157
+ const submission = recoverAssistantStep(branch.slice(0, responseIndex + 1), contract);
1177
1158
  if (submission === undefined) return;
1178
1159
  const responseId = submission.conversation?.lastEntryId ?? submission.assistantMessage?.entryId;
1179
- if (responseId === undefined) return;
1180
- await requestAccepted(client, {
1181
- operation: "interaction.submit",
1182
- requestId: `assistant-${interaction.requestId}-${responseId}`,
1183
- idempotencyKey: `assistant-${interaction.requestId}-${responseId}`,
1184
- runId: interaction.runId,
1185
- expectedRevision: interaction.revision,
1160
+ if (responseId !== settledResponseEntryId) return;
1161
+ const response = await client.request({
1162
+ operation: "interaction.assistant",
1163
+ requestId: `assistant-${message.sourceId}-${responseId}`,
1164
+ idempotencyKey: `assistant-${message.sourceId}-${responseId}`,
1186
1165
  payload: {
1187
- requestId: interaction.requestId,
1166
+ ...sessionCommandPayload(ctx),
1167
+ requestId: message.sourceId,
1188
1168
  submissionId: `assistant-${responseId}`,
1189
- step: contract.nodeId,
1190
- attempt: contract.attemptId,
1191
1169
  value: submission as unknown as JsonValue,
1192
1170
  },
1193
1171
  });
1194
- }
1195
-
1196
- function pendingInteractionForSession(sessionId: string): ClientInteractiveRequest | undefined {
1197
- return sessionSnapshots
1198
- .get(sessionId)
1199
- ?.pendingInteractions.map(parseInteractiveRequest)
1200
- .find((request): request is ClientInteractiveRequest => request !== undefined);
1201
- }
1202
-
1203
- function pendingDecision(sessionId: string, runId?: string): ClientInteractiveRequest | undefined {
1204
- return sessionSnapshots
1205
- .get(sessionId)
1206
- ?.pendingInteractions.map(parseInteractiveRequest)
1207
- .find(
1208
- (request): request is ClientInteractiveRequest =>
1209
- request !== undefined &&
1210
- request.kind === "decision" &&
1211
- (runId === undefined || request.runId === runId),
1212
- );
1172
+ if (response.outcome !== "accepted" && response.outcome !== "adopted") {
1173
+ ctx.ui.notify(`Workflow response was rejected: ${response.error ?? response.outcome}`, "error");
1174
+ }
1213
1175
  }
1214
1176
 
1215
1177
  function workflowRunPaused(runId: string): boolean {
@@ -1240,25 +1202,6 @@ function sessionCommandPayload(ctx: ExtensionContext): {
1240
1202
  return { targetSessionId, coordinatorEpoch: session.coordinatorEpoch };
1241
1203
  }
1242
1204
 
1243
- function terminalMessageForSession(
1244
- ctx: ExtensionContext,
1245
- runId: string,
1246
- ): { workflowMessageId: string; workflowTurnId?: string } {
1247
- const session = sessionSnapshots.get(ctx.sessionManager.getSessionId());
1248
- const message = session?.workflowMessages
1249
- .filter((item) => item.kind === "terminal" && item.runId === runId && item.status === "sent")
1250
- .sort((left, right) => right.order - left.order)[0];
1251
- if (message === undefined)
1252
- throw new Error(`Workflow run ${runId} has no current terminal result`);
1253
- const turn = session?.openWorkflowTurn;
1254
- return {
1255
- workflowMessageId: message.workflowMessageId,
1256
- ...(turn?.workflowMessageId === message.workflowMessageId
1257
- ? { workflowTurnId: turn.workflowTurnId }
1258
- : {}),
1259
- };
1260
- }
1261
-
1262
1205
  function sessionRun(ctx: ExtensionContext, runId?: string): WorkflowRunQueueView | undefined {
1263
1206
  const session = sessionSnapshots.get(ctx.sessionManager.getSessionId());
1264
1207
  if (session?.run === null || session?.run === undefined || !isRecord(session.run.queue)) {
@@ -1288,7 +1231,7 @@ function toolInputToCommand(params: ReturnType<typeof parseWorkflowToolInput>):
1288
1231
  case "start":
1289
1232
  return { kind: "run", ref: params.workflow, input: params.input ?? {} };
1290
1233
  case "restart":
1291
- return { kind: "restart", runId: params.runId };
1234
+ return { kind: "restart", runId: params.runId, expectedRevision: params.expectedRevision };
1292
1235
  case "change-settings":
1293
1236
  return {
1294
1237
  kind: "change-settings",
@@ -1324,8 +1267,8 @@ function toolInputToCommand(params: ReturnType<typeof parseWorkflowToolInput>):
1324
1267
  case "answer":
1325
1268
  return {
1326
1269
  kind: "answer",
1327
- input: params.input ?? {},
1328
- ...(params.runId === undefined ? {} : { runId: params.runId }),
1270
+ requestId: params.requestId,
1271
+ input: params.input,
1329
1272
  };
1330
1273
  case "update":
1331
1274
  case "submit":
@@ -1348,31 +1291,13 @@ async function requestAccepted(
1348
1291
  return response;
1349
1292
  }
1350
1293
 
1351
- function interactionContract(interaction: ClientInteractiveRequest): Record<string, unknown> {
1352
- if (!isRecord(interaction.contract)) throw new Error("Stored interaction contract is invalid");
1353
- return interaction.contract;
1354
- }
1355
-
1356
- function agentContract(interaction: ClientInteractiveRequest): AgentStepContract | undefined {
1357
- const value = interactionContract(interaction).contract;
1358
- if (
1359
- !isRecord(value) ||
1360
- typeof value.runId !== "string" ||
1361
- typeof value.workflowName !== "string" ||
1362
- typeof value.nodeId !== "string" ||
1363
- typeof value.attemptId !== "string" ||
1364
- (value.completion !== "submit" && value.completion !== "assistant")
1365
- ) {
1366
- return undefined;
1367
- }
1368
- return value as unknown as AgentStepContract;
1369
- }
1370
-
1371
1294
  function agentContractForWorkflowMessage(message: WorkflowMessage): AgentStepContract | undefined {
1372
1295
  if (message.kind !== "step" || !isRecord(message.content.details)) return undefined;
1373
1296
  const value = message.content.details.contract;
1374
1297
  if (
1375
1298
  !isRecord(value) ||
1299
+ value.requestId !== message.sourceId ||
1300
+ value.runId !== message.runId ||
1376
1301
  typeof value.runId !== "string" ||
1377
1302
  typeof value.workflowName !== "string" ||
1378
1303
  typeof value.nodeId !== "string" ||
@@ -1484,7 +1409,10 @@ function parseInteractiveRequest(value: unknown): ClientInteractiveRequest | und
1484
1409
  typeof value.runId !== "string" ||
1485
1410
  typeof value.targetSessionId !== "string" ||
1486
1411
  typeof value.revision !== "number" ||
1487
- (value.kind !== "agent" && value.kind !== "assistant" && value.kind !== "decision")
1412
+ (value.kind !== "agent" &&
1413
+ value.kind !== "assistant" &&
1414
+ value.kind !== "checkpoint" &&
1415
+ value.kind !== "decision")
1488
1416
  ) {
1489
1417
  return undefined;
1490
1418
  }
@@ -190,8 +190,8 @@ export class SessionRecorder {
190
190
  }
191
191
 
192
192
  /** Record a workflow-owned turn that is not tied to one engine node attempt. */
193
- beginWorkflowMessage(workflowMessageId: string, kind: "terminal" | "followUp"): void {
194
- this.currentAttempt = { nodeId: kind, attemptId: workflowMessageId };
193
+ beginFollowUp(workflowMessageId: string): void {
194
+ this.currentAttempt = { nodeId: "followUp", attemptId: workflowMessageId };
195
195
  }
196
196
 
197
197
  /** Release ownership of the turn that Pi has fully settled. */
@@ -19,7 +19,7 @@ import {
19
19
  export const WORKFLOW_AGENT_STEP_MESSAGE_TYPE = WORKFLOW_STEP_MESSAGE_TYPE;
20
20
  export const WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA = "pi-workflows.agent-step-message.v1";
21
21
 
22
- type PromptDeliveryReason = "initial" | "reminder" | "resumed";
22
+ type PromptDeliveryReason = "initial" | "resumed";
23
23
 
24
24
  export type WorkflowAgentStepMessageDetails = {
25
25
  schema: typeof WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA;
@@ -152,6 +152,7 @@ export function recoverAssistantStep(
152
152
 
153
153
  function sameAttempt(left: AgentStepContract, right: AgentStepContract): boolean {
154
154
  return (
155
+ left.requestId === right.requestId &&
155
156
  left.runId === right.runId &&
156
157
  left.workflowName === right.workflowName &&
157
158
  left.nodeId === right.nodeId &&
@@ -225,9 +226,7 @@ function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefin
225
226
  if (candidate.schema !== WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA) return undefined;
226
227
  if (
227
228
  candidate.kind !== "step" ||
228
- (candidate.reason !== "initial" &&
229
- candidate.reason !== "reminder" &&
230
- candidate.reason !== "resumed") ||
229
+ (candidate.reason !== "initial" && candidate.reason !== "resumed") ||
231
230
  typeof candidate.requestId !== "string" ||
232
231
  typeof candidate.workflowMessageId !== "string"
233
232
  ) {
@@ -237,6 +236,7 @@ function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefin
237
236
  if (
238
237
  contract === null ||
239
238
  typeof contract !== "object" ||
239
+ typeof contract.requestId !== "string" ||
240
240
  typeof contract.runId !== "string" ||
241
241
  typeof contract.workflowName !== "string" ||
242
242
  typeof contract.nodeId !== "string" ||