@osolmaz/pi-workflows 0.11.2 → 0.12.1

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 (177) hide show
  1. package/README.md +15 -10
  2. package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
  3. package/dist/builtins/autoimplement-command-batches.js +258 -0
  4. package/dist/builtins/autoimplement-command-batches.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +509 -124
  6. package/dist/builtins/autoimplement.workflow.js +884 -327
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +5 -3
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +4 -1
  11. package/dist/builtins/index.js +3 -1
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/monitor.workflow.d.ts +2 -4
  14. package/dist/builtins/monitor.workflow.js +26 -128
  15. package/dist/builtins/monitor.workflow.js.map +1 -1
  16. package/dist/builtins/pi-agent-group.d.ts +72 -0
  17. package/dist/builtins/pi-agent-group.js +1087 -0
  18. package/dist/builtins/pi-agent-group.js.map +1 -0
  19. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  20. package/dist/builtins/plan-approval.workflow.js +92 -14
  21. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  22. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  23. package/dist/builtins/plan-change.workflow.js +256 -0
  24. package/dist/builtins/plan-change.workflow.js.map +1 -0
  25. package/dist/builtins/plan-presentation.js +2 -2
  26. package/dist/builtins/plan-presentation.js.map +1 -1
  27. package/dist/builtins/sanity-check.workflow.d.ts +85 -0
  28. package/dist/builtins/sanity-check.workflow.js +482 -0
  29. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  30. package/dist/controllers/sqlite.d.ts +90 -2
  31. package/dist/controllers/sqlite.js +380 -5
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.d.ts +2 -2
  34. package/dist/extension/decision-channels.js +29 -35
  35. package/dist/extension/decision-channels.js.map +1 -1
  36. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  37. package/dist/extension/deferred-turn-coordinator.js +143 -0
  38. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  39. package/dist/extension/deferred-turn.d.ts +44 -0
  40. package/dist/extension/deferred-turn.js +110 -0
  41. package/dist/extension/deferred-turn.js.map +1 -0
  42. package/dist/extension/index.js +348 -80
  43. package/dist/extension/index.js.map +1 -1
  44. package/dist/extension/session-events.d.ts +2 -2
  45. package/dist/extension/step-message.d.ts +1 -0
  46. package/dist/extension/step-message.js.map +1 -1
  47. package/dist/extension/widget.js +23 -3
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/herdr/setup.js +4 -4
  50. package/dist/render/graph-render.js +1 -2
  51. package/dist/render/graph-render.js.map +1 -1
  52. package/dist/viewer/render.js +7 -6
  53. package/dist/viewer/render.js.map +1 -1
  54. package/dist/workflows/catalog.js +7 -2
  55. package/dist/workflows/catalog.js.map +1 -1
  56. package/dist/workflows/command-batch.d.ts +38 -0
  57. package/dist/workflows/command-batch.js +176 -0
  58. package/dist/workflows/command-batch.js.map +1 -0
  59. package/dist/workflows/composition.js +8 -0
  60. package/dist/workflows/composition.js.map +1 -1
  61. package/dist/workflows/decision-presentation.d.ts +1 -1
  62. package/dist/workflows/decision-presentation.js +51 -38
  63. package/dist/workflows/decision-presentation.js.map +1 -1
  64. package/dist/workflows/engine.d.ts +2 -2
  65. package/dist/workflows/engine.js +22 -17
  66. package/dist/workflows/engine.js.map +1 -1
  67. package/dist/workflows/errors.d.ts +13 -0
  68. package/dist/workflows/errors.js +15 -0
  69. package/dist/workflows/errors.js.map +1 -1
  70. package/dist/workflows/human-decision.d.ts +16 -4
  71. package/dist/workflows/human-decision.js +175 -72
  72. package/dist/workflows/human-decision.js.map +1 -1
  73. package/dist/workflows/index.d.ts +3 -2
  74. package/dist/workflows/index.js +2 -1
  75. package/dist/workflows/index.js.map +1 -1
  76. package/dist/workflows/progress.d.ts +1 -0
  77. package/dist/workflows/progress.js +15 -3
  78. package/dist/workflows/progress.js.map +1 -1
  79. package/dist/workflows/schema.js +12 -1
  80. package/dist/workflows/schema.js.map +1 -1
  81. package/dist/workflows/shell.d.ts +4 -0
  82. package/dist/workflows/shell.js +6 -0
  83. package/dist/workflows/shell.js.map +1 -1
  84. package/dist/workflows/store.js +8 -1
  85. package/dist/workflows/store.js.map +1 -1
  86. package/dist/workflows/types.d.ts +37 -48
  87. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  88. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  89. package/docs/CONTROLLERS.md +2 -2
  90. package/docs/DEFERRED_TURNS.md +298 -0
  91. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  92. package/docs/HUMAN_DECISIONS.md +30 -40
  93. package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
  94. package/docs/MONITOR.md +7 -13
  95. package/docs/WORKFLOW_COMPOSITION.md +10 -9
  96. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  97. package/docs/WORKFLOW_UPDATES.md +28 -5
  98. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  99. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  100. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  101. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  102. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  103. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  104. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  105. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  106. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  107. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  108. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  109. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  110. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  111. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  112. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  113. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  114. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  115. package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
  116. package/docs/run-bundles.md +8 -6
  117. package/docs/workflows.md +85 -22
  118. package/examples/workflows/approved-plan.workflow.ts +19 -46
  119. package/examples/workflows/command-batch.workflow.ts +50 -0
  120. package/examples/workflows/sanity-check.workflow.ts +1 -0
  121. package/herdr-plugin.toml +3 -3
  122. package/package.json +7 -7
  123. package/schemas/decision-presentation-v1.schema.json +1 -1
  124. package/schemas/human-decision-accepted-v1.schema.json +16 -4
  125. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  126. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  127. package/schemas/human-decision-continuation-v1.schema.json +11 -2
  128. package/schemas/human-decision-delivery-v1.schema.json +9 -1
  129. package/schemas/human-decision-receipt-v1.schema.json +9 -1
  130. package/schemas/human-decision-request-v1.schema.json +25 -5
  131. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  132. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  133. package/skills/autodoc/SKILL.md +33 -2
  134. package/skills/autoimplement/SKILL.md +93 -12
  135. package/skills/autoplan/SKILL.md +26 -2
  136. package/skills/monitor/SKILL.md +66 -24
  137. package/skills/pi-workflows/SKILL.md +21 -6
  138. package/skills/sanity-check/SKILL.md +44 -0
  139. package/src/builtins/autoimplement-command-batches.ts +364 -0
  140. package/src/builtins/autoimplement.workflow.ts +1075 -370
  141. package/src/builtins/catalog.ts +5 -3
  142. package/src/builtins/index.ts +24 -0
  143. package/src/builtins/monitor.workflow.ts +27 -150
  144. package/src/builtins/pi-agent-group.ts +1407 -0
  145. package/src/builtins/plan-approval.workflow.ts +157 -24
  146. package/src/builtins/plan-change.workflow.ts +321 -0
  147. package/src/builtins/plan-presentation.ts +2 -2
  148. package/src/builtins/sanity-check.workflow.ts +769 -0
  149. package/src/controllers/sqlite.ts +580 -7
  150. package/src/extension/decision-channels.ts +36 -66
  151. package/src/extension/deferred-turn-coordinator.ts +171 -0
  152. package/src/extension/deferred-turn.ts +166 -0
  153. package/src/extension/index.ts +438 -90
  154. package/src/extension/session-events.ts +2 -2
  155. package/src/extension/step-message.ts +1 -0
  156. package/src/extension/widget.ts +24 -5
  157. package/src/herdr/setup.ts +4 -4
  158. package/src/render/graph-render.ts +1 -2
  159. package/src/viewer/render.ts +7 -6
  160. package/src/workflows/catalog.ts +7 -2
  161. package/src/workflows/command-batch.ts +254 -0
  162. package/src/workflows/composition.ts +9 -0
  163. package/src/workflows/decision-presentation.ts +56 -43
  164. package/src/workflows/engine.ts +25 -20
  165. package/src/workflows/errors.ts +24 -0
  166. package/src/workflows/human-decision.ts +218 -101
  167. package/src/workflows/index.ts +20 -11
  168. package/src/workflows/progress.ts +18 -3
  169. package/src/workflows/schema.ts +19 -1
  170. package/src/workflows/shell.ts +10 -0
  171. package/src/workflows/store.ts +8 -1
  172. package/src/workflows/types.ts +43 -59
  173. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  174. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  175. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  176. package/schemas/human-decision-request-v2.schema.json +0 -69
  177. package/schemas/human-decision-resolution-v2.schema.json +0 -27
@@ -56,7 +56,18 @@ export function decisionPresentationDigest(presentation: DecisionPresentation):
56
56
  export function validateHumanDecisionRequestIntegrity(
57
57
  request: HumanDecisionRequest,
58
58
  ): HumanDecisionRequest {
59
- if (request.schema === "pi-workflows.human-decision-request.v1") return request;
59
+ if (
60
+ request.schema !== "pi-workflows.human-decision-request.v1" ||
61
+ !Object.hasOwn(request, "subject") ||
62
+ !Object.hasOwn(request, "presentation") ||
63
+ !Object.hasOwn(request, "subjectDigest") ||
64
+ !Object.hasOwn(request, "presentationDigest") ||
65
+ !Object.hasOwn(request, "revision")
66
+ ) {
67
+ throw new Error(
68
+ "Human decision state uses an incompatible alpha contract; reset the affected workflow run and decision state.",
69
+ );
70
+ }
60
71
  const presentation = normalizeDecisionPresentation(request.presentation);
61
72
  const subjectDigest = digestCanonical(request.subject);
62
73
  const presentationDigest = digestCanonical(presentation);
@@ -70,6 +81,7 @@ export function validateHumanDecisionRequestIntegrity(
70
81
  title: request.title,
71
82
  choices: request.choices,
72
83
  ...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}),
84
+ ...(request.defaultResponse !== undefined ? { defaultResponse: request.defaultResponse } : {}),
73
85
  subject: request.subject,
74
86
  presentation,
75
87
  revision: request.revision,
@@ -90,10 +102,7 @@ export function humanDecisionChannelRequest(
90
102
  request: HumanDecisionRequest,
91
103
  ): HumanDecisionChannelRequest {
92
104
  validateHumanDecisionRequestIntegrity(request);
93
- const presentation =
94
- request.schema === "pi-workflows.human-decision-request.v2"
95
- ? normalizeDecisionPresentation(request.presentation)
96
- : legacyDecisionPresentation(request.body);
105
+ const presentation = normalizeDecisionPresentation(request.presentation);
97
106
  const presentationDigest = decisionPresentationDigest(presentation);
98
107
  return {
99
108
  schema: "pi-workflows.human-decision-channel-request.v1",
@@ -108,25 +117,26 @@ export function humanDecisionChannelRequest(
108
117
  title: request.title,
109
118
  presentation,
110
119
  presentationDigest,
111
- revision: request.schema === "pi-workflows.human-decision-request.v2" ? request.revision : 1,
120
+ revision: request.revision,
112
121
  choices: request.choices,
113
122
  createdAt: request.createdAt,
114
123
  ...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}),
124
+ ...(request.defaultResponse !== undefined ? { defaultResponse: request.defaultResponse } : {}),
115
125
  };
116
126
  }
117
127
 
118
- export function legacyDecisionPresentation(body: unknown): DecisionPresentation {
128
+ export function valueDecisionPresentation(value: unknown): DecisionPresentation {
119
129
  const blocks: DecisionPresentationBlock[] = [];
120
- if (typeof body === "string") {
121
- appendLegacyText(blocks, body, "Decision details");
130
+ if (typeof value === "string") {
131
+ appendValueText(blocks, value, "Decision details");
122
132
  } else {
123
- appendLegacyValue(blocks, body, undefined);
133
+ appendValue(blocks, value, undefined);
124
134
  }
125
135
  const summary =
126
- typeof body === "string"
136
+ typeof value === "string"
127
137
  ? "Review the decision message below."
128
138
  : "Review the decision details below.";
129
- return normalizeDecisionPresentation(boundLegacyPresentation(summary, blocks, body));
139
+ return normalizeDecisionPresentation(boundValuePresentation(summary, blocks, value));
130
140
  }
131
141
 
132
142
  export function decisionDocumentSegments(
@@ -150,6 +160,13 @@ export function decisionDocumentSegments(
150
160
  segments.push({ kind: block.kind, text: block.text });
151
161
  }
152
162
  }
163
+ if (request.expiresAt !== undefined && request.defaultResponse !== undefined) {
164
+ const selected = request.choices[request.defaultResponse.choice];
165
+ segments.push({
166
+ kind: "paragraph",
167
+ text: `If no answer is accepted by ${request.expiresAt}, this decision continues automatically with: ${selected?.label ?? request.defaultResponse.choice}.`,
168
+ });
169
+ }
153
170
  const choices = Object.values(request.choices).map((choice) =>
154
171
  choice.input === undefined
155
172
  ? `• ${choice.label}`
@@ -251,10 +268,10 @@ function normalizeString(value: unknown, label: string): string {
251
268
  return text;
252
269
  }
253
270
 
254
- function boundLegacyPresentation(
271
+ function boundValuePresentation(
255
272
  summary: string,
256
273
  blocks: DecisionPresentationBlock[],
257
- body: unknown,
274
+ value: unknown,
258
275
  ): DecisionPresentation {
259
276
  const candidate: DecisionPresentation = {
260
277
  schema: "pi-workflows.decision-presentation.v1",
@@ -263,13 +280,13 @@ function boundLegacyPresentation(
263
280
  };
264
281
  if (
265
282
  blocks.length <= MAX_PRESENTATION_BLOCKS &&
266
- blocks.every(legacyBlockFitsSchema) &&
283
+ blocks.every(valueBlockFitsSchema) &&
267
284
  presentationCodeUnits(candidate) <= MAX_PRESENTATION_CODE_UNITS
268
285
  ) {
269
286
  return candidate;
270
287
  }
271
- const originalCodeUnits = canonicalJson(body).length;
272
- const omission = `Some legacy decision content is not shown because it exceeds the readable presentation limit. Inspect the durable v1 request before answering. Body digest: ${digestCanonical(body)}. Original body: ${originalCodeUnits} UTF-16 code units. Generated blocks: ${blocks.length}.`;
288
+ const originalCodeUnits = canonicalJson(value).length;
289
+ const omission = `Some decision content is not shown because it exceeds the readable presentation limit. Inspect the canonical source value before answering. Value digest: ${digestCanonical(value)}. Original value: ${originalCodeUnits} UTF-16 code units. Generated blocks: ${blocks.length}.`;
273
290
  const budget = MAX_PRESENTATION_CODE_UNITS - summary.length - omission.length;
274
291
  const kept: DecisionPresentationBlock[] = [];
275
292
  let used = 0;
@@ -278,7 +295,7 @@ function boundLegacyPresentation(
278
295
  const units = presentationBlockCodeUnits(block);
279
296
  if (
280
297
  kept.length >= MAX_PRESENTATION_BLOCKS ||
281
- !legacyBlockFitsSchema(block) ||
298
+ !valueBlockFitsSchema(block) ||
282
299
  used + units > budget
283
300
  ) {
284
301
  omitted = true;
@@ -298,7 +315,7 @@ function boundLegacyPresentation(
298
315
  };
299
316
  }
300
317
 
301
- function legacyBlockFitsSchema(block: DecisionPresentationBlock): boolean {
318
+ function valueBlockFitsSchema(block: DecisionPresentationBlock): boolean {
302
319
  if (block.kind === "paragraph" || block.kind === "preformatted") {
303
320
  return block.text.length <= MAX_PRESENTATION_STRING_CODE_UNITS;
304
321
  }
@@ -332,47 +349,43 @@ function presentationCodeUnits(presentation: DecisionPresentation): number {
332
349
  return total;
333
350
  }
334
351
 
335
- function appendLegacyValue(
352
+ function appendValue(
336
353
  blocks: DecisionPresentationBlock[],
337
354
  value: unknown,
338
355
  label: string | undefined,
339
356
  ): void {
340
357
  if (isScalar(value)) {
341
- const scalar = legacyScalar(value);
358
+ const scalar = valueScalar(value);
342
359
  if (label === undefined || scalar.length > MAX_PRESENTATION_STRING_CODE_UNITS) {
343
- appendLegacyText(
344
- blocks,
345
- scalar,
346
- label === undefined ? "Decision details" : legacyLabel(label),
347
- );
360
+ appendValueText(blocks, scalar, label === undefined ? "Decision details" : valueLabel(label));
348
361
  } else {
349
- blocks.push({ kind: "fields", items: [{ label: legacyLabel(label), value: scalar }] });
362
+ blocks.push({ kind: "fields", items: [{ label: valueLabel(label), value: scalar }] });
350
363
  }
351
364
  return;
352
365
  }
353
366
  if (Array.isArray(value)) {
354
- if (label !== undefined) blocks.push({ kind: "section", title: legacyLabel(label) });
367
+ if (label !== undefined) blocks.push({ kind: "section", title: valueLabel(label) });
355
368
  if (value.length === 0) {
356
369
  blocks.push({ kind: "paragraph", text: "None" });
357
370
  return;
358
371
  }
359
372
  if (
360
373
  value.every(isScalar) &&
361
- value.every((item) => legacyScalar(item).length <= MAX_PRESENTATION_STRING_CODE_UNITS)
374
+ value.every((item) => valueScalar(item).length <= MAX_PRESENTATION_STRING_CODE_UNITS)
362
375
  ) {
363
- const items = value.map(legacyScalar);
376
+ const items = value.map(valueScalar);
364
377
  for (let index = 0; index < items.length; index += MAX_PRESENTATION_ITEMS) {
365
378
  blocks.push({ kind: "bullets", items: items.slice(index, index + MAX_PRESENTATION_ITEMS) });
366
379
  }
367
380
  return;
368
381
  }
369
- value.forEach((item, index) => appendLegacyValue(blocks, item, `Item ${index + 1}`));
382
+ value.forEach((item, index) => appendValue(blocks, item, `Item ${index + 1}`));
370
383
  return;
371
384
  }
372
385
  const entries = Object.entries(value as Record<string, unknown>).sort(([left], [right]) =>
373
386
  compareKeys(left, right),
374
387
  );
375
- if (label !== undefined) blocks.push({ kind: "section", title: legacyLabel(label) });
388
+ if (label !== undefined) blocks.push({ kind: "section", title: valueLabel(label) });
376
389
  if (entries.length === 0) {
377
390
  blocks.push({ kind: "paragraph", text: "None" });
378
391
  return;
@@ -380,34 +393,34 @@ function appendLegacyValue(
380
393
  const fields = entries
381
394
  .filter(
382
395
  ([, child]) =>
383
- isScalar(child) && legacyScalar(child).length <= MAX_PRESENTATION_STRING_CODE_UNITS,
396
+ isScalar(child) && valueScalar(child).length <= MAX_PRESENTATION_STRING_CODE_UNITS,
384
397
  )
385
- .map(([key, child]) => ({ label: legacyLabel(key), value: legacyScalar(child) }));
398
+ .map(([key, child]) => ({ label: valueLabel(key), value: valueScalar(child) }));
386
399
  for (let index = 0; index < fields.length; index += MAX_PRESENTATION_ITEMS) {
387
400
  blocks.push({ kind: "fields", items: fields.slice(index, index + MAX_PRESENTATION_ITEMS) });
388
401
  }
389
402
  for (const [key, child] of entries.filter(
390
403
  ([, child]) =>
391
- !isScalar(child) || legacyScalar(child).length > MAX_PRESENTATION_STRING_CODE_UNITS,
404
+ !isScalar(child) || valueScalar(child).length > MAX_PRESENTATION_STRING_CODE_UNITS,
392
405
  )) {
393
- appendLegacyValue(blocks, child, key);
406
+ appendValue(blocks, child, key);
394
407
  }
395
408
  }
396
409
 
397
- function appendLegacyText(
410
+ function appendValueText(
398
411
  blocks: DecisionPresentationBlock[],
399
412
  value: string,
400
413
  section: string,
401
414
  ): void {
402
- const text = sanitizeLegacyText(value);
415
+ const text = sanitizeValueText(value);
403
416
  blocks.push({ kind: "section", title: section });
404
417
  for (const part of splitByCodeUnits(text, MAX_PRESENTATION_STRING_CODE_UNITS)) {
405
418
  blocks.push({ kind: "paragraph", text: part.trim().length === 0 ? "None" : part });
406
419
  }
407
420
  }
408
421
 
409
- function legacyLabel(value: string): string {
410
- const sanitized = sanitizeLegacyText(value)
422
+ function valueLabel(value: string): string {
423
+ const sanitized = sanitizeValueText(value)
411
424
  .replaceAll(/[_-]+/gu, " ")
412
425
  .replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2")
413
426
  .replaceAll(/\s+/gu, " ")
@@ -416,13 +429,13 @@ function legacyLabel(value: string): string {
416
429
  return `${sanitized[0]!.toUpperCase()}${sanitized.slice(1)}`;
417
430
  }
418
431
 
419
- function legacyScalar(value: unknown): string {
432
+ function valueScalar(value: unknown): string {
420
433
  if (value === null) return "None";
421
434
  if (typeof value === "boolean") return value ? "Yes" : "No";
422
- return sanitizeLegacyText(String(value));
435
+ return sanitizeValueText(String(value));
423
436
  }
424
437
 
425
- function sanitizeLegacyText(value: string): string {
438
+ function sanitizeValueText(value: string): string {
426
439
  return value
427
440
  .replaceAll("\r\n", "\n")
428
441
  .replaceAll("\r", "\n")
@@ -32,7 +32,7 @@ import {
32
32
  readRunBundle,
33
33
  } from "./store.js";
34
34
  import type {
35
- AcceptedHumanDecision,
35
+ ResolvedHumanDecision,
36
36
  AgentNodeDefinition,
37
37
  AgentStepExecutor,
38
38
  ActionNodeDefinition,
@@ -377,7 +377,7 @@ export class WorkflowEngine {
377
377
  workflowSource?: WorkflowSource;
378
378
  runId?: string;
379
379
  force?: boolean;
380
- humanDecision?: AcceptedHumanDecision;
380
+ humanDecision?: ResolvedHumanDecision;
381
381
  } = {},
382
382
  ): Promise<WorkflowRunResult> {
383
383
  workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
@@ -412,14 +412,13 @@ export class WorkflowEngine {
412
412
  }
413
413
  const request = parent.state.finalOutput as HumanDecisionRequest;
414
414
  if (
415
- (request?.schema !== "pi-workflows.human-decision-request.v1" &&
416
- request?.schema !== "pi-workflows.human-decision-request.v2") ||
415
+ request?.schema !== "pi-workflows.human-decision-request.v1" ||
417
416
  request.decisionId !== options.humanDecision.decisionId ||
418
417
  request.requestDigest !== options.humanDecision.requestDigest
419
418
  ) {
420
419
  throw new Error("Accepted human decision does not match the waiting request");
421
420
  }
422
- const durableDecision = await new HumanDecisionStore(this.store.outputRoot).readAccepted(
421
+ const durableDecision = await new HumanDecisionStore(this.store.outputRoot).readResolved(
423
422
  request.decisionId,
424
423
  );
425
424
  if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
@@ -464,19 +463,17 @@ export class WorkflowEngine {
464
463
  requestDigest: options.humanDecision.requestDigest,
465
464
  nodeId: acceptedNodeId ?? waitingNodeId,
466
465
  response: options.humanDecision.response,
466
+ provenance: options.humanDecision.provenance,
467
467
  acceptedAt: options.humanDecision.acceptedAt,
468
468
  answerDigest: options.humanDecision.answerDigest,
469
469
  };
470
- state.humanDecision =
471
- options.humanDecision.schema === "pi-workflows.human-decision-accepted.v2"
472
- ? {
473
- schema: "pi-workflows.human-decision-receipt.v2",
474
- ...receipt,
475
- subjectDigest: options.humanDecision.subjectDigest,
476
- presentationDigest: options.humanDecision.presentationDigest,
477
- revision: options.humanDecision.revision,
478
- }
479
- : { schema: "pi-workflows.human-decision-receipt.v1", ...receipt };
470
+ state.humanDecision = {
471
+ schema: "pi-workflows.human-decision-receipt.v1",
472
+ ...receipt,
473
+ subjectDigest: options.humanDecision.subjectDigest,
474
+ presentationDigest: options.humanDecision.presentationDigest,
475
+ revision: options.humanDecision.revision,
476
+ };
480
477
  state.outputs[waitingNodeId] = acceptedResponse;
481
478
  const priorResult = state.results[waitingNodeId];
482
479
  if (priorResult === undefined) {
@@ -983,9 +980,11 @@ export class WorkflowEngine {
983
980
  if (abort.signal.aborted) {
984
981
  throw abortError(abort.signal);
985
982
  }
986
- timer = setTimeout(() => {
987
- abort.abort(new TimeoutError(timeoutMs));
988
- }, timeoutMs);
983
+ if (timeoutMs !== null) {
984
+ timer = setTimeout(() => {
985
+ abort.abort(new TimeoutError(timeoutMs));
986
+ }, timeoutMs);
987
+ }
989
988
  this.activeAttempt = { runDir, state, nodeId, attemptId, signal: abort.signal };
990
989
  const dispatched = this.dispatchNode(
991
990
  workflow,
@@ -1047,8 +1046,9 @@ export class WorkflowEngine {
1047
1046
  node: WorkflowNodeDefinition,
1048
1047
  context: WorkflowNodeContext,
1049
1048
  abort: AbortController,
1050
- ): Promise<number> {
1049
+ ): Promise<number | null> {
1051
1050
  const configured = node.timeoutMs;
1051
+ if (configured === null) return null;
1052
1052
  if (typeof configured !== "function") {
1053
1053
  return assertValidTimeout(configured ?? this.defaultNodeTimeoutMs);
1054
1054
  }
@@ -1061,7 +1061,7 @@ export class WorkflowEngine {
1061
1061
  Promise.resolve(configured(context)),
1062
1062
  abortRejection(abort.signal),
1063
1063
  ]);
1064
- return assertValidTimeout(resolved);
1064
+ return resolved === null ? null : assertValidTimeout(resolved);
1065
1065
  } finally {
1066
1066
  clearTimeout(timer);
1067
1067
  }
@@ -1314,6 +1314,10 @@ async function runCheckpointNode(
1314
1314
  typeof node.humanDecision.audience === "function"
1315
1315
  ? await node.humanDecision.audience(context)
1316
1316
  : node.humanDecision.audience;
1317
+ const timeout =
1318
+ typeof node.humanDecision.onTimeout === "function"
1319
+ ? await node.humanDecision.onTimeout(context)
1320
+ : node.humanDecision.onTimeout;
1317
1321
  const request = createHumanDecisionRequest({
1318
1322
  runId: context.state.runId,
1319
1323
  workflowName: execution.workflowName,
@@ -1321,6 +1325,7 @@ async function runCheckpointNode(
1321
1325
  attemptId: execution.attemptId,
1322
1326
  contract: { audience, choices: node.humanDecision.choices },
1323
1327
  prompt,
1328
+ ...(timeout !== undefined ? { timeout } : {}),
1324
1329
  });
1325
1330
  await new HumanDecisionStore(execution.store.outputRoot).createRequest(request);
1326
1331
  return { output: request, promptText: null };
@@ -56,6 +56,30 @@ export class WorkflowSourceChangedError extends Error {
56
56
  }
57
57
  }
58
58
 
59
+ /** A built-in revision is unavailable, so its incompatible run must restart. */
60
+ export class BuiltinWorkflowRevisionChangedError extends Error {
61
+ readonly runId: string;
62
+ readonly workflowId: string;
63
+ readonly previousRevision: string;
64
+ readonly currentRevision: string;
65
+
66
+ constructor(options: {
67
+ runId: string;
68
+ workflowId: string;
69
+ previousRevision: string;
70
+ currentRevision: string;
71
+ }) {
72
+ super(
73
+ `Built-in workflow ${options.workflowId} revision ${options.previousRevision} cannot resume run ${options.runId} with installed revision ${options.currentRevision}; cancel run ${options.runId}, then start ${options.workflowId} again`,
74
+ );
75
+ this.name = "BuiltinWorkflowRevisionChangedError";
76
+ this.runId = options.runId;
77
+ this.workflowId = options.workflowId;
78
+ this.previousRevision = options.previousRevision;
79
+ this.currentRevision = options.currentRevision;
80
+ }
81
+ }
82
+
59
83
  export function isClaimLostError(error: unknown): error is ClaimLostError {
60
84
  return error instanceof ClaimLostError;
61
85
  }