@osolmaz/pi-workflows 0.12.0 → 0.13.0

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 (257) hide show
  1. package/README.md +56 -31
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +797 -142
  4. package/dist/builtins/autoimplement.workflow.js +17 -105
  5. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  6. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  7. package/dist/builtins/autoplan.workflow.js +200 -29
  8. package/dist/builtins/autoplan.workflow.js.map +1 -1
  9. package/dist/builtins/catalog.js +7 -5
  10. package/dist/builtins/catalog.js.map +1 -1
  11. package/dist/builtins/index.d.ts +4 -2
  12. package/dist/builtins/index.js +3 -1
  13. package/dist/builtins/index.js.map +1 -1
  14. package/dist/builtins/monitor.workflow.d.ts +56 -23
  15. package/dist/builtins/monitor.workflow.js +508 -329
  16. package/dist/builtins/monitor.workflow.js.map +1 -1
  17. package/dist/builtins/pi-agent-group.d.ts +72 -0
  18. package/dist/builtins/pi-agent-group.js +1087 -0
  19. package/dist/builtins/pi-agent-group.js.map +1 -0
  20. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  21. package/dist/builtins/plain-summary.workflow.js +134 -0
  22. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  23. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  24. package/dist/builtins/plan-approval.workflow.js +92 -14
  25. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  26. package/dist/builtins/plan-change.workflow.d.ts +449 -0
  27. package/dist/builtins/plan-change.workflow.js +256 -0
  28. package/dist/builtins/plan-change.workflow.js.map +1 -0
  29. package/dist/builtins/plan-presentation.js +2 -2
  30. package/dist/builtins/plan-presentation.js.map +1 -1
  31. package/dist/builtins/sanity-check.workflow.d.ts +50 -6
  32. package/dist/builtins/sanity-check.workflow.js +150 -28
  33. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  34. package/dist/controllers/effects.d.ts +3 -2
  35. package/dist/controllers/effects.js +8 -1
  36. package/dist/controllers/effects.js.map +1 -1
  37. package/dist/controllers/index.d.ts +1 -1
  38. package/dist/controllers/index.js +1 -1
  39. package/dist/controllers/index.js.map +1 -1
  40. package/dist/controllers/manager.d.ts +1 -0
  41. package/dist/controllers/manager.js +41 -17
  42. package/dist/controllers/manager.js.map +1 -1
  43. package/dist/controllers/sqlite.d.ts +107 -85
  44. package/dist/controllers/sqlite.js +1611 -1486
  45. package/dist/controllers/sqlite.js.map +1 -1
  46. package/dist/controllers/store.d.ts +9 -8
  47. package/dist/controllers/store.js +1 -31
  48. package/dist/controllers/store.js.map +1 -1
  49. package/dist/controllers/types.d.ts +3 -0
  50. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  51. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  52. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  53. package/dist/controllers/workflows.d.ts +2 -2
  54. package/dist/controllers/workflows.js +11 -10
  55. package/dist/controllers/workflows.js.map +1 -1
  56. package/dist/extension/controller-host.js +2 -2
  57. package/dist/extension/controller-host.js.map +1 -1
  58. package/dist/extension/decision-channels.d.ts +4 -2
  59. package/dist/extension/decision-channels.js +333 -146
  60. package/dist/extension/decision-channels.js.map +1 -1
  61. package/dist/extension/executor.d.ts +9 -2
  62. package/dist/extension/executor.js +89 -2
  63. package/dist/extension/executor.js.map +1 -1
  64. package/dist/extension/herdr-viewer.d.ts +0 -1
  65. package/dist/extension/herdr-viewer.js +0 -4
  66. package/dist/extension/herdr-viewer.js.map +1 -1
  67. package/dist/extension/index.js +209 -127
  68. package/dist/extension/index.js.map +1 -1
  69. package/dist/extension/recorder.d.ts +3 -10
  70. package/dist/extension/recorder.js +13 -22
  71. package/dist/extension/recorder.js.map +1 -1
  72. package/dist/extension/session-events.d.ts +2 -2
  73. package/dist/extension/step-message.d.ts +7 -2
  74. package/dist/extension/step-message.js +94 -2
  75. package/dist/extension/step-message.js.map +1 -1
  76. package/dist/extension/widget.js +29 -3
  77. package/dist/extension/widget.js.map +1 -1
  78. package/dist/host/rpc-executor.d.ts +3 -0
  79. package/dist/host/rpc-executor.js +2 -0
  80. package/dist/host/rpc-executor.js.map +1 -1
  81. package/dist/host/runner.d.ts +3 -6
  82. package/dist/host/runner.js +83 -32
  83. package/dist/host/runner.js.map +1 -1
  84. package/dist/render/graph-render.js +15 -10
  85. package/dist/render/graph-render.js.map +1 -1
  86. package/dist/state/database.d.ts +35 -0
  87. package/dist/state/database.js +287 -0
  88. package/dist/state/database.js.map +1 -0
  89. package/dist/state/index.d.ts +4 -0
  90. package/dist/state/index.js +5 -0
  91. package/dist/state/index.js.map +1 -0
  92. package/dist/state/json.d.ts +6 -0
  93. package/dist/state/json.js +38 -0
  94. package/dist/state/json.js.map +1 -0
  95. package/dist/state/mutation.d.ts +57 -0
  96. package/dist/state/mutation.js +255 -0
  97. package/dist/state/mutation.js.map +1 -0
  98. package/dist/state/schema.d.ts +6 -0
  99. package/dist/state/schema.js +473 -0
  100. package/dist/state/schema.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +2 -2
  102. package/dist/viewer/cli.js +142 -122
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/viewer/render.d.ts +4 -4
  105. package/dist/viewer/render.js +9 -23
  106. package/dist/viewer/render.js.map +1 -1
  107. package/dist/viewer/session-reducer.d.ts +1 -1
  108. package/dist/viewer/session-reducer.js +1 -1
  109. package/dist/viewer/tui.d.ts +3 -3
  110. package/dist/viewer/tui.js +15 -13
  111. package/dist/viewer/tui.js.map +1 -1
  112. package/dist/viewer/watch.d.ts +2 -5
  113. package/dist/viewer/watch.js +13 -13
  114. package/dist/viewer/watch.js.map +1 -1
  115. package/dist/workflows/catalog.js +7 -2
  116. package/dist/workflows/catalog.js.map +1 -1
  117. package/dist/workflows/composition.js +20 -2
  118. package/dist/workflows/composition.js.map +1 -1
  119. package/dist/workflows/decision-presentation.d.ts +1 -1
  120. package/dist/workflows/decision-presentation.js +51 -38
  121. package/dist/workflows/decision-presentation.js.map +1 -1
  122. package/dist/workflows/definition.d.ts +6 -2
  123. package/dist/workflows/definition.js +17 -0
  124. package/dist/workflows/definition.js.map +1 -1
  125. package/dist/workflows/engine.d.ts +7 -7
  126. package/dist/workflows/engine.js +196 -115
  127. package/dist/workflows/engine.js.map +1 -1
  128. package/dist/workflows/errors.d.ts +16 -3
  129. package/dist/workflows/errors.js +18 -3
  130. package/dist/workflows/errors.js.map +1 -1
  131. package/dist/workflows/human-decision.d.ts +45 -8
  132. package/dist/workflows/human-decision.js +641 -274
  133. package/dist/workflows/human-decision.js.map +1 -1
  134. package/dist/workflows/index.d.ts +5 -6
  135. package/dist/workflows/index.js +4 -5
  136. package/dist/workflows/index.js.map +1 -1
  137. package/dist/workflows/progress.d.ts +1 -0
  138. package/dist/workflows/progress.js +15 -3
  139. package/dist/workflows/progress.js.map +1 -1
  140. package/dist/workflows/schema.js +27 -1
  141. package/dist/workflows/schema.js.map +1 -1
  142. package/dist/workflows/store.d.ts +83 -103
  143. package/dist/workflows/store.js +907 -951
  144. package/dist/workflows/store.js.map +1 -1
  145. package/dist/workflows/types.d.ts +88 -126
  146. package/docs/CONTROLLERS.md +10 -8
  147. package/docs/DEFERRED_TURNS.md +2 -2
  148. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  149. package/docs/HUMAN_DECISIONS.md +36 -47
  150. package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
  151. package/docs/MONITOR.md +179 -237
  152. package/docs/SQLITE_STATE.md +219 -0
  153. package/docs/WORKFLOW_COMPOSITION.md +27 -26
  154. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  155. package/docs/WORKFLOW_UPDATES.md +10 -9
  156. package/docs/development.md +9 -9
  157. package/docs/live-replay-protocol.md +28 -37
  158. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  159. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  160. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  161. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  162. package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
  163. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  164. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  165. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  166. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  167. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  168. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  169. package/docs/session-event-journal.md +63 -434
  170. package/docs/tui-viewer.md +13 -15
  171. package/docs/workflows.md +127 -53
  172. package/examples/workflows/approved-plan.workflow.ts +19 -46
  173. package/examples/workflows/plain-summary.workflow.ts +1 -0
  174. package/herdr-plugin.toml +1 -1
  175. package/package.json +7 -7
  176. package/plugins/herdr/viewer.mjs +1 -11
  177. package/schemas/human-decision-accepted-v1.schema.json +15 -3
  178. package/schemas/human-decision-continuation-v1.schema.json +10 -1
  179. package/schemas/human-decision-delivery-v1.schema.json +8 -0
  180. package/schemas/human-decision-receipt-v1.schema.json +8 -0
  181. package/schemas/human-decision-request-v1.schema.json +24 -4
  182. package/skills/autoimplement/SKILL.md +27 -0
  183. package/skills/autoplan/SKILL.md +5 -4
  184. package/skills/monitor/SKILL.md +91 -135
  185. package/skills/pi-workflows/SKILL.md +9 -8
  186. package/skills/sanity-check/SKILL.md +44 -0
  187. package/src/builtins/autoimplement.workflow.ts +19 -118
  188. package/src/builtins/autoplan.workflow.ts +290 -40
  189. package/src/builtins/catalog.ts +7 -5
  190. package/src/builtins/index.ts +18 -1
  191. package/src/builtins/monitor.workflow.ts +673 -382
  192. package/src/builtins/pi-agent-group.ts +1407 -0
  193. package/src/builtins/plain-summary.workflow.ts +185 -0
  194. package/src/builtins/plan-approval.workflow.ts +157 -24
  195. package/src/builtins/plan-change.workflow.ts +321 -0
  196. package/src/builtins/plan-presentation.ts +2 -2
  197. package/src/builtins/sanity-check.workflow.ts +248 -48
  198. package/src/controllers/effects.ts +7 -0
  199. package/src/controllers/index.ts +0 -6
  200. package/src/controllers/manager.ts +72 -32
  201. package/src/controllers/sqlite.ts +2420 -1948
  202. package/src/controllers/store.ts +17 -38
  203. package/src/controllers/types.ts +3 -0
  204. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  205. package/src/controllers/workflows.ts +45 -21
  206. package/src/extension/controller-host.ts +1 -2
  207. package/src/extension/decision-channels.ts +456 -203
  208. package/src/extension/executor.ts +105 -2
  209. package/src/extension/herdr-viewer.ts +0 -5
  210. package/src/extension/index.ts +251 -151
  211. package/src/extension/recorder.ts +13 -22
  212. package/src/extension/session-events.ts +2 -2
  213. package/src/extension/step-message.ts +106 -4
  214. package/src/extension/widget.ts +32 -5
  215. package/src/host/rpc-executor.ts +4 -0
  216. package/src/host/runner.ts +93 -41
  217. package/src/render/graph-render.ts +12 -6
  218. package/src/state/database.ts +375 -0
  219. package/src/state/index.ts +29 -0
  220. package/src/state/json.ts +42 -0
  221. package/src/state/mutation.ts +435 -0
  222. package/src/state/schema.ts +475 -0
  223. package/src/viewer/cli.ts +151 -144
  224. package/src/viewer/render.ts +19 -30
  225. package/src/viewer/session-reducer.ts +1 -1
  226. package/src/viewer/tui.ts +18 -17
  227. package/src/viewer/watch.ts +13 -17
  228. package/src/workflows/catalog.ts +7 -2
  229. package/src/workflows/composition.ts +21 -3
  230. package/src/workflows/decision-presentation.ts +56 -43
  231. package/src/workflows/definition.ts +36 -3
  232. package/src/workflows/engine.ts +228 -130
  233. package/src/workflows/errors.ts +27 -3
  234. package/src/workflows/human-decision.ts +921 -349
  235. package/src/workflows/index.ts +20 -30
  236. package/src/workflows/progress.ts +18 -3
  237. package/src/workflows/schema.ts +36 -1
  238. package/src/workflows/store.ts +1300 -1163
  239. package/src/workflows/types.ts +100 -137
  240. package/dist/builtins/sanity-check-session.d.ts +0 -17
  241. package/dist/builtins/sanity-check-session.js +0 -168
  242. package/dist/builtins/sanity-check-session.js.map +0 -1
  243. package/dist/workflows/artifacts.d.ts +0 -40
  244. package/dist/workflows/artifacts.js +0 -155
  245. package/dist/workflows/artifacts.js.map +0 -1
  246. package/dist/workflows/migrate-sources.d.ts +0 -42
  247. package/dist/workflows/migrate-sources.js +0 -133
  248. package/dist/workflows/migrate-sources.js.map +0 -1
  249. package/docs/run-bundles.md +0 -481
  250. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  251. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  252. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  253. package/schemas/human-decision-request-v2.schema.json +0 -69
  254. package/schemas/human-decision-resolution-v2.schema.json +0 -27
  255. package/src/builtins/sanity-check-session.ts +0 -205
  256. package/src/workflows/artifacts.ts +0 -188
  257. package/src/workflows/migrate-sources.ts +0 -178
package/src/viewer/tui.ts CHANGED
@@ -1,21 +1,20 @@
1
- import { listRunBundles, readRunBundle } from "../workflows/store.js";
2
- import type { LoadedRunBundle } from "../workflows/store.js";
1
+ import { WorkflowRunStore, type LoadedWorkflowRun } from "../workflows/store.js";
3
2
  import {
4
3
  maxDetailScroll,
5
4
  renderRunDetailLines,
6
5
  renderRunListLines,
7
6
  type ViewportSize,
8
7
  } from "./render.js";
9
- import { watchRunsDir } from "./watch.js";
8
+ import { watchStateDatabase } from "./watch.js";
10
9
 
11
10
  const ALT_SCREEN_ON = "\u001b[?1049h\u001b[?25l";
12
11
  const ALT_SCREEN_OFF = "\u001b[?25h\u001b[?1049l";
13
12
  const CLEAR = "\u001b[2J\u001b[H";
14
13
 
15
- type ViewerMode = { view: "list" } | { view: "detail"; runDir: string };
14
+ type ViewerMode = { view: "list" } | { view: "detail"; runId: string };
16
15
 
17
16
  export type ViewerOptions = {
18
- runsDir: string;
17
+ databasePath: string;
19
18
  runId?: string | undefined;
20
19
  /** Redraw interval for elapsed timers while a run is active. */
21
20
  tickMs?: number;
@@ -29,12 +28,13 @@ function viewportSize(): ViewportSize {
29
28
  }
30
29
 
31
30
  /**
32
- * Interactive live viewer. Watches the runs directory and re-renders as run
33
- * bundles change on disk. Returns when the user quits.
31
+ * Interactive live viewer. Watches the SQLite database and re-renders after
32
+ * committed run changes. Returns when the user quits.
34
33
  */
35
34
  export async function runViewer(options: ViewerOptions): Promise<void> {
35
+ const store = new WorkflowRunStore(options.databasePath, { readOnly: true });
36
36
  let mode: ViewerMode = { view: "list" };
37
- let bundles: LoadedRunBundle[] = [];
37
+ let bundles: LoadedWorkflowRun[] = [];
38
38
  let selectedIndex = 0;
39
39
  let detailScroll = 0;
40
40
  /** Replay position; null follows the latest step live. */
@@ -42,29 +42,29 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
42
42
  let detailStepCount = 0;
43
43
 
44
44
  if (options.runId) {
45
- bundles = await listRunBundles(options.runsDir);
45
+ bundles = store.listRuns();
46
46
  const match = bundles.find((bundle) => bundle.state.runId === options.runId);
47
47
  if (!match) {
48
48
  throw new Error(`Run not found: ${options.runId}`);
49
49
  }
50
- mode = { view: "detail", runDir: match.runDir };
50
+ mode = { view: "detail", runId: match.runId };
51
51
  }
52
52
 
53
53
  const draw = async () => {
54
- bundles = await listRunBundles(options.runsDir);
54
+ bundles = store.listRuns();
55
55
  selectedIndex = Math.min(selectedIndex, Math.max(0, bundles.length - 1));
56
56
  const size = viewportSize();
57
57
  const lines =
58
58
  mode.view === "list"
59
59
  ? renderRunListLines(bundles, selectedIndex, size)
60
- : await renderDetail(mode.runDir, size);
60
+ : await renderDetail(mode.runId, size);
61
61
  process.stdout.write(CLEAR + lines.join("\n"));
62
62
  };
63
63
 
64
- const renderDetail = async (runDir: string, size: ViewportSize): Promise<string[]> => {
65
- const bundle = await readRunBundle(runDir, { includeTrace: true });
64
+ const renderDetail = async (runId: string, size: ViewportSize): Promise<string[]> => {
65
+ const bundle = store.readRun(runId, { includeTrace: true });
66
66
  if (!bundle) {
67
- return ["Run bundle disappeared. Press q to go back."];
67
+ return ["SQLite run state disappeared. Press q to go back."];
68
68
  }
69
69
  detailStepCount = bundle.state.steps.length;
70
70
  if (selectedStep !== null && selectedStep >= detailStepCount - 1) {
@@ -76,7 +76,7 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
76
76
  };
77
77
 
78
78
  process.stdout.write(ALT_SCREEN_ON);
79
- const stopWatching = watchRunsDir(options.runsDir, () => {
79
+ const stopWatching = watchStateDatabase(options.databasePath, () => {
80
80
  void draw();
81
81
  });
82
82
  const ticker = setInterval(() => {
@@ -136,7 +136,7 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
136
136
  } else if (key === "\r" || key === "\n") {
137
137
  const selected = bundles[selectedIndex];
138
138
  if (selected) {
139
- mode = { view: "detail", runDir: selected.runDir };
139
+ mode = { view: "detail", runId: selected.runId };
140
140
  detailScroll = 0;
141
141
  selectedStep = null;
142
142
  void draw();
@@ -150,6 +150,7 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
150
150
  } finally {
151
151
  clearInterval(ticker);
152
152
  stopWatching();
153
+ store.close();
153
154
  if (rawModeSupported) {
154
155
  process.stdin.setRawMode(false);
155
156
  }
@@ -1,28 +1,25 @@
1
1
  import fs from "node:fs";
2
+ import path from "node:path";
2
3
 
3
4
  export type Unsubscribe = () => void;
4
5
 
5
- /**
6
- * Watch a directory tree for changes with a polling fallback. `onChange` is
7
- * debounced so bursts of writes trigger one refresh.
8
- */
9
- export function watchRunsDir(
10
- dir: string,
6
+ /** Watch the canonical database and its WAL with a polling fallback. */
7
+ export function watchStateDatabase(
8
+ databasePath: string,
11
9
  onChange: () => void,
12
10
  options: { pollMs?: number; debounceMs?: number } = {},
13
11
  ): Unsubscribe {
14
12
  const pollMs = options.pollMs ?? 1_000;
15
13
  const debounceMs = options.debounceMs ?? 80;
14
+ const directory = path.dirname(databasePath);
15
+ const base = path.basename(databasePath);
16
+ const relevant = new Set([base, `${base}-wal`, `${base}-shm`]);
16
17
  let debounceTimer: NodeJS.Timeout | null = null;
17
18
  let closed = false;
18
19
 
19
20
  const fire = () => {
20
- if (closed) {
21
- return;
22
- }
23
- if (debounceTimer) {
24
- clearTimeout(debounceTimer);
25
- }
21
+ if (closed) return;
22
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
26
23
  debounceTimer = setTimeout(() => {
27
24
  debounceTimer = null;
28
25
  onChange();
@@ -31,7 +28,9 @@ export function watchRunsDir(
31
28
 
32
29
  let watcher: fs.FSWatcher | null = null;
33
30
  try {
34
- watcher = fs.watch(dir, { recursive: true }, fire);
31
+ watcher = fs.watch(directory, (_event, filename) => {
32
+ if (filename === null || relevant.has(filename.toString())) fire();
33
+ });
35
34
  watcher.on("error", () => {
36
35
  watcher?.close();
37
36
  watcher = null;
@@ -40,15 +39,12 @@ export function watchRunsDir(
40
39
  watcher = null;
41
40
  }
42
41
 
43
- // Polling fallback covers platforms without recursive fs.watch and missed events.
44
42
  const poller = setInterval(fire, pollMs);
45
43
  poller.unref?.();
46
44
 
47
45
  return () => {
48
46
  closed = true;
49
- if (debounceTimer) {
50
- clearTimeout(debounceTimer);
51
- }
47
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
52
48
  clearInterval(poller);
53
49
  watcher?.close();
54
50
  };
@@ -1,5 +1,5 @@
1
1
  import { isWorkflowDefinition } from "./definition.js";
2
- import { WorkflowSourceChangedError } from "./errors.js";
2
+ import { BuiltinWorkflowRevisionChangedError } from "./errors.js";
3
3
  import type { WorkflowDefinition, WorkflowSource } from "./types.js";
4
4
 
5
5
  const BUILTIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
@@ -102,7 +102,12 @@ export class BuiltinWorkflowCatalog {
102
102
  throw new Error(`Unknown built-in workflow: ${source.id}`);
103
103
  }
104
104
  if (entry.revision !== source.revision) {
105
- throw new WorkflowSourceChangedError(runId);
105
+ throw new BuiltinWorkflowRevisionChangedError({
106
+ runId,
107
+ workflowId: entry.id,
108
+ previousRevision: source.revision,
109
+ currentRevision: entry.revision,
110
+ });
106
111
  }
107
112
  return entry.definition;
108
113
  }
@@ -3,7 +3,6 @@ import { readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import type {
5
5
  ActionNodeDefinition,
6
- AgentNodeDefinition,
7
6
  CheckpointNodeDefinition,
8
7
  ComputeNodeDefinition,
9
8
  FunctionActionNodeDefinition,
@@ -451,11 +450,20 @@ function wrapNode(
451
450
  ...(node.statusDetail !== undefined ? { statusDetail: node.statusDetail } : {}),
452
451
  };
453
452
  switch (node.nodeType) {
454
- case "agent":
453
+ case "agent": {
454
+ const prompt = (context: WorkflowNodeContext) => node.prompt(project(context));
455
+ if (typeof node.expectedOutput === "object") {
456
+ return {
457
+ ...common,
458
+ nodeType: "agent",
459
+ prompt,
460
+ expectedOutput: node.expectedOutput,
461
+ };
462
+ }
455
463
  return {
456
464
  ...common,
457
465
  nodeType: "agent",
458
- prompt: (context) => (node as AgentNodeDefinition).prompt(project(context)),
466
+ prompt,
459
467
  ...(node.expectedOutput !== undefined ? { expectedOutput: node.expectedOutput } : {}),
460
468
  ...(node.validate !== undefined
461
469
  ? {
@@ -464,6 +472,7 @@ function wrapNode(
464
472
  }
465
473
  : {}),
466
474
  };
475
+ }
467
476
  case "compute":
468
477
  return {
469
478
  ...common,
@@ -480,6 +489,7 @@ function wrapNode(
480
489
  case "checkpoint": {
481
490
  const human = node.humanDecision;
482
491
  const audience = human?.audience;
492
+ const onTimeout = human?.onTimeout;
483
493
  return {
484
494
  ...common,
485
495
  nodeType: "checkpoint",
@@ -496,6 +506,14 @@ function wrapNode(
496
506
  : audience!,
497
507
  choices: human.choices,
498
508
  request: (context: WorkflowNodeContext) => human.request(project(context)),
509
+ ...(onTimeout !== undefined
510
+ ? {
511
+ onTimeout:
512
+ typeof onTimeout === "function"
513
+ ? (context: WorkflowNodeContext) => onTimeout(project(context))
514
+ : onTimeout,
515
+ }
516
+ : {}),
499
517
  },
500
518
  }
501
519
  : {}),
@@ -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")
@@ -9,6 +9,9 @@ import {
9
9
  } from "./schema.js";
10
10
  import type {
11
11
  AgentNodeDefinition,
12
+ AssistantAgentNodeDefinition,
13
+ AssistantMessageOutput,
14
+ SubmittedAgentNodeDefinition,
12
15
  ActionNodeDefinition,
13
16
  CheckpointNodeDefinition,
14
17
  ComputeNodeDefinition,
@@ -154,11 +157,41 @@ export function isWorkflowDefinition(value: unknown): value is WorkflowDefinitio
154
157
  );
155
158
  }
156
159
 
160
+ export function assistantMessage(options: { maxChars?: number } = {}): AssistantMessageOutput {
161
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
162
+ throw new Error("Invalid workflow definition: assistantMessage options must be an object");
163
+ }
164
+ const unknown = Object.keys(options).filter((key) => key !== "maxChars");
165
+ if (unknown.length > 0) {
166
+ throw new Error(
167
+ `Invalid workflow definition: assistantMessage has unknown option ${JSON.stringify(unknown[0])}`,
168
+ );
169
+ }
170
+ if (
171
+ options.maxChars !== undefined &&
172
+ (!Number.isInteger(options.maxChars) || options.maxChars <= 0)
173
+ ) {
174
+ throw new Error(
175
+ "Invalid workflow definition: assistantMessage maxChars must be a positive integer",
176
+ );
177
+ }
178
+ return Object.freeze({
179
+ kind: "assistant-message" as const,
180
+ ...(options.maxChars !== undefined ? { maxChars: options.maxChars } : {}),
181
+ });
182
+ }
183
+
184
+ export function agent(
185
+ definition: Omit<SubmittedAgentNodeDefinition, "nodeType">,
186
+ ): SubmittedAgentNodeDefinition;
187
+ export function agent(
188
+ definition: Omit<AssistantAgentNodeDefinition, "nodeType">,
189
+ ): AssistantAgentNodeDefinition;
157
190
  export function agent(definition: Omit<AgentNodeDefinition, "nodeType">): AgentNodeDefinition {
158
- const node: AgentNodeDefinition = {
159
- nodeType: "agent",
191
+ const node = {
192
+ nodeType: "agent" as const,
160
193
  ...definition,
161
- };
194
+ } as AgentNodeDefinition;
162
195
  assertValidAgentNode(node);
163
196
  return node;
164
197
  }