@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
@@ -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);
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);
202
204
  if (sourceMismatch && options.force !== true) {
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;
@@ -249,16 +254,8 @@ export class WorkflowEngine {
249
254
  });
250
255
  await this.onRunStarted?.(runId, state);
251
256
  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) {
257
+ // The last attempt finished before the terminal event was committed.
258
+ if (point.failedResult === undefined) {
262
259
  await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
263
260
  }
264
261
  else {
@@ -283,138 +280,12 @@ export class WorkflowEngine {
283
280
  }
284
281
  return { runId, state };
285
282
  }
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
283
  /**
413
284
  * Find where a resumed run continues. An in-flight node reruns; otherwise
414
285
  * routing continues from the last recorded step. A null nodeId means the
415
286
  * graph was already done when the crash hit.
416
287
  */
417
- resumePointFor(workflow, state, checkpointBehavior) {
288
+ resumePointFor(workflow, state) {
418
289
  if (state.currentNode !== undefined) {
419
290
  if (workflow.nodes[state.currentNode] === undefined) {
420
291
  throw new Error(`Resume node is missing from the workflow: ${state.currentNode}`);
@@ -430,17 +301,6 @@ export class WorkflowEngine {
430
301
  return { nodeId: lastStep.nodeId };
431
302
  }
432
303
  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
304
  const next = resolveNext(workflow.edges, lastStep.nodeId, result.output, result);
445
305
  return next === null
446
306
  ? { nodeId: null, lastOutput: result.output }
@@ -602,6 +462,7 @@ export class WorkflowEngine {
602
462
  scopeId: latest.scopeId,
603
463
  previousChangeNumber: attempt.settings.changeNumber,
604
464
  changeNumber: latest.changeNumber,
465
+ settingsHash: latest.settingsHash,
605
466
  },
606
467
  });
607
468
  resumedAttempt = {
@@ -647,13 +508,6 @@ export class WorkflowEngine {
647
508
  });
648
509
  }
649
510
  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
511
  currentNodeId = resolveNext(workflow.edges, attempt.result.nodeId, attempt.result.output, attempt.result);
658
512
  }
659
513
  await this.finishRun(runId, state, "completed", { finalOutput: lastOutput });
@@ -749,7 +603,8 @@ export class WorkflowEngine {
749
603
  delete state.currentNode;
750
604
  delete state.currentAttemptId;
751
605
  delete state.currentNodeStartedAt;
752
- delete state.currentNodeDeadlineAt;
606
+ delete state.currentNodeTimeoutMs;
607
+ delete state.currentNodeElapsedMs;
753
608
  delete state.currentSettingsScopeId;
754
609
  delete state.currentSettingsChangeNumber;
755
610
  delete state.currentSettingsHash;
@@ -795,7 +650,7 @@ export class WorkflowEngine {
795
650
  }
796
651
  const meta = { promptText: null };
797
652
  try {
798
- const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, resumedAttempt?.deadlineAt);
653
+ const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, resumedAttempt?.timeoutMs, resumedAttempt?.elapsedMs);
799
654
  return {
800
655
  result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
801
656
  execution,
@@ -845,7 +700,7 @@ export class WorkflowEngine {
845
700
  ...(output !== undefined ? { output } : {}),
846
701
  };
847
702
  }
848
- async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, persistedDeadlineAt) {
703
+ async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, persistedTimeoutMs, elapsedMs = 0) {
849
704
  const abort = new AbortController();
850
705
  const context = this.createNodeContext(state, abort.signal, settings);
851
706
  let timer;
@@ -862,43 +717,41 @@ export class WorkflowEngine {
862
717
  }
863
718
  let timeoutMs;
864
719
  let timeoutErrorMs;
865
- if (persistedDeadlineAt === undefined) {
720
+ if (persistedTimeoutMs === undefined) {
866
721
  timeoutMs = await this.resolveNodeTimeout(node, context, abort);
867
722
  timeoutErrorMs = timeoutMs;
868
- if (node.nodeType === "agent" && this.executor.preservesDeadlineWhileParked === true) {
869
- state.currentNodeDeadlineAt =
870
- timeoutMs === null ? null : new Date(Date.now() + timeoutMs).toISOString();
723
+ if (node.nodeType === "agent" && this.executor.preservesActiveTimeBudget === true) {
724
+ state.currentNodeTimeoutMs = timeoutMs;
725
+ state.currentNodeElapsedMs = 0;
871
726
  await this.persist(runId, state, {
872
727
  scope: "node",
873
- type: "node_deadline_set",
728
+ type: "node_timeout_set",
874
729
  nodeId,
875
730
  attemptId,
876
- payload: { deadlineAt: state.currentNodeDeadlineAt },
731
+ payload: { timeoutMs },
877
732
  });
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
733
  }
884
734
  else {
885
- delete state.currentNodeDeadlineAt;
735
+ delete state.currentNodeTimeoutMs;
736
+ delete state.currentNodeElapsedMs;
886
737
  }
887
738
  }
888
- else if (persistedDeadlineAt === null) {
889
- state.currentNodeDeadlineAt = null;
739
+ else if (persistedTimeoutMs === null) {
740
+ state.currentNodeTimeoutMs = null;
890
741
  timeoutMs = null;
891
742
  timeoutErrorMs = null;
892
743
  }
893
744
  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");
745
+ if (!Number.isFinite(persistedTimeoutMs) ||
746
+ persistedTimeoutMs <= 0 ||
747
+ !Number.isFinite(elapsedMs) ||
748
+ elapsedMs < 0) {
749
+ throw new Error("Persisted node timeout is invalid");
898
750
  }
899
- state.currentNodeDeadlineAt = persistedDeadlineAt;
900
- timeoutMs = deadline - Date.now();
901
- timeoutErrorMs = Math.max(1, deadline - started);
751
+ state.currentNodeTimeoutMs = persistedTimeoutMs;
752
+ state.currentNodeElapsedMs = elapsedMs;
753
+ timeoutMs = persistedTimeoutMs - elapsedMs;
754
+ timeoutErrorMs = persistedTimeoutMs;
902
755
  if (timeoutMs <= 0)
903
756
  throw new TimeoutError(timeoutErrorMs);
904
757
  }
@@ -1010,13 +863,42 @@ export class WorkflowEngine {
1010
863
  }
1011
864
  case "action":
1012
865
  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
- });
866
+ case "checkpoint": {
867
+ const saved = await this.store.readCheckpoint(state.runId, attemptId);
868
+ if (saved !== undefined) {
869
+ if (saved.request.status === "cancelled")
870
+ throw new Error("Checkpoint request is cancelled");
871
+ if (saved.request.status === "settled") {
872
+ if (saved.humanDecision !== undefined)
873
+ state.humanDecision = saved.humanDecision;
874
+ return { output: saved.output, promptText: null };
875
+ }
876
+ }
877
+ else {
878
+ const request = await checkpointRequest(node, context, {
879
+ workflowName: workflow.name,
880
+ nodeId,
881
+ attemptId,
882
+ });
883
+ const event = await this.store.commitTransition(state.runId, {
884
+ kind: "waitForInput",
885
+ ...request,
886
+ event: {
887
+ scope: "node",
888
+ type: "checkpoint_requested",
889
+ nodeId,
890
+ attemptId,
891
+ payload: { requestId: request.requestId, requestKind: request.requestKind },
892
+ },
893
+ });
894
+ state.traceSeq = event.seq;
895
+ state.updatedAt = event.at;
896
+ }
897
+ state.status = "waiting";
898
+ state.waitingOn = nodeId;
899
+ delete state.finishedAt;
900
+ throw new RunParkedError();
901
+ }
1020
902
  }
1021
903
  }
1022
904
  async captureSettingsBinding(workflow, state, nodeId) {
@@ -1072,11 +954,12 @@ export class WorkflowEngine {
1072
954
  return scope;
1073
955
  }
1074
956
  createNodeContext(state, signal, settings) {
957
+ const snapshot = deepFreezeJson(structuredClone(state));
1075
958
  return {
1076
- input: state.input,
1077
- outputs: state.outputs,
1078
- results: state.results,
1079
- state,
959
+ input: snapshot.input,
960
+ outputs: snapshot.outputs,
961
+ results: snapshot.results,
962
+ state: snapshot,
1080
963
  ...(settings !== undefined
1081
964
  ? {
1082
965
  settings: deepFreezeJson(structuredClone(settings.settings)),
@@ -1114,7 +997,17 @@ export class WorkflowEngine {
1114
997
  // may already be terminal.
1115
998
  throw abortError(signal);
1116
999
  }
1117
- const prompt = appendStepContract(basePrompt, workflow.name, nodeId, attemptId, node.expectedOutput);
1000
+ const contract = {
1001
+ requestId: workflowRequestId(state.runId, attemptId),
1002
+ runId: state.runId,
1003
+ workflowName: workflow.name,
1004
+ nodeId,
1005
+ attemptId,
1006
+ completion: assistant === undefined ? "submit" : "assistant",
1007
+ ...(typeof node.expectedOutput === "string" ? { expectedOutput: node.expectedOutput } : {}),
1008
+ ...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
1009
+ };
1010
+ const prompt = appendStepContract(basePrompt, contract);
1118
1011
  meta.promptText = prompt;
1119
1012
  await this.persist(runId, state, {
1120
1013
  scope: "agent",
@@ -1124,17 +1017,7 @@ export class WorkflowEngine {
1124
1017
  payload: { prompt, completion: assistant === undefined ? "submit" : "assistant" },
1125
1018
  });
1126
1019
  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
- },
1020
+ contract,
1138
1021
  prompt,
1139
1022
  ...(state.runTitle !== undefined || node.statusDetail !== undefined
1140
1023
  ? {
@@ -1251,7 +1134,9 @@ export class WorkflowEngine {
1251
1134
  }
1252
1135
  }
1253
1136
  async persist(runId, state, event) {
1254
- const traceEvent = await this.store.writeSnapshot(runId, state, event);
1137
+ const traceEvent = await this.store.commitTransition(runId, executionTransition(state, event));
1138
+ state.traceSeq = traceEvent.seq;
1139
+ state.updatedAt = traceEvent.at;
1255
1140
  try {
1256
1141
  this.onEvent?.(traceEvent, state);
1257
1142
  }
@@ -1270,41 +1155,29 @@ export class WorkflowEngine {
1270
1155
  if (fields.error !== undefined) {
1271
1156
  state.error = fields.error;
1272
1157
  }
1273
- if (fields.waitingOn !== undefined) {
1274
- state.waitingOn = fields.waitingOn;
1275
- }
1276
1158
  if (fields.finalOutput !== undefined) {
1277
1159
  state.finalOutput = fields.finalOutput;
1278
1160
  }
1279
1161
  delete state.currentNode;
1280
1162
  delete state.currentAttemptId;
1281
1163
  delete state.currentNodeStartedAt;
1282
- delete state.currentNodeDeadlineAt;
1164
+ delete state.currentNodeTimeoutMs;
1165
+ delete state.currentNodeElapsedMs;
1283
1166
  delete state.currentSettingsScopeId;
1284
1167
  delete state.currentSettingsChangeNumber;
1285
1168
  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
1169
  await this.persist(runId, state, {
1295
1170
  scope: "run",
1296
1171
  type: `run_${status}`,
1297
1172
  payload: {
1298
1173
  status,
1299
1174
  ...(fields.error !== undefined ? { error: fields.error } : {}),
1300
- ...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
1301
1175
  ...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
1302
- presentationRequired: status === "completed" && this.presentationRequired,
1303
1176
  },
1304
1177
  });
1305
1178
  }
1306
1179
  }
1307
- async function runCheckpointNode(node, context, execution) {
1180
+ async function checkpointRequest(node, context, execution) {
1308
1181
  if (node.humanDecision !== undefined) {
1309
1182
  const prompt = await node.humanDecision.request(context);
1310
1183
  const audience = typeof node.humanDecision.audience === "function"
@@ -1322,11 +1195,18 @@ async function runCheckpointNode(node, context, execution) {
1322
1195
  prompt,
1323
1196
  ...(timeout !== undefined ? { timeout } : {}),
1324
1197
  });
1325
- await execution.store.createHumanDecisionRequest(request);
1326
- return { output: request, promptText: null };
1198
+ return {
1199
+ requestId: request.decisionId,
1200
+ requestKind: "decision",
1201
+ contract: request,
1202
+ };
1327
1203
  }
1328
1204
  const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
1329
- return { output, promptText: null };
1205
+ return {
1206
+ requestId: workflowRequestId(context.state.runId, execution.attemptId),
1207
+ requestKind: "checkpoint",
1208
+ contract: output,
1209
+ };
1330
1210
  }
1331
1211
  function shellReceipt(result) {
1332
1212
  return {
@@ -1578,9 +1458,9 @@ function settingsDefinitionForNode(workflow, nodeId) {
1578
1458
  * The step contract appended to every agent-node prompt. This is the
1579
1459
  * documented standard for how the model completes a workflow step.
1580
1460
  */
1581
- export function appendStepContract(prompt, workflowName, nodeId, attemptId, expectedOutput) {
1582
- const assistant = assistantMessageOutput(expectedOutput);
1583
- if (assistant !== undefined) {
1461
+ export function appendStepContract(prompt, contract) {
1462
+ const { requestId, workflowName, nodeId, attemptId, expectedOutput } = contract;
1463
+ if (contract.completion === "assistant") {
1584
1464
  return [
1585
1465
  prompt.trimEnd(),
1586
1466
  "",
@@ -1590,8 +1470,8 @@ export function appendStepContract(prompt, workflowName, nodeId, attemptId, expe
1590
1470
  "Reply with a normal assistant message.",
1591
1471
  "Do not call the workflow tool to complete this step.",
1592
1472
  "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.`]
1473
+ ...(contract.maxOutputChars !== undefined
1474
+ ? [`Keep the visible reply within ${contract.maxOutputChars} characters.`]
1595
1475
  : []),
1596
1476
  ].join("\n");
1597
1477
  }
@@ -1602,9 +1482,9 @@ export function appendStepContract(prompt, workflowName, nodeId, attemptId, expe
1602
1482
  `Workflow step contract (workflow: ${workflowName}, step: ${nodeId}, attempt: ${attemptId})`,
1603
1483
  "",
1604
1484
  "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": {...}}}`,
1485
+ `{"action": "update", "requestId": ${JSON.stringify(requestId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
1606
1486
  "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>}`,
1487
+ `{"action": "submit", "requestId": ${JSON.stringify(requestId)}, "output": <your result>}`,
1608
1488
  `Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
1609
1489
  "The step is complete only after the workflow tool accepts the output.",
1610
1490
  "If the tool reports a validation error, correct the output and call it again.",