@osolmaz/pi-workflows 0.16.5 → 0.16.7

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 (181) 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 +116 -166
  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/widget.js +1 -1
  21. package/dist/extension/widget.js.map +1 -1
  22. package/dist/extension/workflow-message-coordinator.d.ts +12 -3
  23. package/dist/extension/workflow-message-coordinator.js +108 -94
  24. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  25. package/dist/render/graph-render.js +1 -1
  26. package/dist/resource-managers/index.d.ts +1 -2
  27. package/dist/resource-managers/index.js +1 -2
  28. package/dist/resource-managers/index.js.map +1 -1
  29. package/dist/resource-managers/sqlite.d.ts +2 -295
  30. package/dist/resource-managers/sqlite.js +71 -1528
  31. package/dist/resource-managers/sqlite.js.map +1 -1
  32. package/dist/resource-managers/types.d.ts +2 -7
  33. package/dist/server/resource-runner-entry.js +2 -6
  34. package/dist/server/resource-runner-entry.js.map +1 -1
  35. package/dist/server/rpc-bridge.js +1 -1
  36. package/dist/server/rpc-bridge.js.map +1 -1
  37. package/dist/server/rpc-executor.js +2 -4
  38. package/dist/server/rpc-executor.js.map +1 -1
  39. package/dist/server/server-entry.js +3 -0
  40. package/dist/server/server-entry.js.map +1 -1
  41. package/dist/server/server.d.ts +9 -8
  42. package/dist/server/server.js +364 -541
  43. package/dist/server/server.js.map +1 -1
  44. package/dist/server/state.d.ts +16 -29
  45. package/dist/server/state.js +105 -319
  46. package/dist/server/state.js.map +1 -1
  47. package/dist/server/view.d.ts +6 -5
  48. package/dist/server/view.js +68 -51
  49. package/dist/server/view.js.map +1 -1
  50. package/dist/server/workflow-runner-entry.d.ts +1 -1
  51. package/dist/server/workflow-runner-entry.js +1 -49
  52. package/dist/server/workflow-runner-entry.js.map +1 -1
  53. package/dist/server/workflow-runner-protocol.d.ts +2 -7
  54. package/dist/server/workflow-runner-protocol.js +5 -10
  55. package/dist/server/workflow-runner-protocol.js.map +1 -1
  56. package/dist/server/workflow-runner-store.d.ts +5 -6
  57. package/dist/server/workflow-runner-store.js +7 -14
  58. package/dist/server/workflow-runner-store.js.map +1 -1
  59. package/dist/state/attempt-time.d.ts +21 -0
  60. package/dist/state/attempt-time.js +95 -0
  61. package/dist/state/attempt-time.js.map +1 -0
  62. package/dist/state/json.d.ts +2 -1
  63. package/dist/state/json.js.map +1 -1
  64. package/dist/state/project-store.d.ts +39 -0
  65. package/dist/state/project-store.js +154 -0
  66. package/dist/state/project-store.js.map +1 -0
  67. package/dist/state/prune.js +0 -7
  68. package/dist/state/prune.js.map +1 -1
  69. package/dist/state/schema.js +23 -23
  70. package/dist/state/workflow-messages.d.ts +1 -1
  71. package/dist/state/workflow-messages.js +3 -1
  72. package/dist/state/workflow-messages.js.map +1 -1
  73. package/dist/viewer/backup.js +5 -0
  74. package/dist/viewer/backup.js.map +1 -1
  75. package/dist/workflows/diagnostics.d.ts +8 -0
  76. package/dist/workflows/diagnostics.js +61 -0
  77. package/dist/workflows/diagnostics.js.map +1 -0
  78. package/dist/workflows/engine.d.ts +2 -16
  79. package/dist/workflows/engine.js +128 -252
  80. package/dist/workflows/engine.js.map +1 -1
  81. package/dist/workflows/errors.d.ts +1 -1
  82. package/dist/workflows/errors.js +2 -2
  83. package/dist/workflows/errors.js.map +1 -1
  84. package/dist/workflows/human-decision.d.ts +3 -7
  85. package/dist/workflows/human-decision.js +2 -56
  86. package/dist/workflows/human-decision.js.map +1 -1
  87. package/dist/workflows/index.d.ts +2 -1
  88. package/dist/workflows/index.js +1 -0
  89. package/dist/workflows/index.js.map +1 -1
  90. package/dist/workflows/queue.d.ts +267 -0
  91. package/dist/workflows/queue.js +1248 -0
  92. package/dist/workflows/queue.js.map +1 -0
  93. package/dist/workflows/requests.d.ts +47 -0
  94. package/dist/workflows/requests.js +158 -0
  95. package/dist/workflows/requests.js.map +1 -0
  96. package/dist/workflows/schema.js +2 -4
  97. package/dist/workflows/schema.js.map +1 -1
  98. package/dist/workflows/store.d.ts +11 -16
  99. package/dist/workflows/store.js +177 -174
  100. package/dist/workflows/store.js.map +1 -1
  101. package/dist/workflows/tool-input.d.ts +6 -9
  102. package/dist/workflows/tool-input.js +15 -12
  103. package/dist/workflows/tool-input.js.map +1 -1
  104. package/dist/workflows/transitions.d.ts +38 -0
  105. package/dist/workflows/transitions.js +276 -0
  106. package/dist/workflows/transitions.js.map +1 -0
  107. package/dist/workflows/types.d.ts +9 -42
  108. package/dist/workflows/workflow-message-content.d.ts +7 -1
  109. package/dist/workflows/workflow-message-content.js +22 -4
  110. package/dist/workflows/workflow-message-content.js.map +1 -1
  111. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  112. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  113. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  114. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  115. package/docs/2026-09-07-workflow-handoff-plan.md +84 -0
  116. package/docs/DEFERRED_TURNS.md +84 -105
  117. package/docs/HUMAN_DECISIONS.md +17 -19
  118. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  119. package/docs/SQLITE_STATE.md +68 -29
  120. package/docs/WORKFLOW_SERVER.md +21 -10
  121. package/docs/WORKFLOW_STEP_MESSAGES.md +83 -54
  122. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  123. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  124. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  125. package/docs/workflows.md +111 -45
  126. package/examples/workflows/autoresearch.workflow.ts +0 -2
  127. package/examples/workflows/branch.workflow.ts +0 -4
  128. package/examples/workflows/echo.workflow.ts +0 -2
  129. package/examples/workflows/live-settings.workflow.ts +0 -2
  130. package/examples/workflows/two-turn.workflow.ts +0 -2
  131. package/herdr-plugin.toml +1 -1
  132. package/package.json +1 -1
  133. package/protocol/fixtures/client-v1.json +1 -1
  134. package/skills/pi-workflows/SKILL.md +8 -7
  135. package/src/builtins/autoimplement.workflow.ts +31 -10
  136. package/src/client/client.ts +3 -1
  137. package/src/client/protocol.ts +21 -1
  138. package/src/client/view.ts +9 -5
  139. package/src/extension/index.ts +118 -190
  140. package/src/extension/recorder.ts +2 -2
  141. package/src/extension/step-message.ts +4 -4
  142. package/src/extension/widget.ts +1 -1
  143. package/src/extension/workflow-message-coordinator.ts +133 -95
  144. package/src/render/graph-render.ts +1 -1
  145. package/src/resource-managers/index.ts +1 -9
  146. package/src/resource-managers/sqlite.ts +330 -2655
  147. package/src/resource-managers/types.ts +2 -3
  148. package/src/server/resource-runner-entry.ts +2 -7
  149. package/src/server/rpc-bridge.ts +1 -1
  150. package/src/server/rpc-executor.ts +4 -9
  151. package/src/server/server-entry.ts +3 -0
  152. package/src/server/server.ts +416 -641
  153. package/src/server/state.ts +122 -466
  154. package/src/server/view.ts +73 -58
  155. package/src/server/workflow-runner-entry.ts +2 -55
  156. package/src/server/workflow-runner-protocol.ts +5 -15
  157. package/src/server/workflow-runner-store.ts +12 -27
  158. package/src/state/attempt-time.ts +122 -0
  159. package/src/state/json.ts +2 -1
  160. package/src/state/project-store.ts +218 -0
  161. package/src/state/prune.ts +0 -13
  162. package/src/state/schema.ts +23 -23
  163. package/src/state/workflow-messages.ts +4 -2
  164. package/src/viewer/backup.ts +7 -0
  165. package/src/workflows/diagnostics.ts +78 -0
  166. package/src/workflows/engine.ts +136 -299
  167. package/src/workflows/errors.ts +4 -2
  168. package/src/workflows/human-decision.ts +3 -81
  169. package/src/workflows/index.ts +8 -2
  170. package/src/workflows/queue.ts +2091 -0
  171. package/src/workflows/requests.ts +283 -0
  172. package/src/workflows/schema.ts +2 -6
  173. package/src/workflows/store.ts +226 -236
  174. package/src/workflows/tool-input.ts +20 -12
  175. package/src/workflows/transitions.ts +330 -0
  176. package/src/workflows/types.ts +9 -46
  177. package/src/workflows/workflow-message-content.ts +28 -4
  178. package/dist/resource-managers/runtime.d.ts +0 -59
  179. package/dist/resource-managers/runtime.js +0 -423
  180. package/dist/resource-managers/runtime.js.map +0 -1
  181. package/src/resource-managers/runtime.ts +0 -576
@@ -5,12 +5,14 @@ import { StaleResourceError } from "../state/mutation.js";
5
5
  import { compileWorkflowDefinition, compositionMetadata, isCompiledWorkflow, } from "./composition.js";
6
6
  import { CancelledError, errorMessage, isAbortLikeError, isClaimLostError, isRunParkedError, RunParkedError, TimeoutError, WorkflowSourceChangedError, } from "./errors.js";
7
7
  import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
8
- import { createHumanDecisionRequest, validateHumanDecisionResponse } from "./human-decision.js";
8
+ import { createHumanDecisionRequest } from "./human-decision.js";
9
9
  import { extractJsonValue } from "./json.js";
10
+ import { workflowRequestId } from "./requests.js";
10
11
  import { parseWorkflowSettingsValue, resolveInitialWorkflowSettings, } from "./settings.js";
11
12
  import { runShellAction, shellResultFromError } from "./shell.js";
12
13
  import { RUN_STATE_SCHEMA, WorkflowRunStore, createDefinitionSnapshot, createRunId, } from "./store.js";
13
- import { UpdateRateLimiter, updateReceipt, validateWorkflowUpdate } from "./updates.js";
14
+ import { executionTransition } from "./transitions.js";
15
+ import { UpdateRateLimiter, updateProjection, updateReceipt, validateWorkflowUpdate, } from "./updates.js";
14
16
  const DEFAULT_NODE_TIMEOUT_MS = 15 * 60_000;
15
17
  const DEFAULT_MAX_STEPS = 100;
16
18
  const TITLE_TIMEOUT_MS = 30_000;
@@ -32,7 +34,6 @@ export class WorkflowEngine {
32
34
  maxSteps;
33
35
  onEvent;
34
36
  onRunStarted;
35
- onRunFinishing;
36
37
  activeAbort = null;
37
38
  activeAttempt;
38
39
  updateLimiters = new Map();
@@ -40,7 +41,6 @@ export class WorkflowEngine {
40
41
  cancelled = false;
41
42
  parked = false;
42
43
  paused = false;
43
- presentationRequired = false;
44
44
  wakePause = null;
45
45
  constructor(options) {
46
46
  this.executor = options.executor;
@@ -50,7 +50,6 @@ export class WorkflowEngine {
50
50
  this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
51
51
  this.onEvent = options.onEvent;
52
52
  this.onRunStarted = options.onRunStarted;
53
- this.onRunFinishing = options.onRunFinishing;
54
53
  }
55
54
  get databasePath() {
56
55
  return this.store.databasePath;
@@ -80,7 +79,10 @@ export class WorkflowEngine {
80
79
  this.updateLimiters.set(active.state.runId, limiter);
81
80
  }
82
81
  limiter.take();
83
- const { event, record } = await this.store.publishUpdate(active.runId, active.state, active.nodeId, active.attemptId, update, { signal: active.signal });
82
+ const { event, record } = await this.store.publishUpdate(active.runId, active.nodeId, active.attemptId, update, { signal: active.signal });
83
+ active.state.updates = updateProjection(active.state.updates, record);
84
+ active.state.traceSeq = event.seq;
85
+ active.state.updatedAt = event.at;
84
86
  try {
85
87
  this.onEvent?.(event, active.state);
86
88
  }
@@ -133,7 +135,6 @@ export class WorkflowEngine {
133
135
  async run(workflow, input, options = {}) {
134
136
  workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
135
137
  validateWorkflowDefinition(workflow);
136
- this.presentationRequired = workflow.presentationPrompt !== undefined;
137
138
  // Fail before any run row exists so bad input cannot leave partial state.
138
139
  const suppliedInput = input === undefined ? null : input;
139
140
  const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
@@ -191,34 +192,37 @@ export class WorkflowEngine {
191
192
  async resumeRun(workflow, runId, options = {}) {
192
193
  workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
193
194
  validateWorkflowDefinition(workflow);
194
- this.presentationRequired = workflow.presentationPrompt !== undefined;
195
195
  // Reset before any await: a park or cancel landing during preparation
196
196
  // must survive, or a server drain would hang while the run executes.
197
197
  this.cancelled = false;
198
198
  this.paused = false;
199
199
  this.parked = false;
200
- const state = await this.store.prepareRunResume(runId);
201
- const sourceMismatch = workflowIdentityMismatch(state, workflow, options.workflowSource);
202
- if (sourceMismatch && options.force !== true) {
200
+ const stored = await this.store.readRunState(runId);
201
+ if (stored === null)
202
+ throw new Error(`Cannot resume unreadable workflow run: ${runId}`);
203
+ const sourceMismatch = workflowIdentityMismatch(stored, workflow, options.workflowSource);
204
+ if (sourceMismatch) {
203
205
  throw new WorkflowSourceChangedError(runId);
204
206
  }
205
- const point = this.resumePointFor(workflow, state, "wait");
207
+ const state = await this.store.prepareRunResume(runId);
208
+ const point = this.resumePointFor(workflow, state);
206
209
  const interruptedNode = state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
207
210
  let resumedAttempt;
208
211
  if (state.currentNode !== undefined &&
209
212
  state.currentAttemptId !== undefined &&
210
213
  state.currentNodeStartedAt !== undefined &&
211
- interruptedNode?.nodeType === "agent" &&
212
- (assistantMessageConfig(interruptedNode) !== undefined ||
213
- options.resumeInteractionAttemptId === state.currentAttemptId)) {
214
+ (interruptedNode?.nodeType === "checkpoint" ||
215
+ (interruptedNode?.nodeType === "agent" &&
216
+ (assistantMessageConfig(interruptedNode) !== undefined ||
217
+ options.resumeInteractionAttemptId === state.currentAttemptId)))) {
214
218
  const resumedSettings = await this.persistedSettingsBinding(state);
215
219
  resumedAttempt = {
216
220
  nodeId: state.currentNode,
217
221
  attemptId: state.currentAttemptId,
218
222
  startedAt: state.currentNodeStartedAt,
219
- ...(state.currentNodeDeadlineAt === undefined
223
+ ...(state.currentNodeTimeoutMs === undefined
220
224
  ? {}
221
- : { deadlineAt: state.currentNodeDeadlineAt }),
225
+ : { timeoutMs: state.currentNodeTimeoutMs, elapsedMs: state.currentNodeElapsedMs ?? 0 }),
222
226
  ...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
223
227
  };
224
228
  }
@@ -231,7 +235,8 @@ export class WorkflowEngine {
231
235
  delete state.currentNode;
232
236
  delete state.currentAttemptId;
233
237
  delete state.currentNodeStartedAt;
234
- delete state.currentNodeDeadlineAt;
238
+ delete state.currentNodeTimeoutMs;
239
+ delete state.currentNodeElapsedMs;
235
240
  delete state.currentSettingsScopeId;
236
241
  delete state.currentSettingsChangeNumber;
237
242
  delete state.currentSettingsHash;
@@ -244,21 +249,12 @@ export class WorkflowEngine {
244
249
  ...(point.nodeId !== null ? { resumeAt: point.nodeId } : {}),
245
250
  ...(resumedAttempt !== undefined ? { resumedAttemptId: resumedAttempt.attemptId } : {}),
246
251
  replayedSteps: state.steps.length,
247
- ...(sourceMismatch ? { workflowSourceMismatch: true, forced: true } : {}),
248
252
  },
249
253
  });
250
254
  await this.onRunStarted?.(runId, state);
251
255
  if (point.nodeId === null) {
252
- // The last recorded transition already finished the graph; the crash
253
- // happened before the terminal event was written. A finished
254
- // checkpoint restores its waiting gate rather than completing.
255
- if (point.waitingOn !== undefined) {
256
- await this.finishRun(runId, state, "waiting", {
257
- waitingOn: point.waitingOn,
258
- finalOutput: point.lastOutput,
259
- });
260
- }
261
- else if (point.failedResult === undefined) {
256
+ // The last attempt finished before the terminal event was committed.
257
+ if (point.failedResult === undefined) {
262
258
  await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
263
259
  }
264
260
  else {
@@ -283,138 +279,12 @@ export class WorkflowEngine {
283
279
  }
284
280
  return { runId, state };
285
281
  }
286
- /**
287
- * Start a continuation run from a checkpointed parent. The new run gets a
288
- * fresh run and event stream, carries forward the parent's outputs, results,
289
- * and step accounting, and continues routing after the checkpoint.
290
- */
291
- async continueRun(workflow, parentRunId, input, options = {}) {
292
- workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
293
- validateWorkflowDefinition(workflow);
294
- this.presentationRequired = workflow.presentationPrompt !== undefined;
295
- this.cancelled = false;
296
- this.paused = false;
297
- this.parked = false;
298
- const parentState = await this.store.readRunState(parentRunId);
299
- if (parentState === null) {
300
- throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
301
- }
302
- if (parentState.status !== "waiting" || parentState.waitingOn === undefined) {
303
- throw new Error(`Cannot continue workflow run ${parentRunId} with status ${parentState.status}`);
304
- }
305
- const sourceMismatch = workflowIdentityMismatch(parentState, workflow, options.workflowSource);
306
- if (sourceMismatch && options.force !== true) {
307
- throw new WorkflowSourceChangedError(parentRunId);
308
- }
309
- const waitingNodeId = parentState.waitingOn;
310
- const waitingNode = workflow.nodes[waitingNodeId];
311
- const humanContract = waitingNode?.nodeType === "checkpoint" ? waitingNode.humanDecision : undefined;
312
- let acceptedResponse;
313
- let acceptedNodeId;
314
- let normalizedInput;
315
- if (humanContract !== undefined) {
316
- if (options.humanDecision === undefined) {
317
- throw new Error(`Checkpoint ${waitingNodeId} requires an accepted verified human decision`);
318
- }
319
- const request = parentState.finalOutput;
320
- if (request?.schema !== "pi-workflows.human-decision-request.v1" ||
321
- request.decisionId !== options.humanDecision.decisionId ||
322
- request.requestDigest !== options.humanDecision.requestDigest) {
323
- throw new Error("Accepted human decision does not match the waiting request");
324
- }
325
- const durableDecision = await this.store.readResolvedHumanDecision(request.decisionId);
326
- if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
327
- throw new Error("Accepted human decision does not match the durable decision record");
328
- }
329
- acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
330
- acceptedNodeId = request.nodeId;
331
- normalizedInput = structuredClone(parentState.input);
332
- }
333
- else {
334
- const suppliedInput = input === undefined ? null : input;
335
- normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
336
- }
337
- assertJsonSerializable(normalizedInput, "Workflow run input");
338
- if (options.runId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(options.runId)) {
339
- throw new Error(`Invalid workflow run id: ${JSON.stringify(options.runId)}`);
340
- }
341
- const state = await this.createRunState(workflow, normalizedInput, options.workflowSource, options.runId);
342
- state.parentRunId = parentRunId;
343
- state.outputs = structuredClone(parentState.outputs);
344
- state.results = structuredClone(parentState.results);
345
- state.steps = structuredClone(parentState.steps);
346
- if (humanContract !== undefined && options.humanDecision !== undefined) {
347
- const receipt = {
348
- decisionId: options.humanDecision.decisionId,
349
- requestDigest: options.humanDecision.requestDigest,
350
- nodeId: acceptedNodeId ?? waitingNodeId,
351
- response: options.humanDecision.response,
352
- provenance: options.humanDecision.provenance,
353
- acceptedAt: options.humanDecision.acceptedAt,
354
- answerDigest: options.humanDecision.answerDigest,
355
- };
356
- state.humanDecision = {
357
- schema: "pi-workflows.human-decision-receipt.v1",
358
- ...receipt,
359
- subjectDigest: options.humanDecision.subjectDigest,
360
- presentationDigest: options.humanDecision.presentationDigest,
361
- revision: options.humanDecision.revision,
362
- };
363
- state.outputs[waitingNodeId] = acceptedResponse;
364
- const priorResult = state.results[waitingNodeId];
365
- if (priorResult === undefined) {
366
- throw new Error(`Waiting human decision result is missing for ${waitingNodeId}`);
367
- }
368
- state.results[waitingNodeId] = { ...priorResult, output: acceptedResponse };
369
- const stepIndex = state.steps.findLastIndex((step) => step.nodeId === waitingNodeId);
370
- if (stepIndex < 0)
371
- throw new Error(`Waiting human decision step is missing for ${waitingNodeId}`);
372
- const priorStep = state.steps[stepIndex];
373
- if (priorStep === undefined)
374
- throw new Error("Waiting human decision step became unavailable");
375
- state.steps[stepIndex] = { ...priorStep, output: acceptedResponse };
376
- }
377
- state.carriedStepCount = state.steps.length;
378
- const runId = await this.store.initializeRun(workflow, state);
379
- await this.persist(runId, state, {
380
- scope: "run",
381
- type: "run_started",
382
- payload: {
383
- workflowName: workflow.name,
384
- ...(state.runTitle ? { runTitle: state.runTitle } : {}),
385
- input: state.input,
386
- continuedFrom: parentRunId,
387
- checkpoint: parentState.waitingOn,
388
- carriedSteps: state.steps.length,
389
- },
390
- });
391
- await this.onRunStarted?.(runId, state);
392
- const point = this.resumePointFor(workflow, state, "continue");
393
- if (point.nodeId === null) {
394
- // The checkpoint was the final node; the answer completes the chain.
395
- await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
396
- return { runId, state };
397
- }
398
- try {
399
- await this.executeGraph(workflow, state, runId, point.nodeId, countExecutableSteps(workflow, state.steps), point.lastOutput);
400
- }
401
- catch (error) {
402
- if (isClaimLostError(error))
403
- throw error;
404
- if (isRunParkedError(error) || this.parked) {
405
- return { runId, state };
406
- }
407
- await this.finishAfterError(runId, state, error);
408
- return { runId, state };
409
- }
410
- return { runId, state };
411
- }
412
282
  /**
413
283
  * Find where a resumed run continues. An in-flight node reruns; otherwise
414
284
  * routing continues from the last recorded step. A null nodeId means the
415
285
  * graph was already done when the crash hit.
416
286
  */
417
- resumePointFor(workflow, state, checkpointBehavior) {
287
+ resumePointFor(workflow, state) {
418
288
  if (state.currentNode !== undefined) {
419
289
  if (workflow.nodes[state.currentNode] === undefined) {
420
290
  throw new Error(`Resume node is missing from the workflow: ${state.currentNode}`);
@@ -430,17 +300,6 @@ export class WorkflowEngine {
430
300
  return { nodeId: lastStep.nodeId };
431
301
  }
432
302
  if (result.outcome === "ok") {
433
- // A recorded checkpoint means the run should be waiting; a crash
434
- // before the run_waiting persist restores the gate instead of
435
- // routing past it. The gate applies to this run's own checkpoint
436
- // only: a continuation's carried steps end with the parent's
437
- // already-answered checkpoint, and routing must continue from it.
438
- const isCarriedStep = state.steps.length <= (state.carriedStepCount ?? 0);
439
- if (checkpointBehavior === "wait" &&
440
- !isCarriedStep &&
441
- workflow.nodes[lastStep.nodeId]?.nodeType === "checkpoint") {
442
- return { nodeId: null, waitingOn: lastStep.nodeId, lastOutput: result.output };
443
- }
444
303
  const next = resolveNext(workflow.edges, lastStep.nodeId, result.output, result);
445
304
  return next === null
446
305
  ? { nodeId: null, lastOutput: result.output }
@@ -496,9 +355,7 @@ export class WorkflowEngine {
496
355
  ...(await this.resolveTitleBounded(workflow, input)),
497
356
  ...(workflowSource !== undefined ? { workflowSource } : {}),
498
357
  ...(composition?.sources.length ? { workflowSources: composition.sources } : {}),
499
- ...(composition?.snapshot.mounts.length
500
- ? { definitionDigest: definitionDigest(workflow) }
501
- : {}),
358
+ definitionDigest: definitionDigest(workflow),
502
359
  startedAt: now,
503
360
  updatedAt: now,
504
361
  status: "running",
@@ -602,6 +459,7 @@ export class WorkflowEngine {
602
459
  scopeId: latest.scopeId,
603
460
  previousChangeNumber: attempt.settings.changeNumber,
604
461
  changeNumber: latest.changeNumber,
462
+ settingsHash: latest.settingsHash,
605
463
  },
606
464
  });
607
465
  resumedAttempt = {
@@ -647,13 +505,6 @@ export class WorkflowEngine {
647
505
  });
648
506
  }
649
507
  lastOutput = attempt.result.output;
650
- if (node.nodeType === "checkpoint") {
651
- await this.finishRun(runId, state, "waiting", {
652
- waitingOn: attempt.result.nodeId,
653
- finalOutput: lastOutput,
654
- });
655
- return;
656
- }
657
508
  currentNodeId = resolveNext(workflow.edges, attempt.result.nodeId, attempt.result.output, attempt.result);
658
509
  }
659
510
  await this.finishRun(runId, state, "completed", { finalOutput: lastOutput });
@@ -749,7 +600,8 @@ export class WorkflowEngine {
749
600
  delete state.currentNode;
750
601
  delete state.currentAttemptId;
751
602
  delete state.currentNodeStartedAt;
752
- delete state.currentNodeDeadlineAt;
603
+ delete state.currentNodeTimeoutMs;
604
+ delete state.currentNodeElapsedMs;
753
605
  delete state.currentSettingsScopeId;
754
606
  delete state.currentSettingsChangeNumber;
755
607
  delete state.currentSettingsHash;
@@ -795,7 +647,7 @@ export class WorkflowEngine {
795
647
  }
796
648
  const meta = { promptText: null };
797
649
  try {
798
- const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, resumedAttempt?.deadlineAt);
650
+ const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, resumedAttempt?.timeoutMs, resumedAttempt?.elapsedMs);
799
651
  return {
800
652
  result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
801
653
  execution,
@@ -845,7 +697,7 @@ export class WorkflowEngine {
845
697
  ...(output !== undefined ? { output } : {}),
846
698
  };
847
699
  }
848
- async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, persistedDeadlineAt) {
700
+ async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, persistedTimeoutMs, elapsedMs = 0) {
849
701
  const abort = new AbortController();
850
702
  const context = this.createNodeContext(state, abort.signal, settings);
851
703
  let timer;
@@ -862,43 +714,41 @@ export class WorkflowEngine {
862
714
  }
863
715
  let timeoutMs;
864
716
  let timeoutErrorMs;
865
- if (persistedDeadlineAt === undefined) {
717
+ if (persistedTimeoutMs === undefined) {
866
718
  timeoutMs = await this.resolveNodeTimeout(node, context, abort);
867
719
  timeoutErrorMs = timeoutMs;
868
- if (node.nodeType === "agent" && this.executor.preservesDeadlineWhileParked === true) {
869
- state.currentNodeDeadlineAt =
870
- timeoutMs === null ? null : new Date(Date.now() + timeoutMs).toISOString();
720
+ if (node.nodeType === "agent" && this.executor.preservesActiveTimeBudget === true) {
721
+ state.currentNodeTimeoutMs = timeoutMs;
722
+ state.currentNodeElapsedMs = 0;
871
723
  await this.persist(runId, state, {
872
724
  scope: "node",
873
- type: "node_deadline_set",
725
+ type: "node_timeout_set",
874
726
  nodeId,
875
727
  attemptId,
876
- payload: { deadlineAt: state.currentNodeDeadlineAt },
728
+ payload: { timeoutMs },
877
729
  });
878
- if (state.currentNodeDeadlineAt !== null && timeoutErrorMs !== null) {
879
- timeoutMs = Date.parse(state.currentNodeDeadlineAt) - Date.now();
880
- if (timeoutMs <= 0)
881
- throw new TimeoutError(timeoutErrorMs);
882
- }
883
730
  }
884
731
  else {
885
- delete state.currentNodeDeadlineAt;
732
+ delete state.currentNodeTimeoutMs;
733
+ delete state.currentNodeElapsedMs;
886
734
  }
887
735
  }
888
- else if (persistedDeadlineAt === null) {
889
- state.currentNodeDeadlineAt = null;
736
+ else if (persistedTimeoutMs === null) {
737
+ state.currentNodeTimeoutMs = null;
890
738
  timeoutMs = null;
891
739
  timeoutErrorMs = null;
892
740
  }
893
741
  else {
894
- const deadline = Date.parse(persistedDeadlineAt);
895
- const started = Date.parse(state.currentNodeStartedAt ?? persistedDeadlineAt);
896
- if (!Number.isFinite(deadline) || !Number.isFinite(started)) {
897
- throw new Error("Persisted node deadline is invalid");
742
+ if (!Number.isFinite(persistedTimeoutMs) ||
743
+ persistedTimeoutMs <= 0 ||
744
+ !Number.isFinite(elapsedMs) ||
745
+ elapsedMs < 0) {
746
+ throw new Error("Persisted node timeout is invalid");
898
747
  }
899
- state.currentNodeDeadlineAt = persistedDeadlineAt;
900
- timeoutMs = deadline - Date.now();
901
- timeoutErrorMs = Math.max(1, deadline - started);
748
+ state.currentNodeTimeoutMs = persistedTimeoutMs;
749
+ state.currentNodeElapsedMs = elapsedMs;
750
+ timeoutMs = persistedTimeoutMs - elapsedMs;
751
+ timeoutErrorMs = persistedTimeoutMs;
902
752
  if (timeoutMs <= 0)
903
753
  throw new TimeoutError(timeoutErrorMs);
904
754
  }
@@ -1010,13 +860,42 @@ export class WorkflowEngine {
1010
860
  }
1011
861
  case "action":
1012
862
  return await this.runActionNode(node, context, nodeId, attemptId, signal, meta);
1013
- case "checkpoint":
1014
- return await runCheckpointNode(node, context, {
1015
- store: this.store,
1016
- workflowName: workflow.name,
1017
- nodeId,
1018
- attemptId,
1019
- });
863
+ case "checkpoint": {
864
+ const saved = await this.store.readCheckpoint(state.runId, attemptId);
865
+ if (saved !== undefined) {
866
+ if (saved.request.status === "cancelled")
867
+ throw new Error("Checkpoint request is cancelled");
868
+ if (saved.request.status === "settled") {
869
+ if (saved.humanDecision !== undefined)
870
+ state.humanDecision = saved.humanDecision;
871
+ return { output: saved.output, promptText: null };
872
+ }
873
+ }
874
+ else {
875
+ const request = await checkpointRequest(node, context, {
876
+ workflowName: workflow.name,
877
+ nodeId,
878
+ attemptId,
879
+ });
880
+ const event = await this.store.commitTransition(state.runId, {
881
+ kind: "waitForInput",
882
+ ...request,
883
+ event: {
884
+ scope: "node",
885
+ type: "checkpoint_requested",
886
+ nodeId,
887
+ attemptId,
888
+ payload: { requestId: request.requestId, requestKind: request.requestKind },
889
+ },
890
+ });
891
+ state.traceSeq = event.seq;
892
+ state.updatedAt = event.at;
893
+ }
894
+ state.status = "waiting";
895
+ state.waitingOn = nodeId;
896
+ delete state.finishedAt;
897
+ throw new RunParkedError();
898
+ }
1020
899
  }
1021
900
  }
1022
901
  async captureSettingsBinding(workflow, state, nodeId) {
@@ -1072,11 +951,12 @@ export class WorkflowEngine {
1072
951
  return scope;
1073
952
  }
1074
953
  createNodeContext(state, signal, settings) {
954
+ const snapshot = deepFreezeJson(structuredClone(state));
1075
955
  return {
1076
- input: state.input,
1077
- outputs: state.outputs,
1078
- results: state.results,
1079
- state,
956
+ input: snapshot.input,
957
+ outputs: snapshot.outputs,
958
+ results: snapshot.results,
959
+ state: snapshot,
1080
960
  ...(settings !== undefined
1081
961
  ? {
1082
962
  settings: deepFreezeJson(structuredClone(settings.settings)),
@@ -1114,7 +994,17 @@ export class WorkflowEngine {
1114
994
  // may already be terminal.
1115
995
  throw abortError(signal);
1116
996
  }
1117
- const prompt = appendStepContract(basePrompt, workflow.name, nodeId, attemptId, node.expectedOutput);
997
+ const contract = {
998
+ requestId: workflowRequestId(state.runId, attemptId),
999
+ runId: state.runId,
1000
+ workflowName: workflow.name,
1001
+ nodeId,
1002
+ attemptId,
1003
+ completion: assistant === undefined ? "submit" : "assistant",
1004
+ ...(typeof node.expectedOutput === "string" ? { expectedOutput: node.expectedOutput } : {}),
1005
+ ...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
1006
+ };
1007
+ const prompt = appendStepContract(basePrompt, contract);
1118
1008
  meta.promptText = prompt;
1119
1009
  await this.persist(runId, state, {
1120
1010
  scope: "agent",
@@ -1124,17 +1014,7 @@ export class WorkflowEngine {
1124
1014
  payload: { prompt, completion: assistant === undefined ? "submit" : "assistant" },
1125
1015
  });
1126
1016
  const submission = await this.executor.runAgentStep({
1127
- contract: {
1128
- runId: state.runId,
1129
- workflowName: workflow.name,
1130
- nodeId,
1131
- attemptId,
1132
- completion: assistant === undefined ? "submit" : "assistant",
1133
- ...(typeof node.expectedOutput === "string"
1134
- ? { expectedOutput: node.expectedOutput }
1135
- : {}),
1136
- ...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
1137
- },
1017
+ contract,
1138
1018
  prompt,
1139
1019
  ...(state.runTitle !== undefined || node.statusDetail !== undefined
1140
1020
  ? {
@@ -1251,7 +1131,9 @@ export class WorkflowEngine {
1251
1131
  }
1252
1132
  }
1253
1133
  async persist(runId, state, event) {
1254
- const traceEvent = await this.store.writeSnapshot(runId, state, event);
1134
+ const traceEvent = await this.store.commitTransition(runId, executionTransition(state, event));
1135
+ state.traceSeq = traceEvent.seq;
1136
+ state.updatedAt = traceEvent.at;
1255
1137
  try {
1256
1138
  this.onEvent?.(traceEvent, state);
1257
1139
  }
@@ -1270,41 +1152,29 @@ export class WorkflowEngine {
1270
1152
  if (fields.error !== undefined) {
1271
1153
  state.error = fields.error;
1272
1154
  }
1273
- if (fields.waitingOn !== undefined) {
1274
- state.waitingOn = fields.waitingOn;
1275
- }
1276
1155
  if (fields.finalOutput !== undefined) {
1277
1156
  state.finalOutput = fields.finalOutput;
1278
1157
  }
1279
1158
  delete state.currentNode;
1280
1159
  delete state.currentAttemptId;
1281
1160
  delete state.currentNodeStartedAt;
1282
- delete state.currentNodeDeadlineAt;
1161
+ delete state.currentNodeTimeoutMs;
1162
+ delete state.currentNodeElapsedMs;
1283
1163
  delete state.currentSettingsScopeId;
1284
1164
  delete state.currentSettingsChangeNumber;
1285
1165
  delete state.currentSettingsHash;
1286
- // Let observers stop, drain, and prepare terminal delivery from the final
1287
- // in-memory state before the immutable terminal event is committed.
1288
- try {
1289
- await this.onRunFinishing?.(runId, state);
1290
- }
1291
- catch {
1292
- // Finishing the run wins over observer failures.
1293
- }
1294
1166
  await this.persist(runId, state, {
1295
1167
  scope: "run",
1296
1168
  type: `run_${status}`,
1297
1169
  payload: {
1298
1170
  status,
1299
1171
  ...(fields.error !== undefined ? { error: fields.error } : {}),
1300
- ...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
1301
1172
  ...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
1302
- presentationRequired: status === "completed" && this.presentationRequired,
1303
1173
  },
1304
1174
  });
1305
1175
  }
1306
1176
  }
1307
- async function runCheckpointNode(node, context, execution) {
1177
+ async function checkpointRequest(node, context, execution) {
1308
1178
  if (node.humanDecision !== undefined) {
1309
1179
  const prompt = await node.humanDecision.request(context);
1310
1180
  const audience = typeof node.humanDecision.audience === "function"
@@ -1322,11 +1192,18 @@ async function runCheckpointNode(node, context, execution) {
1322
1192
  prompt,
1323
1193
  ...(timeout !== undefined ? { timeout } : {}),
1324
1194
  });
1325
- await execution.store.createHumanDecisionRequest(request);
1326
- return { output: request, promptText: null };
1195
+ return {
1196
+ requestId: request.decisionId,
1197
+ requestKind: "decision",
1198
+ contract: request,
1199
+ };
1327
1200
  }
1328
1201
  const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
1329
- return { output, promptText: null };
1202
+ return {
1203
+ requestId: workflowRequestId(context.state.runId, execution.attemptId),
1204
+ requestKind: "checkpoint",
1205
+ contract: output,
1206
+ };
1330
1207
  }
1331
1208
  function shellReceipt(result) {
1332
1209
  return {
@@ -1496,8 +1373,7 @@ export function workflowIdentityMismatch(state, workflow, source) {
1496
1373
  const currentSources = metadata?.sources ?? [];
1497
1374
  if (!isDeepStrictEqual(state.workflowSources ?? [], currentSources))
1498
1375
  return true;
1499
- const currentDigest = metadata?.snapshot.mounts.length ? definitionDigest(workflow) : undefined;
1500
- return state.definitionDigest !== currentDigest;
1376
+ return state.definitionDigest !== definitionDigest(workflow);
1501
1377
  }
1502
1378
  function definitionDigest(workflow) {
1503
1379
  return `sha256:${createHash("sha256")
@@ -1578,9 +1454,9 @@ function settingsDefinitionForNode(workflow, nodeId) {
1578
1454
  * The step contract appended to every agent-node prompt. This is the
1579
1455
  * documented standard for how the model completes a workflow step.
1580
1456
  */
1581
- export function appendStepContract(prompt, workflowName, nodeId, attemptId, expectedOutput) {
1582
- const assistant = assistantMessageOutput(expectedOutput);
1583
- if (assistant !== undefined) {
1457
+ export function appendStepContract(prompt, contract) {
1458
+ const { requestId, workflowName, nodeId, attemptId, expectedOutput } = contract;
1459
+ if (contract.completion === "assistant") {
1584
1460
  return [
1585
1461
  prompt.trimEnd(),
1586
1462
  "",
@@ -1590,8 +1466,8 @@ export function appendStepContract(prompt, workflowName, nodeId, attemptId, expe
1590
1466
  "Reply with a normal assistant message.",
1591
1467
  "Do not call the workflow tool to complete this step.",
1592
1468
  "Your visible reply becomes the workflow step output after the turn settles.",
1593
- ...(assistant.maxChars !== undefined
1594
- ? [`Keep the visible reply within ${assistant.maxChars} characters.`]
1469
+ ...(contract.maxOutputChars !== undefined
1470
+ ? [`Keep the visible reply within ${contract.maxOutputChars} characters.`]
1595
1471
  : []),
1596
1472
  ].join("\n");
1597
1473
  }
@@ -1602,9 +1478,9 @@ export function appendStepContract(prompt, workflowName, nodeId, attemptId, expe
1602
1478
  `Workflow step contract (workflow: ${workflowName}, step: ${nodeId}, attempt: ${attemptId})`,
1603
1479
  "",
1604
1480
  "While this step is active, you may publish non-completing updates with:",
1605
- `{"action": "update", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
1481
+ `{"action": "update", "requestId": ${JSON.stringify(requestId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
1606
1482
  "Complete this step by calling the `workflow` tool exactly once with:",
1607
- `{"action": "submit", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
1483
+ `{"action": "submit", "requestId": ${JSON.stringify(requestId)}, "output": <your result>}`,
1608
1484
  `Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
1609
1485
  "The step is complete only after the workflow tool accepts the output.",
1610
1486
  "If the tool reports a validation error, correct the output and call it again.",