@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
@@ -18,8 +18,9 @@ import {
18
18
  WorkflowSourceChangedError,
19
19
  } from "./errors.js";
20
20
  import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
21
- import { createHumanDecisionRequest, validateHumanDecisionResponse } from "./human-decision.js";
21
+ import { createHumanDecisionRequest } from "./human-decision.js";
22
22
  import { extractJsonValue } from "./json.js";
23
+ import { workflowRequestId } from "./requests.js";
23
24
  import {
24
25
  parseWorkflowSettingsValue,
25
26
  resolveInitialWorkflowSettings,
@@ -33,11 +34,12 @@ import {
33
34
  createRunId,
34
35
  type WorkflowExecutionStore,
35
36
  } from "./store.js";
37
+ import { executionTransition } from "./transitions.js";
36
38
  import type {
37
- ResolvedHumanDecision,
38
39
  AgentExpectedOutput,
39
40
  AgentNodeDefinition,
40
41
  AgentStepExecutor,
42
+ AgentStepContract,
41
43
  AssistantMessageOutput,
42
44
  AssistantMessageReceipt,
43
45
  ActionNodeDefinition,
@@ -54,7 +56,6 @@ import type {
54
56
  WorkflowNodeOutcome,
55
57
  WorkflowNodeResult,
56
58
  WorkflowNotificationSink,
57
- HumanDecisionRequest,
58
59
  WorkflowRunResult,
59
60
  WorkflowRunState,
60
61
  WorkflowSource,
@@ -63,7 +64,12 @@ import type {
63
64
  WorkflowUpdateInput,
64
65
  WorkflowUpdateReceipt,
65
66
  } from "./types.js";
66
- import { UpdateRateLimiter, updateReceipt, validateWorkflowUpdate } from "./updates.js";
67
+ import {
68
+ UpdateRateLimiter,
69
+ updateProjection,
70
+ updateReceipt,
71
+ validateWorkflowUpdate,
72
+ } from "./updates.js";
67
73
 
68
74
  const DEFAULT_NODE_TIMEOUT_MS = 15 * 60_000;
69
75
  const DEFAULT_MAX_STEPS = 100;
@@ -101,7 +107,8 @@ type ResumedNodeAttempt = {
101
107
  nodeId: string;
102
108
  attemptId: string;
103
109
  startedAt: string;
104
- deadlineAt?: string | null;
110
+ timeoutMs?: number | null;
111
+ elapsedMs?: number;
105
112
  settings?: WorkflowSettingsScopeRecord;
106
113
  };
107
114
 
@@ -119,7 +126,6 @@ export class WorkflowEngine {
119
126
  private readonly maxSteps: number;
120
127
  private readonly onEvent?: WorkflowEngineOptions["onEvent"];
121
128
  private readonly onRunStarted?: WorkflowEngineOptions["onRunStarted"];
122
- private readonly onRunFinishing?: WorkflowEngineOptions["onRunFinishing"];
123
129
  private activeAbort: AbortController | null = null;
124
130
  private activeAttempt:
125
131
  | {
@@ -136,7 +142,6 @@ export class WorkflowEngine {
136
142
  private cancelled = false;
137
143
  private parked = false;
138
144
  private paused = false;
139
- private presentationRequired = false;
140
145
  private wakePause: (() => void) | null = null;
141
146
 
142
147
  constructor(options: WorkflowEngineOptions) {
@@ -147,7 +152,6 @@ export class WorkflowEngine {
147
152
  this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
148
153
  this.onEvent = options.onEvent;
149
154
  this.onRunStarted = options.onRunStarted;
150
- this.onRunFinishing = options.onRunFinishing;
151
155
  }
152
156
 
153
157
  get databasePath(): string {
@@ -190,12 +194,14 @@ export class WorkflowEngine {
190
194
  limiter.take();
191
195
  const { event, record } = await this.store.publishUpdate(
192
196
  active.runId,
193
- active.state,
194
197
  active.nodeId,
195
198
  active.attemptId,
196
199
  update,
197
200
  { signal: active.signal },
198
201
  );
202
+ active.state.updates = updateProjection(active.state.updates, record);
203
+ active.state.traceSeq = event.seq;
204
+ active.state.updatedAt = event.at;
199
205
  try {
200
206
  this.onEvent?.(event, active.state);
201
207
  } catch {
@@ -257,7 +263,6 @@ export class WorkflowEngine {
257
263
  ): Promise<WorkflowRunResult> {
258
264
  workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
259
265
  validateWorkflowDefinition(workflow);
260
- this.presentationRequired = workflow.presentationPrompt !== undefined;
261
266
  // Fail before any run row exists so bad input cannot leave partial state.
262
267
  const suppliedInput = input === undefined ? null : input;
263
268
  const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
@@ -324,25 +329,25 @@ export class WorkflowEngine {
324
329
  runId: string,
325
330
  options: {
326
331
  workflowSource?: WorkflowSource;
327
- force?: boolean;
328
332
  resumeInteractionAttemptId?: string;
329
333
  } = {},
330
334
  ): Promise<WorkflowRunResult> {
331
335
  workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
332
336
  validateWorkflowDefinition(workflow);
333
- this.presentationRequired = workflow.presentationPrompt !== undefined;
334
337
  // Reset before any await: a park or cancel landing during preparation
335
338
  // must survive, or a server drain would hang while the run executes.
336
339
  this.cancelled = false;
337
340
  this.paused = false;
338
341
  this.parked = false;
339
- const state = await this.store.prepareRunResume(runId);
340
- const sourceMismatch = workflowIdentityMismatch(state, workflow, options.workflowSource);
341
- if (sourceMismatch && options.force !== true) {
342
+ const stored = await this.store.readRunState(runId);
343
+ if (stored === null) throw new Error(`Cannot resume unreadable workflow run: ${runId}`);
344
+ const sourceMismatch = workflowIdentityMismatch(stored, workflow, options.workflowSource);
345
+ if (sourceMismatch) {
342
346
  throw new WorkflowSourceChangedError(runId);
343
347
  }
348
+ const state = await this.store.prepareRunResume(runId);
344
349
 
345
- const point = this.resumePointFor(workflow, state, "wait");
350
+ const point = this.resumePointFor(workflow, state);
346
351
  const interruptedNode =
347
352
  state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
348
353
  let resumedAttempt: ResumedNodeAttempt | undefined;
@@ -350,18 +355,19 @@ export class WorkflowEngine {
350
355
  state.currentNode !== undefined &&
351
356
  state.currentAttemptId !== undefined &&
352
357
  state.currentNodeStartedAt !== undefined &&
353
- interruptedNode?.nodeType === "agent" &&
354
- (assistantMessageConfig(interruptedNode) !== undefined ||
355
- options.resumeInteractionAttemptId === state.currentAttemptId)
358
+ (interruptedNode?.nodeType === "checkpoint" ||
359
+ (interruptedNode?.nodeType === "agent" &&
360
+ (assistantMessageConfig(interruptedNode) !== undefined ||
361
+ options.resumeInteractionAttemptId === state.currentAttemptId)))
356
362
  ) {
357
363
  const resumedSettings = await this.persistedSettingsBinding(state);
358
364
  resumedAttempt = {
359
365
  nodeId: state.currentNode,
360
366
  attemptId: state.currentAttemptId,
361
367
  startedAt: state.currentNodeStartedAt,
362
- ...(state.currentNodeDeadlineAt === undefined
368
+ ...(state.currentNodeTimeoutMs === undefined
363
369
  ? {}
364
- : { deadlineAt: state.currentNodeDeadlineAt }),
370
+ : { timeoutMs: state.currentNodeTimeoutMs, elapsedMs: state.currentNodeElapsedMs ?? 0 }),
365
371
  ...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
366
372
  };
367
373
  }
@@ -374,7 +380,8 @@ export class WorkflowEngine {
374
380
  delete state.currentNode;
375
381
  delete state.currentAttemptId;
376
382
  delete state.currentNodeStartedAt;
377
- delete state.currentNodeDeadlineAt;
383
+ delete state.currentNodeTimeoutMs;
384
+ delete state.currentNodeElapsedMs;
378
385
  delete state.currentSettingsScopeId;
379
386
  delete state.currentSettingsChangeNumber;
380
387
  delete state.currentSettingsHash;
@@ -387,21 +394,13 @@ export class WorkflowEngine {
387
394
  ...(point.nodeId !== null ? { resumeAt: point.nodeId } : {}),
388
395
  ...(resumedAttempt !== undefined ? { resumedAttemptId: resumedAttempt.attemptId } : {}),
389
396
  replayedSteps: state.steps.length,
390
- ...(sourceMismatch ? { workflowSourceMismatch: true, forced: true } : {}),
391
397
  },
392
398
  });
393
399
  await this.onRunStarted?.(runId, state);
394
400
 
395
401
  if (point.nodeId === null) {
396
- // The last recorded transition already finished the graph; the crash
397
- // happened before the terminal event was written. A finished
398
- // checkpoint restores its waiting gate rather than completing.
399
- if (point.waitingOn !== undefined) {
400
- await this.finishRun(runId, state, "waiting", {
401
- waitingOn: point.waitingOn,
402
- finalOutput: point.lastOutput,
403
- });
404
- } else if (point.failedResult === undefined) {
402
+ // The last attempt finished before the terminal event was committed.
403
+ if (point.failedResult === undefined) {
405
404
  await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
406
405
  } else {
407
406
  const timedOut = point.failedResult.outcome === "timed_out";
@@ -433,161 +432,6 @@ export class WorkflowEngine {
433
432
  return { runId, state };
434
433
  }
435
434
 
436
- /**
437
- * Start a continuation run from a checkpointed parent. The new run gets a
438
- * fresh run and event stream, carries forward the parent's outputs, results,
439
- * and step accounting, and continues routing after the checkpoint.
440
- */
441
- async continueRun(
442
- workflow: WorkflowDefinition,
443
- parentRunId: string,
444
- input: unknown,
445
- options: {
446
- workflowSource?: WorkflowSource;
447
- runId?: string;
448
- force?: boolean;
449
- humanDecision?: ResolvedHumanDecision;
450
- } = {},
451
- ): Promise<WorkflowRunResult> {
452
- workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
453
- validateWorkflowDefinition(workflow);
454
- this.presentationRequired = workflow.presentationPrompt !== undefined;
455
- this.cancelled = false;
456
- this.paused = false;
457
- this.parked = false;
458
- const parentState = await this.store.readRunState(parentRunId);
459
- if (parentState === null) {
460
- throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
461
- }
462
- if (parentState.status !== "waiting" || parentState.waitingOn === undefined) {
463
- throw new Error(
464
- `Cannot continue workflow run ${parentRunId} with status ${parentState.status}`,
465
- );
466
- }
467
- const sourceMismatch = workflowIdentityMismatch(parentState, workflow, options.workflowSource);
468
- if (sourceMismatch && options.force !== true) {
469
- throw new WorkflowSourceChangedError(parentRunId);
470
- }
471
-
472
- const waitingNodeId = parentState.waitingOn;
473
- const waitingNode = workflow.nodes[waitingNodeId];
474
- const humanContract =
475
- waitingNode?.nodeType === "checkpoint" ? waitingNode.humanDecision : undefined;
476
- let acceptedResponse: unknown;
477
- let acceptedNodeId: string | undefined;
478
- let normalizedInput: unknown;
479
- if (humanContract !== undefined) {
480
- if (options.humanDecision === undefined) {
481
- throw new Error(`Checkpoint ${waitingNodeId} requires an accepted verified human decision`);
482
- }
483
- const request = parentState.finalOutput as HumanDecisionRequest;
484
- if (
485
- request?.schema !== "pi-workflows.human-decision-request.v1" ||
486
- request.decisionId !== options.humanDecision.decisionId ||
487
- request.requestDigest !== options.humanDecision.requestDigest
488
- ) {
489
- throw new Error("Accepted human decision does not match the waiting request");
490
- }
491
- const durableDecision = await this.store.readResolvedHumanDecision(request.decisionId);
492
- if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
493
- throw new Error("Accepted human decision does not match the durable decision record");
494
- }
495
- acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
496
- acceptedNodeId = request.nodeId;
497
- normalizedInput = structuredClone(parentState.input);
498
- } else {
499
- const suppliedInput = input === undefined ? null : input;
500
- normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
501
- }
502
- assertJsonSerializable(normalizedInput, "Workflow run input");
503
- if (options.runId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(options.runId)) {
504
- throw new Error(`Invalid workflow run id: ${JSON.stringify(options.runId)}`);
505
- }
506
-
507
- const state = await this.createRunState(
508
- workflow,
509
- normalizedInput,
510
- options.workflowSource,
511
- options.runId,
512
- );
513
- state.parentRunId = parentRunId;
514
- state.outputs = structuredClone(parentState.outputs);
515
- state.results = structuredClone(parentState.results);
516
- state.steps = structuredClone(parentState.steps);
517
- if (humanContract !== undefined && options.humanDecision !== undefined) {
518
- const receipt = {
519
- decisionId: options.humanDecision.decisionId,
520
- requestDigest: options.humanDecision.requestDigest,
521
- nodeId: acceptedNodeId ?? waitingNodeId,
522
- response: options.humanDecision.response,
523
- provenance: options.humanDecision.provenance,
524
- acceptedAt: options.humanDecision.acceptedAt,
525
- answerDigest: options.humanDecision.answerDigest,
526
- };
527
- state.humanDecision = {
528
- schema: "pi-workflows.human-decision-receipt.v1",
529
- ...receipt,
530
- subjectDigest: options.humanDecision.subjectDigest,
531
- presentationDigest: options.humanDecision.presentationDigest,
532
- revision: options.humanDecision.revision,
533
- };
534
- state.outputs[waitingNodeId] = acceptedResponse;
535
- const priorResult = state.results[waitingNodeId];
536
- if (priorResult === undefined) {
537
- throw new Error(`Waiting human decision result is missing for ${waitingNodeId}`);
538
- }
539
- state.results[waitingNodeId] = { ...priorResult, output: acceptedResponse };
540
- const stepIndex = state.steps.findLastIndex((step) => step.nodeId === waitingNodeId);
541
- if (stepIndex < 0)
542
- throw new Error(`Waiting human decision step is missing for ${waitingNodeId}`);
543
- const priorStep = state.steps[stepIndex];
544
- if (priorStep === undefined)
545
- throw new Error("Waiting human decision step became unavailable");
546
- state.steps[stepIndex] = { ...priorStep, output: acceptedResponse };
547
- }
548
- state.carriedStepCount = state.steps.length;
549
-
550
- const runId = await this.store.initializeRun(workflow, state);
551
- await this.persist(runId, state, {
552
- scope: "run",
553
- type: "run_started",
554
- payload: {
555
- workflowName: workflow.name,
556
- ...(state.runTitle ? { runTitle: state.runTitle } : {}),
557
- input: state.input,
558
- continuedFrom: parentRunId,
559
- checkpoint: parentState.waitingOn,
560
- carriedSteps: state.steps.length,
561
- },
562
- });
563
- await this.onRunStarted?.(runId, state);
564
-
565
- const point = this.resumePointFor(workflow, state, "continue");
566
- if (point.nodeId === null) {
567
- // The checkpoint was the final node; the answer completes the chain.
568
- await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
569
- return { runId, state };
570
- }
571
- try {
572
- await this.executeGraph(
573
- workflow,
574
- state,
575
- runId,
576
- point.nodeId,
577
- countExecutableSteps(workflow, state.steps),
578
- point.lastOutput,
579
- );
580
- } catch (error) {
581
- if (isClaimLostError(error)) throw error;
582
- if (isRunParkedError(error) || this.parked) {
583
- return { runId, state };
584
- }
585
- await this.finishAfterError(runId, state, error);
586
- return { runId, state };
587
- }
588
- return { runId, state };
589
- }
590
-
591
435
  /**
592
436
  * Find where a resumed run continues. An in-flight node reruns; otherwise
593
437
  * routing continues from the last recorded step. A null nodeId means the
@@ -596,12 +440,10 @@ export class WorkflowEngine {
596
440
  private resumePointFor(
597
441
  workflow: WorkflowDefinition,
598
442
  state: WorkflowRunState,
599
- checkpointBehavior: "wait" | "continue",
600
443
  ): {
601
444
  nodeId: string | null;
602
445
  lastOutput?: unknown;
603
446
  failedResult?: WorkflowNodeResult;
604
- waitingOn?: string;
605
447
  } {
606
448
  if (state.currentNode !== undefined) {
607
449
  if (workflow.nodes[state.currentNode] === undefined) {
@@ -618,19 +460,6 @@ export class WorkflowEngine {
618
460
  return { nodeId: lastStep.nodeId };
619
461
  }
620
462
  if (result.outcome === "ok") {
621
- // A recorded checkpoint means the run should be waiting; a crash
622
- // before the run_waiting persist restores the gate instead of
623
- // routing past it. The gate applies to this run's own checkpoint
624
- // only: a continuation's carried steps end with the parent's
625
- // already-answered checkpoint, and routing must continue from it.
626
- const isCarriedStep = state.steps.length <= (state.carriedStepCount ?? 0);
627
- if (
628
- checkpointBehavior === "wait" &&
629
- !isCarriedStep &&
630
- workflow.nodes[lastStep.nodeId]?.nodeType === "checkpoint"
631
- ) {
632
- return { nodeId: null, waitingOn: lastStep.nodeId, lastOutput: result.output };
633
- }
634
463
  const next = resolveNext(workflow.edges, lastStep.nodeId, result.output, result);
635
464
  return next === null
636
465
  ? { nodeId: null, lastOutput: result.output }
@@ -702,9 +531,7 @@ export class WorkflowEngine {
702
531
  ...(await this.resolveTitleBounded(workflow, input)),
703
532
  ...(workflowSource !== undefined ? { workflowSource } : {}),
704
533
  ...(composition?.sources.length ? { workflowSources: composition.sources } : {}),
705
- ...(composition?.snapshot.mounts.length
706
- ? { definitionDigest: definitionDigest(workflow) }
707
- : {}),
534
+ definitionDigest: definitionDigest(workflow),
708
535
  startedAt: now,
709
536
  updatedAt: now,
710
537
  status: "running",
@@ -833,6 +660,7 @@ export class WorkflowEngine {
833
660
  scopeId: latest.scopeId,
834
661
  previousChangeNumber: attempt.settings.changeNumber,
835
662
  changeNumber: latest.changeNumber,
663
+ settingsHash: latest.settingsHash,
836
664
  },
837
665
  });
838
666
  resumedAttempt = {
@@ -881,13 +709,6 @@ export class WorkflowEngine {
881
709
  }
882
710
 
883
711
  lastOutput = attempt.result.output;
884
- if (node.nodeType === "checkpoint") {
885
- await this.finishRun(runId, state, "waiting", {
886
- waitingOn: attempt.result.nodeId,
887
- finalOutput: lastOutput,
888
- });
889
- return;
890
- }
891
712
  currentNodeId = resolveNext(
892
713
  workflow.edges,
893
714
  attempt.result.nodeId,
@@ -999,7 +820,8 @@ export class WorkflowEngine {
999
820
  delete state.currentNode;
1000
821
  delete state.currentAttemptId;
1001
822
  delete state.currentNodeStartedAt;
1002
- delete state.currentNodeDeadlineAt;
823
+ delete state.currentNodeTimeoutMs;
824
+ delete state.currentNodeElapsedMs;
1003
825
  delete state.currentSettingsScopeId;
1004
826
  delete state.currentSettingsChangeNumber;
1005
827
  delete state.currentSettingsHash;
@@ -1063,7 +885,8 @@ export class WorkflowEngine {
1063
885
  node,
1064
886
  meta,
1065
887
  settings,
1066
- resumedAttempt?.deadlineAt,
888
+ resumedAttempt?.timeoutMs,
889
+ resumedAttempt?.elapsedMs,
1067
890
  );
1068
891
  return {
1069
892
  result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
@@ -1131,7 +954,8 @@ export class WorkflowEngine {
1131
954
  node: WorkflowNodeDefinition,
1132
955
  meta: NodeExecutionMeta,
1133
956
  settings?: WorkflowSettingsScopeRecord,
1134
- persistedDeadlineAt?: string | null,
957
+ persistedTimeoutMs?: number | null,
958
+ elapsedMs = 0,
1135
959
  ): Promise<NodeExecution> {
1136
960
  const abort = new AbortController();
1137
961
  const context = this.createNodeContext(state, abort.signal, settings);
@@ -1150,39 +974,40 @@ export class WorkflowEngine {
1150
974
 
1151
975
  let timeoutMs: number | null;
1152
976
  let timeoutErrorMs: number | null;
1153
- if (persistedDeadlineAt === undefined) {
977
+ if (persistedTimeoutMs === undefined) {
1154
978
  timeoutMs = await this.resolveNodeTimeout(node, context, abort);
1155
979
  timeoutErrorMs = timeoutMs;
1156
- if (node.nodeType === "agent" && this.executor.preservesDeadlineWhileParked === true) {
1157
- state.currentNodeDeadlineAt =
1158
- timeoutMs === null ? null : new Date(Date.now() + timeoutMs).toISOString();
980
+ if (node.nodeType === "agent" && this.executor.preservesActiveTimeBudget === true) {
981
+ state.currentNodeTimeoutMs = timeoutMs;
982
+ state.currentNodeElapsedMs = 0;
1159
983
  await this.persist(runId, state, {
1160
984
  scope: "node",
1161
- type: "node_deadline_set",
985
+ type: "node_timeout_set",
1162
986
  nodeId,
1163
987
  attemptId,
1164
- payload: { deadlineAt: state.currentNodeDeadlineAt },
988
+ payload: { timeoutMs },
1165
989
  });
1166
- if (state.currentNodeDeadlineAt !== null && timeoutErrorMs !== null) {
1167
- timeoutMs = Date.parse(state.currentNodeDeadlineAt) - Date.now();
1168
- if (timeoutMs <= 0) throw new TimeoutError(timeoutErrorMs);
1169
- }
1170
990
  } else {
1171
- delete state.currentNodeDeadlineAt;
991
+ delete state.currentNodeTimeoutMs;
992
+ delete state.currentNodeElapsedMs;
1172
993
  }
1173
- } else if (persistedDeadlineAt === null) {
1174
- state.currentNodeDeadlineAt = null;
994
+ } else if (persistedTimeoutMs === null) {
995
+ state.currentNodeTimeoutMs = null;
1175
996
  timeoutMs = null;
1176
997
  timeoutErrorMs = null;
1177
998
  } else {
1178
- const deadline = Date.parse(persistedDeadlineAt);
1179
- const started = Date.parse(state.currentNodeStartedAt ?? persistedDeadlineAt);
1180
- if (!Number.isFinite(deadline) || !Number.isFinite(started)) {
1181
- throw new Error("Persisted node deadline is invalid");
999
+ if (
1000
+ !Number.isFinite(persistedTimeoutMs) ||
1001
+ persistedTimeoutMs <= 0 ||
1002
+ !Number.isFinite(elapsedMs) ||
1003
+ elapsedMs < 0
1004
+ ) {
1005
+ throw new Error("Persisted node timeout is invalid");
1182
1006
  }
1183
- state.currentNodeDeadlineAt = persistedDeadlineAt;
1184
- timeoutMs = deadline - Date.now();
1185
- timeoutErrorMs = Math.max(1, deadline - started);
1007
+ state.currentNodeTimeoutMs = persistedTimeoutMs;
1008
+ state.currentNodeElapsedMs = elapsedMs;
1009
+ timeoutMs = persistedTimeoutMs - elapsedMs;
1010
+ timeoutErrorMs = persistedTimeoutMs;
1186
1011
  if (timeoutMs <= 0) throw new TimeoutError(timeoutErrorMs);
1187
1012
  }
1188
1013
  if (abort.signal.aborted) {
@@ -1333,13 +1158,40 @@ export class WorkflowEngine {
1333
1158
  }
1334
1159
  case "action":
1335
1160
  return await this.runActionNode(node, context, nodeId, attemptId, signal, meta);
1336
- case "checkpoint":
1337
- return await runCheckpointNode(node, context, {
1338
- store: this.store,
1339
- workflowName: workflow.name,
1340
- nodeId,
1341
- attemptId,
1342
- });
1161
+ case "checkpoint": {
1162
+ const saved = await this.store.readCheckpoint(state.runId, attemptId);
1163
+ if (saved !== undefined) {
1164
+ if (saved.request.status === "cancelled")
1165
+ throw new Error("Checkpoint request is cancelled");
1166
+ if (saved.request.status === "settled") {
1167
+ if (saved.humanDecision !== undefined) state.humanDecision = saved.humanDecision;
1168
+ return { output: saved.output, promptText: null };
1169
+ }
1170
+ } else {
1171
+ const request = await checkpointRequest(node, context, {
1172
+ workflowName: workflow.name,
1173
+ nodeId,
1174
+ attemptId,
1175
+ });
1176
+ const event = await this.store.commitTransition(state.runId, {
1177
+ kind: "waitForInput",
1178
+ ...request,
1179
+ event: {
1180
+ scope: "node",
1181
+ type: "checkpoint_requested",
1182
+ nodeId,
1183
+ attemptId,
1184
+ payload: { requestId: request.requestId, requestKind: request.requestKind },
1185
+ },
1186
+ });
1187
+ state.traceSeq = event.seq;
1188
+ state.updatedAt = event.at;
1189
+ }
1190
+ state.status = "waiting";
1191
+ state.waitingOn = nodeId;
1192
+ delete state.finishedAt;
1193
+ throw new RunParkedError();
1194
+ }
1343
1195
  }
1344
1196
  }
1345
1197
 
@@ -1414,11 +1266,12 @@ export class WorkflowEngine {
1414
1266
  signal: AbortSignal,
1415
1267
  settings?: WorkflowSettingsScopeRecord,
1416
1268
  ): WorkflowNodeContext {
1269
+ const snapshot = deepFreezeJson(structuredClone(state));
1417
1270
  return {
1418
- input: state.input,
1419
- outputs: state.outputs,
1420
- results: state.results,
1421
- state,
1271
+ input: snapshot.input,
1272
+ outputs: snapshot.outputs,
1273
+ results: snapshot.results,
1274
+ state: snapshot,
1422
1275
  ...(settings !== undefined
1423
1276
  ? {
1424
1277
  settings: deepFreezeJson(structuredClone(settings.settings)),
@@ -1468,13 +1321,17 @@ export class WorkflowEngine {
1468
1321
  // may already be terminal.
1469
1322
  throw abortError(signal);
1470
1323
  }
1471
- const prompt = appendStepContract(
1472
- basePrompt,
1473
- workflow.name,
1324
+ const contract: AgentStepContract = {
1325
+ requestId: workflowRequestId(state.runId, attemptId),
1326
+ runId: state.runId,
1327
+ workflowName: workflow.name,
1474
1328
  nodeId,
1475
1329
  attemptId,
1476
- node.expectedOutput,
1477
- );
1330
+ completion: assistant === undefined ? "submit" : "assistant",
1331
+ ...(typeof node.expectedOutput === "string" ? { expectedOutput: node.expectedOutput } : {}),
1332
+ ...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
1333
+ };
1334
+ const prompt = appendStepContract(basePrompt, contract);
1478
1335
  meta.promptText = prompt;
1479
1336
  await this.persist(runId, state, {
1480
1337
  scope: "agent",
@@ -1486,17 +1343,7 @@ export class WorkflowEngine {
1486
1343
 
1487
1344
  const submission = await this.executor.runAgentStep(
1488
1345
  {
1489
- contract: {
1490
- runId: state.runId,
1491
- workflowName: workflow.name,
1492
- nodeId,
1493
- attemptId,
1494
- completion: assistant === undefined ? "submit" : "assistant",
1495
- ...(typeof node.expectedOutput === "string"
1496
- ? { expectedOutput: node.expectedOutput }
1497
- : {}),
1498
- ...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
1499
- },
1346
+ contract,
1500
1347
  prompt,
1501
1348
  ...(state.runTitle !== undefined || node.statusDetail !== undefined
1502
1349
  ? {
@@ -1632,7 +1479,9 @@ export class WorkflowEngine {
1632
1479
  state: WorkflowRunState,
1633
1480
  event: WorkflowTraceEventDraft,
1634
1481
  ): Promise<void> {
1635
- const traceEvent = await this.store.writeSnapshot(runId, state, event);
1482
+ const traceEvent = await this.store.commitTransition(runId, executionTransition(state, event));
1483
+ state.traceSeq = traceEvent.seq;
1484
+ state.updatedAt = traceEvent.at;
1636
1485
  try {
1637
1486
  this.onEvent?.(traceEvent, state);
1638
1487
  } catch {
@@ -1645,8 +1494,8 @@ export class WorkflowEngine {
1645
1494
  private async finishRun(
1646
1495
  runId: string,
1647
1496
  state: WorkflowRunState,
1648
- status: WorkflowRunState["status"],
1649
- fields: { error?: string; waitingOn?: string; finalOutput?: unknown },
1497
+ status: "completed" | "failed" | "timed_out" | "cancelled",
1498
+ fields: { error?: string; finalOutput?: unknown },
1650
1499
  ): Promise<void> {
1651
1500
  if (status === "failed" && state.status === "timed_out") {
1652
1501
  status = "timed_out";
@@ -1656,50 +1505,38 @@ export class WorkflowEngine {
1656
1505
  if (fields.error !== undefined) {
1657
1506
  state.error = fields.error;
1658
1507
  }
1659
- if (fields.waitingOn !== undefined) {
1660
- state.waitingOn = fields.waitingOn;
1661
- }
1662
1508
  if (fields.finalOutput !== undefined) {
1663
1509
  state.finalOutput = fields.finalOutput;
1664
1510
  }
1665
1511
  delete state.currentNode;
1666
1512
  delete state.currentAttemptId;
1667
1513
  delete state.currentNodeStartedAt;
1668
- delete state.currentNodeDeadlineAt;
1514
+ delete state.currentNodeTimeoutMs;
1515
+ delete state.currentNodeElapsedMs;
1669
1516
  delete state.currentSettingsScopeId;
1670
1517
  delete state.currentSettingsChangeNumber;
1671
1518
  delete state.currentSettingsHash;
1672
- // Let observers stop, drain, and prepare terminal delivery from the final
1673
- // in-memory state before the immutable terminal event is committed.
1674
- try {
1675
- await this.onRunFinishing?.(runId, state);
1676
- } catch {
1677
- // Finishing the run wins over observer failures.
1678
- }
1679
1519
  await this.persist(runId, state, {
1680
1520
  scope: "run",
1681
1521
  type: `run_${status}`,
1682
1522
  payload: {
1683
1523
  status,
1684
1524
  ...(fields.error !== undefined ? { error: fields.error } : {}),
1685
- ...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
1686
1525
  ...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
1687
- presentationRequired: status === "completed" && this.presentationRequired,
1688
1526
  },
1689
1527
  });
1690
1528
  }
1691
1529
  }
1692
1530
 
1693
- async function runCheckpointNode(
1531
+ async function checkpointRequest(
1694
1532
  node: CheckpointNodeDefinition,
1695
1533
  context: WorkflowNodeContext,
1696
1534
  execution: {
1697
- store: WorkflowExecutionStore;
1698
1535
  workflowName: string;
1699
1536
  nodeId: string;
1700
1537
  attemptId: string;
1701
1538
  },
1702
- ): Promise<NodeExecution> {
1539
+ ): Promise<{ requestId: string; requestKind: "checkpoint" | "decision"; contract: JsonValue }> {
1703
1540
  if (node.humanDecision !== undefined) {
1704
1541
  const prompt = await node.humanDecision.request(context);
1705
1542
  const audience =
@@ -1719,11 +1556,18 @@ async function runCheckpointNode(
1719
1556
  prompt,
1720
1557
  ...(timeout !== undefined ? { timeout } : {}),
1721
1558
  });
1722
- await execution.store.createHumanDecisionRequest(request);
1723
- return { output: request, promptText: null };
1559
+ return {
1560
+ requestId: request.decisionId,
1561
+ requestKind: "decision",
1562
+ contract: request as unknown as JsonValue,
1563
+ };
1724
1564
  }
1725
1565
  const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
1726
- return { output, promptText: null };
1566
+ return {
1567
+ requestId: workflowRequestId(context.state.runId, execution.attemptId),
1568
+ requestKind: "checkpoint",
1569
+ contract: output as JsonValue,
1570
+ };
1727
1571
  }
1728
1572
 
1729
1573
  function shellReceipt(result: ShellActionResult): WorkflowActionReceipt {
@@ -1930,8 +1774,7 @@ export function workflowIdentityMismatch(
1930
1774
  const metadata = compositionMetadata(workflow);
1931
1775
  const currentSources = metadata?.sources ?? [];
1932
1776
  if (!isDeepStrictEqual(state.workflowSources ?? [], currentSources)) return true;
1933
- const currentDigest = metadata?.snapshot.mounts.length ? definitionDigest(workflow) : undefined;
1934
- return state.definitionDigest !== currentDigest;
1777
+ return state.definitionDigest !== definitionDigest(workflow);
1935
1778
  }
1936
1779
 
1937
1780
  function definitionDigest(workflow: WorkflowDefinition): string {
@@ -2040,15 +1883,9 @@ function settingsDefinitionForNode(
2040
1883
  * The step contract appended to every agent-node prompt. This is the
2041
1884
  * documented standard for how the model completes a workflow step.
2042
1885
  */
2043
- export function appendStepContract(
2044
- prompt: string,
2045
- workflowName: string,
2046
- nodeId: string,
2047
- attemptId: string,
2048
- expectedOutput: AgentExpectedOutput | undefined,
2049
- ): string {
2050
- const assistant = assistantMessageOutput(expectedOutput);
2051
- if (assistant !== undefined) {
1886
+ export function appendStepContract(prompt: string, contract: AgentStepContract): string {
1887
+ const { requestId, workflowName, nodeId, attemptId, expectedOutput } = contract;
1888
+ if (contract.completion === "assistant") {
2052
1889
  return [
2053
1890
  prompt.trimEnd(),
2054
1891
  "",
@@ -2058,8 +1895,8 @@ export function appendStepContract(
2058
1895
  "Reply with a normal assistant message.",
2059
1896
  "Do not call the workflow tool to complete this step.",
2060
1897
  "Your visible reply becomes the workflow step output after the turn settles.",
2061
- ...(assistant.maxChars !== undefined
2062
- ? [`Keep the visible reply within ${assistant.maxChars} characters.`]
1898
+ ...(contract.maxOutputChars !== undefined
1899
+ ? [`Keep the visible reply within ${contract.maxOutputChars} characters.`]
2063
1900
  : []),
2064
1901
  ].join("\n");
2065
1902
  }
@@ -2070,9 +1907,9 @@ export function appendStepContract(
2070
1907
  `Workflow step contract (workflow: ${workflowName}, step: ${nodeId}, attempt: ${attemptId})`,
2071
1908
  "",
2072
1909
  "While this step is active, you may publish non-completing updates with:",
2073
- `{"action": "update", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
1910
+ `{"action": "update", "requestId": ${JSON.stringify(requestId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
2074
1911
  "Complete this step by calling the `workflow` tool exactly once with:",
2075
- `{"action": "submit", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
1912
+ `{"action": "submit", "requestId": ${JSON.stringify(requestId)}, "output": <your result>}`,
2076
1913
  `Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
2077
1914
  "The step is complete only after the workflow tool accepts the output.",
2078
1915
  "If the tool reports a validation error, correct the output and call it again.",