@osolmaz/pi-workflows 0.13.4 → 0.15.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 (237) hide show
  1. package/README.md +146 -160
  2. package/dist/builtins/autoimplement.workflow.js +3 -1
  3. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  4. package/dist/builtins/change-verification.workflow.js +7 -2
  5. package/dist/builtins/change-verification.workflow.js.map +1 -1
  6. package/dist/builtins/metadata.d.ts +22 -0
  7. package/dist/builtins/metadata.js +10 -0
  8. package/dist/builtins/metadata.js.map +1 -0
  9. package/dist/builtins/monitor.workflow.js +4 -1
  10. package/dist/builtins/monitor.workflow.js.map +1 -1
  11. package/dist/builtins/sanity-check.workflow.js +4 -1
  12. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  13. package/dist/builtins/workspace-preparation.workflow.js +3 -1
  14. package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
  15. package/dist/controllers/index.d.ts +1 -2
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +85 -31
  19. package/dist/controllers/sqlite.js +541 -126
  20. package/dist/controllers/sqlite.js.map +1 -1
  21. package/dist/controllers/store.d.ts +1 -0
  22. package/dist/extension/controller-command.d.ts +22 -0
  23. package/dist/extension/controller-command.js +32 -0
  24. package/dist/extension/controller-command.js.map +1 -0
  25. package/dist/extension/index.d.ts +2 -9
  26. package/dist/extension/index.js +876 -2928
  27. package/dist/extension/index.js.map +1 -1
  28. package/dist/extension/message-card.d.ts +13 -0
  29. package/dist/extension/message-card.js +46 -0
  30. package/dist/extension/message-card.js.map +1 -0
  31. package/dist/extension/step-message.d.ts +5 -6
  32. package/dist/extension/step-message.js +41 -52
  33. package/dist/extension/step-message.js.map +1 -1
  34. package/dist/host/child-worker-supervisor.d.ts +50 -0
  35. package/dist/host/child-worker-supervisor.js +159 -0
  36. package/dist/host/child-worker-supervisor.js.map +1 -0
  37. package/dist/host/client.d.ts +48 -0
  38. package/dist/host/client.js +216 -0
  39. package/dist/host/client.js.map +1 -0
  40. package/dist/host/controller-worker-entry.d.ts +2 -0
  41. package/dist/host/controller-worker-entry.js +244 -0
  42. package/dist/host/controller-worker-entry.js.map +1 -0
  43. package/dist/host/controller-worker-protocol.d.ts +36 -0
  44. package/dist/host/controller-worker-protocol.js +49 -0
  45. package/dist/host/controller-worker-protocol.js.map +1 -0
  46. package/dist/host/controller-worker-supervisor.d.ts +21 -0
  47. package/dist/host/controller-worker-supervisor.js +54 -0
  48. package/dist/host/controller-worker-supervisor.js.map +1 -0
  49. package/dist/host/host-entry.d.ts +2 -0
  50. package/dist/host/host-entry.js +23 -0
  51. package/dist/host/host-entry.js.map +1 -0
  52. package/dist/host/processes.d.ts +17 -12
  53. package/dist/host/processes.js +154 -50
  54. package/dist/host/processes.js.map +1 -1
  55. package/dist/host/protocol.d.ts +38 -0
  56. package/dist/host/protocol.js +156 -0
  57. package/dist/host/protocol.js.map +1 -0
  58. package/dist/host/resolver-entry.d.ts +49 -0
  59. package/dist/host/resolver-entry.js +149 -0
  60. package/dist/host/resolver-entry.js.map +1 -0
  61. package/dist/host/rpc-executor.d.ts +14 -3
  62. package/dist/host/rpc-executor.js +63 -30
  63. package/dist/host/rpc-executor.js.map +1 -1
  64. package/dist/host/runner.d.ts +95 -29
  65. package/dist/host/runner.js +2311 -348
  66. package/dist/host/runner.js.map +1 -1
  67. package/dist/host/state.d.ts +174 -0
  68. package/dist/host/state.js +689 -0
  69. package/dist/host/state.js.map +1 -0
  70. package/dist/host/worker-entry.d.ts +10 -0
  71. package/dist/host/worker-entry.js +446 -0
  72. package/dist/host/worker-entry.js.map +1 -0
  73. package/dist/host/worker-protocol.d.ts +31 -0
  74. package/dist/host/worker-protocol.js +122 -0
  75. package/dist/host/worker-protocol.js.map +1 -0
  76. package/dist/host/worker-store.d.ts +80 -0
  77. package/dist/host/worker-store.js +91 -0
  78. package/dist/host/worker-store.js.map +1 -0
  79. package/dist/host/worker-supervisor.d.ts +22 -0
  80. package/dist/host/worker-supervisor.js +54 -0
  81. package/dist/host/worker-supervisor.js.map +1 -0
  82. package/dist/render/canvas.d.ts +2 -1
  83. package/dist/render/canvas.js +40 -15
  84. package/dist/render/canvas.js.map +1 -1
  85. package/dist/render/graph-render.d.ts +7 -3
  86. package/dist/render/graph-render.js +125 -74
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.js +3 -1
  89. package/dist/state/database.js.map +1 -1
  90. package/dist/state/index.d.ts +1 -0
  91. package/dist/state/index.js +1 -0
  92. package/dist/state/index.js.map +1 -1
  93. package/dist/state/prune.js +36 -10
  94. package/dist/state/prune.js.map +1 -1
  95. package/dist/state/schema.d.ts +1 -1
  96. package/dist/state/schema.js +171 -3
  97. package/dist/state/schema.js.map +1 -1
  98. package/dist/state/viewer.d.ts +46 -0
  99. package/dist/state/viewer.js +249 -0
  100. package/dist/state/viewer.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +1 -1
  102. package/dist/viewer/cli.js +47 -18
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/workflows/composition.js +25 -2
  105. package/dist/workflows/composition.js.map +1 -1
  106. package/dist/workflows/definition.d.ts +3 -1
  107. package/dist/workflows/definition.js +25 -0
  108. package/dist/workflows/definition.js.map +1 -1
  109. package/dist/workflows/engine.d.ts +1 -0
  110. package/dist/workflows/engine.js +157 -42
  111. package/dist/workflows/engine.js.map +1 -1
  112. package/dist/workflows/errors.d.ts +3 -1
  113. package/dist/workflows/errors.js +4 -7
  114. package/dist/workflows/errors.js.map +1 -1
  115. package/dist/workflows/human-decision.d.ts +3 -0
  116. package/dist/workflows/human-decision.js +31 -0
  117. package/dist/workflows/human-decision.js.map +1 -1
  118. package/dist/workflows/index.d.ts +1 -1
  119. package/dist/workflows/index.js +1 -1
  120. package/dist/workflows/index.js.map +1 -1
  121. package/dist/workflows/schema.js +17 -6
  122. package/dist/workflows/schema.js.map +1 -1
  123. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  124. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  125. package/dist/workflows/session-reducer.js.map +1 -0
  126. package/dist/workflows/store.d.ts +87 -1
  127. package/dist/workflows/store.js +892 -93
  128. package/dist/workflows/store.js.map +1 -1
  129. package/dist/workflows/tool-input.d.ts +0 -22
  130. package/dist/workflows/tool-input.js +1 -44
  131. package/dist/workflows/tool-input.js.map +1 -1
  132. package/dist/workflows/types.d.ts +36 -4
  133. package/docs/2026-08-25-workflow-follow-ups.md +8 -6
  134. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  135. package/docs/CONTROLLERS.md +11 -11
  136. package/docs/DEFERRED_TURNS.md +61 -27
  137. package/docs/HUMAN_DECISIONS.md +12 -4
  138. package/docs/SQLITE_STATE.md +42 -8
  139. package/docs/WORKFLOW_HOST.md +452 -0
  140. package/docs/development.md +46 -30
  141. package/docs/live-replay-protocol.md +129 -100
  142. package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
  143. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
  144. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  145. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  146. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  147. package/docs/tui-viewer.md +18 -3
  148. package/docs/workflows.md +164 -156
  149. package/examples/workflows/command-batch.workflow.ts +2 -0
  150. package/examples/workflows/shell.workflow.ts +2 -1
  151. package/herdr-plugin.toml +1 -1
  152. package/package.json +1 -1
  153. package/skills/autodoc/SKILL.md +1 -1
  154. package/skills/autoimplement/SKILL.md +1 -1
  155. package/skills/autoplan/SKILL.md +1 -1
  156. package/skills/pi-workflows/SKILL.md +2 -0
  157. package/src/builtins/autoimplement.workflow.ts +3 -0
  158. package/src/builtins/change-verification.workflow.ts +7 -2
  159. package/src/builtins/metadata.ts +9 -0
  160. package/src/builtins/monitor.workflow.ts +4 -0
  161. package/src/builtins/sanity-check.workflow.ts +4 -0
  162. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  163. package/src/controllers/index.ts +3 -5
  164. package/src/controllers/sqlite.ts +929 -225
  165. package/src/controllers/store.ts +1 -0
  166. package/src/extension/controller-command.ts +45 -0
  167. package/src/extension/index.ts +948 -3506
  168. package/src/extension/message-card.ts +61 -0
  169. package/src/extension/step-message.ts +58 -63
  170. package/src/host/child-worker-supervisor.ts +183 -0
  171. package/src/host/client.ts +293 -0
  172. package/src/host/controller-worker-entry.ts +311 -0
  173. package/src/host/controller-worker-protocol.ts +104 -0
  174. package/src/host/controller-worker-supervisor.ts +79 -0
  175. package/src/host/host-entry.ts +23 -0
  176. package/src/host/processes.ts +171 -54
  177. package/src/host/protocol.ts +196 -0
  178. package/src/host/resolver-entry.ts +241 -0
  179. package/src/host/rpc-executor.ts +76 -34
  180. package/src/host/runner.ts +2813 -422
  181. package/src/host/state.ts +1160 -0
  182. package/src/host/worker-entry.ts +533 -0
  183. package/src/host/worker-protocol.ts +165 -0
  184. package/src/host/worker-store.ts +229 -0
  185. package/src/host/worker-supervisor.ts +74 -0
  186. package/src/render/canvas.ts +44 -10
  187. package/src/render/graph-render.ts +145 -90
  188. package/src/state/database.ts +2 -1
  189. package/src/state/index.ts +14 -0
  190. package/src/state/prune.ts +35 -9
  191. package/src/state/schema.ts +171 -3
  192. package/src/state/viewer.ts +356 -0
  193. package/src/viewer/cli.ts +49 -17
  194. package/src/workflows/composition.ts +36 -2
  195. package/src/workflows/definition.ts +32 -0
  196. package/src/workflows/engine.ts +157 -54
  197. package/src/workflows/errors.ts +11 -2
  198. package/src/workflows/human-decision.ts +49 -0
  199. package/src/workflows/index.ts +2 -0
  200. package/src/workflows/schema.ts +19 -6
  201. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  202. package/src/workflows/store.ts +1316 -108
  203. package/src/workflows/tool-input.ts +2 -60
  204. package/src/workflows/types.ts +32 -4
  205. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  206. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  207. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  208. package/dist/extension/controller-host.d.ts +0 -48
  209. package/dist/extension/controller-host.js +0 -110
  210. package/dist/extension/controller-host.js.map +0 -1
  211. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  212. package/dist/extension/deferred-turn-coordinator.js +0 -143
  213. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  214. package/dist/extension/deferred-turn.d.ts +0 -44
  215. package/dist/extension/deferred-turn.js +0 -110
  216. package/dist/extension/deferred-turn.js.map +0 -1
  217. package/dist/extension/executor.d.ts +0 -86
  218. package/dist/extension/executor.js +0 -311
  219. package/dist/extension/executor.js.map +0 -1
  220. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  221. package/dist/extension/follow-up-coordinator.js +0 -131
  222. package/dist/extension/follow-up-coordinator.js.map +0 -1
  223. package/dist/extension/recorder.d.ts +0 -84
  224. package/dist/extension/recorder.js +0 -528
  225. package/dist/extension/recorder.js.map +0 -1
  226. package/dist/extension/session-events.d.ts +0 -133
  227. package/dist/extension/session-events.js +0 -61
  228. package/dist/extension/session-events.js.map +0 -1
  229. package/dist/viewer/session-reducer.js.map +0 -1
  230. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  231. package/src/extension/controller-host.ts +0 -167
  232. package/src/extension/deferred-turn-coordinator.ts +0 -171
  233. package/src/extension/deferred-turn.ts +0 -166
  234. package/src/extension/executor.ts +0 -411
  235. package/src/extension/follow-up-coordinator.ts +0 -151
  236. package/src/extension/recorder.ts +0 -655
  237. package/src/extension/session-events.ts +0 -121
package/docs/workflows.md CHANGED
@@ -19,14 +19,12 @@ Pi Workflows includes built-in `plain-summary`, `autoplan`, `autodoc`,
19
19
  `autoimplement`, `plan-approval`, `sanity-check`, and `monitor` workflows. `autoplan` is the current name for the
20
20
  planning workflow that was first released as `autodevise`; the old command and
21
21
  export are not retained. A project or global file named `monitor.workflow.ts`
22
- replaces the built-in monitor. The package registers each built-in in
23
- a process-local catalog with a stable reference such as `builtin:monitor` and
24
- an explicit revision. Built-ins are imported with the engine when a Pi process
25
- starts. They are not read from the package directory when a run starts or
26
- resumes. Updating the package on disk cannot mix a new built-in with that
27
- process's old engine; reload or restart Pi to use the new built-in. A revision
28
- mismatch refuses resume. Project and global workflow files still reload on
29
- each run and use their path and SHA-256 hash as their source identity.
22
+ replaces the built-in monitor. Each built-in has a stable reference such as
23
+ `builtin:monitor` and an explicit revision. A resolver child snapshots the
24
+ selected built-in before start, and each run worker verifies that identity
25
+ before execution. A revision mismatch refuses resume. Project and global
26
+ workflow files also use their absolute path and SHA-256 hash as source
27
+ identity.
30
28
 
31
29
  The workflow's command name is the file stem, so `.pi/workflows/triage.workflow.ts`
32
30
  runs as `/workflow triage`. A direct path also works: `/workflow ./somewhere/x.workflow.ts`.
@@ -40,7 +38,6 @@ import { agent, compute, defineWorkflow } from "@osolmaz/pi-workflows";
40
38
  export default defineWorkflow({
41
39
  name: "example",
42
40
  title: ({ input }) => `example: ${(input as { task?: string }).task}`,
43
- presentationPrompt: "Present the final answer clearly and concisely.",
44
41
  startAt: "ask",
45
42
  maxSteps: 50,
46
43
  nodes: {
@@ -56,20 +53,19 @@ export default defineWorkflow({
56
53
 
57
54
  Top-level fields:
58
55
 
59
- | Field | Type | Notes |
60
- | -------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
61
- | `name` | `string` | Required. Used in run ids and the step contract. `answer`, `cancel`, `list`, `pause`, `resume`, and `status` are reserved for `/workflow` subcommands. |
62
- | `source` | `string` | Optional `import.meta.url` for exact provenance when another TypeScript workflow imports this definition directly. |
63
- | `contractId` | `string` | Optional stable input-and-exit contract identity. Dynamic overrides must match it. |
64
- | `input` | `function` | Optional runtime input normalizer and validator. Its return type is the workflow input type. |
65
- | `title` | `string` or function | Optional run title, resolved once at start from `{ input, workflowName }`. Async resolution is bounded (30s) and cancellable. |
66
- | `presentationPrompt` | `string` or function | Optional instructions for a normal assistant response after the run. A function receives `{ state, finalOutput, signal }` and may return a prompt or `undefined`. See [Result presentation](#result-presentation). |
67
- | `startAt` | `string` | Required. Id of the first node. |
68
- | `nodes` | `Record<string, node>` | Required, non-empty. Node ids must match `[A-Za-z_][A-Za-z0-9_-]*`. |
69
- | `includes` | `Record<string, include>` | Optional imported or dynamically resolved child workflows. |
70
- | `exits` | `Record<string, exit>` | Optional named successful terminal nodes used when another workflow includes this workflow. |
71
- | `edges` | `WorkflowEdge[]` | Required. See routing below. |
72
- | `maxSteps` | `number` | Optional loop bound, default 100. The run fails when exceeded. |
56
+ | Field | Type | Notes |
57
+ | ------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
58
+ | `name` | `string` | Required. Used in run ids and the step contract. `answer`, `cancel`, `list`, `pause`, `resume`, and `status` are reserved for `/workflow` subcommands. |
59
+ | `source` | `string` | Optional `import.meta.url` for exact provenance when another TypeScript workflow imports this definition directly. |
60
+ | `contractId` | `string` | Optional stable input-and-exit contract identity. Dynamic overrides must match it. |
61
+ | `input` | `function` | Optional runtime input normalizer and validator. Its return type is the workflow input type. |
62
+ | `title` | `string` or function | Optional run title, resolved once at start from `{ input, workflowName }`. Async resolution is bounded (30s) and cancellable. |
63
+ | `startAt` | `string` | Required. Id of the first node. |
64
+ | `nodes` | `Record<string, node>` | Required, non-empty. Node ids must match `[A-Za-z_][A-Za-z0-9_-]*`. |
65
+ | `includes` | `Record<string, include>` | Optional imported or dynamically resolved child workflows. |
66
+ | `exits` | `Record<string, exit>` | Optional named successful terminal nodes used when another workflow includes this workflow. |
67
+ | `edges` | `WorkflowEdge[]` | Required. See routing below. |
68
+ | `maxSteps` | `number` | Optional loop bound, default 100. The run fails when exceeded. |
73
69
 
74
70
  `defineWorkflow` validates the shape eagerly (node ids, edge shapes, function
75
71
  fields) and validates the graph (unknown targets, duplicate outgoing edges,
@@ -112,36 +108,51 @@ Function actions receive `WorkflowActionContext`, which adds
112
108
 
113
109
  ## Durable runs, parking, and resume
114
110
 
115
- Every interactive `/workflow` run is tracked in the project run queue (see
116
- [CONTROLLERS.md](CONTROLLERS.md) for the store). The session that starts a run
117
- claims it and owns it while it executes; every owner-only SQLite write proves the claim
118
- first (write fencing).
111
+ Every run enters one global SQLite queue. One package-owned host claims runs,
112
+ renews live claims, commits state, and supervises one child process for each
113
+ active run generation. The extension is a local host client. It does not run
114
+ the workflow engine or workflow definitions.
119
115
 
120
- Closing the Pi session mid-run no longer cancels the run. The engine **parks**:
121
- it stops without a terminal event, releases the claim, and leaves the run
122
- resumable. When a runner is available again (a reopened Pi session or the
123
- standalone host), the run **resumes** at the node it stopped on. Completed
124
- nodes replay from the recorded state; only the interrupted node and everything
125
- downstream rerun. Resume repairs a torn trace tail, drops trace events the
126
- state projection never recorded, and refuses to continue if the workflow
127
- source changed since the run started (a forced resume records the mismatch).
128
-
129
- The standalone host runs without any Pi session:
116
+ The host starts on demand when a Pi or CLI client needs it. These commands
117
+ control the same user-level host for all projects:
130
118
 
131
119
  ```bash
132
- pi-workflows host --project /path/to/project
120
+ pi-workflows host start
121
+ pi-workflows host status
122
+ pi-workflows host stop
123
+ pi-workflows host run
133
124
  ```
134
125
 
135
- The host claims parked runs, resumes them, and reconciles durable controllers.
136
- Agent nodes that submit through the workflow tool execute in headless
137
- `pi --mode rpc` children that load a small bridge extension. An agent node with
138
- `expectedOutput: assistantMessage()` parks before prompting and waits for the
139
- origin Pi session because its result must be a visible assistant message. A
140
- detached run without an origin session fails clearly. The host is a foreground process: start it in a terminal and
141
- stop it with Ctrl-C. A second host for the same project refuses to start, and
142
- a host that dies has its orphaned children reaped by the next one. While the host works, reports enter a durable outbox addressed to the Pi
143
- session that started the run. They remain pending while that session is closed
144
- and never enter another conversation in the same project.
126
+ `host run` stays attached. The other commands start, inspect, or stop the
127
+ on-demand process. No command installs an operating-system service.
128
+
129
+ A worker verifies the root and all mounted source identities before it loads
130
+ workflow modules. It then checks the resolved mounted-source map and executes
131
+ from committed state through a host-backed store. A source mismatch parks the
132
+ run with `workflowSourceChanged`; normal scheduling does not retry it. Restore
133
+ the recorded source and explicitly resume, or cancel the run. A headless Pi
134
+ child uses its own registered process group. The worker stops that group on
135
+ normal completion, and the host reaps it if the worker exits first. If the
136
+ worker stops for another recoverable reason, pure work can run again. A
137
+ protected write checks and renews the exact live token and generation in one
138
+ transaction. An expired or replaced owner cannot revive itself.
139
+
140
+ Interactive agent and assistant-message steps do not run headlessly for a Pi
141
+ session. The worker commits a durable interaction request and parks. The origin
142
+ session presents the request through documented Pi APIs and submits the exact
143
+ request, node, attempt, and revision. The host records submitted output as
144
+ provisional. A new supervised worker loads the workflow and runs its `validate`
145
+ function before the host accepts the submission. A validation error leaves the
146
+ same request pending and returns the error to the model. Closing Pi leaves that
147
+ request pending; reopening the same session adopts the existing session entry
148
+ or presents it once. Notifications use the durable session outbox. A root
149
+ `presentationPrompt` creates a durable terminal turn only after completion is
150
+ committed. A controller child without an origin session can use a supervised
151
+ headless `pi --mode rpc` child for structured agent steps.
152
+
153
+ Pause stops the worker and parks at the last durable boundary. Resume takes a
154
+ new generation. Cancellation can stop a live worker or atomically claim and
155
+ cancel an expired running row. Resume refuses changed workflow source.
145
156
 
146
157
  ## Node types
147
158
 
@@ -179,10 +190,13 @@ agent({
179
190
  because an invalid response is already visible and must not be retried.
180
191
 
181
192
  For submitted output, the engine appends the existing workflow-tool contract.
182
- The output passes through tolerant JSON normalization and then `validate`.
183
- Rejected submissions can retry in the same step. If the model settles without
184
- submitting, the extension nudges it twice by default and then fails the step.
185
- For assistant-message output, the engine appends a normal-response contract,
193
+ The host checks the durable transport identifiers, stores a `validating`
194
+ submission, and starts a supervised worker. In that worker, the output passes
195
+ through tolerant JSON normalization and then `validate`. The tool reports
196
+ success only after this check accepts the output. Rejected submissions return
197
+ the validation error and can retry in the same step. If the model settles
198
+ without submitting, the durable request stays pending until it receives valid
199
+ output, times out, or is cancelled. For assistant-message output, the engine appends a normal-response contract,
186
200
  waits for `agent_settled`, rejects empty, failed, aborted, or tool-only results,
187
201
  and never suppresses the visible text. Timeout and cancellation abort either
188
202
  form's active Pi turn.
@@ -225,20 +239,34 @@ controller resource instead.
225
239
 
226
240
  ### action
227
241
 
228
- Performs a side effect. Two forms exist. The function form runs arbitrary
229
- TypeScript:
242
+ Performs managed work. Every function action and shell action must declare how
243
+ the host recovers if the worker exits after the external operation but before
244
+ it saves a receipt.
245
+
246
+ Use `idempotentEffect(type)` only when the operation has a stable external
247
+ idempotency key or a read-back check that makes another attempt safe:
230
248
 
231
249
  ```typescript
232
- action({ run: async ({ input }) => await deployPreview(input) });
250
+ import { action, idempotentEffect } from "@osolmaz/pi-workflows";
251
+
252
+ action({
253
+ effect: idempotentEffect("preview.deploy"),
254
+ run: async ({ input }) => await deployPreview(input),
255
+ });
233
256
  ```
234
257
 
258
+ Use `manualEffect(type)` when the external system cannot prove whether an
259
+ uncertain request applied. An uncertain worker exit marks that effect
260
+ `ambiguous`, parks the run, and requires explicit operator recovery. The host
261
+ does not retry it automatically.
262
+
235
263
  The shell form (`shell` is a synonym that requires `exec`) runs a command owned
236
- by the runtime, so the workflow author decides exactly what executes, with a
237
- timeout and captured output:
264
+ by the workflow definition, with a timeout and captured output:
238
265
 
239
266
  ```typescript
240
267
  shell({
241
- exec: ({ input }) => ({
268
+ effect: idempotentEffect("repository.status"),
269
+ exec: () => ({
242
270
  command: "git",
243
271
  args: ["status", "--porcelain"],
244
272
  cwd: "/path/to/repo",
@@ -261,6 +289,7 @@ A function action can publish a durable update without completing the node:
261
289
 
262
290
  ```typescript
263
291
  action({
292
+ effect: idempotentEffect("dataset.process"),
264
293
  run: async ({ publishUpdate }) => {
265
294
  await publishUpdate({
266
295
  type: "progress",
@@ -283,6 +312,12 @@ keeps normal output capture. Lines and update data are each limited to 64 KiB.
283
312
  See [WORKFLOW_UPDATES.md](WORKFLOW_UPDATES.md) for the envelope, progress
284
313
  schema, limits, estimation, and error rules.
285
314
 
315
+ The host reserves the effect before it lets the action run. A repeated key with
316
+ the same request adopts the durable record; the same key with another request
317
+ is a conflict. A normal caught error settles the attempt as rejected. After an
318
+ uncertain process exit, an idempotent effect returns to pending for retry, while
319
+ a manual effect becomes ambiguous. This is not an exactly-once claim.
320
+
286
321
  ### checkpoint
287
322
 
288
323
  Ends the run in a `waiting` state for human review. The checkpoint run is
@@ -333,9 +368,9 @@ humanDecision({
333
368
  });
334
369
  ```
335
370
 
336
- The waiting run stores a versioned request and asks every channel configured for the logical audience. The structured `subject` remains machine data. Channels receive only the normalized `presentation`, title, choices, input prompts, and any deadline policy. The first valid verified human answer wins. When `onTimeout` is present and no human answer wins before the saved deadline, recovery applies the validated response with `timeout` provenance. This policy can continue without a configured channel. A continuation preserves the original workflow input and exposes the resolved response as the checkpoint output. `humanDecisionEdge()` provides exhaustive routing for the choices. Existing `body` requests remain a legacy compatibility form and use deterministic readable formatting.
371
+ The waiting run stores a versioned request and asks every channel configured for the logical audience. The structured `subject` remains machine data. Channels receive only the normalized `presentation`, title, choices, input prompts, and any deadline policy. The first valid verified human answer wins. When `onTimeout` is present and no human answer wins before the saved deadline, the host takes a control claim on the waiting parent and atomically applies the validated response with `timeout` provenance, closes the interaction, and reserves the continuation. This policy can continue without a configured channel. A continuation preserves the original workflow input and exposes the resolved response as the checkpoint output. `humanDecisionEdge()` provides exhaustive routing for the choices. Existing `body` requests remain a legacy compatibility form and use deterministic readable formatting.
337
372
 
338
- The model-facing workflow tool cannot answer a protected human decision. Pi interactive UI and configured external channels use a host-owned answer path. Ordinary checkpoints keep the existing `/workflow answer` behavior.
373
+ The model-facing workflow tool cannot answer a protected human decision. The origin Pi session displays the request without starting a model turn. A person uses `/workflow answer` to send the answer through the host-owned path. Ordinary checkpoints can also use the model-facing `answer` action.
339
374
 
340
375
  See [Human decisions](HUMAN_DECISIONS.md) for channels, recovery, persistence, and plan approval.
341
376
 
@@ -428,32 +463,31 @@ The run records every mounted source and a digest of the resolved graph. Resume
428
463
 
429
464
  The model sees one `workflow` tool. Its `action` field supports:
430
465
 
431
- - `list` for discovered workflow names and sources.
432
- - `start` with a workflow name or path and structured input.
433
- - `status` for the active run or a supplied run ID.
434
- - `pause`, `resume`, and `cancel` for the active run.
435
- - `answer` with ordinary checkpoint input and an optional run ID. Protected `humanDecision()` gates reject this model-facing action.
436
- - `change-settings` with an RFC 6902 patch, optional scope ID, and optional expected change number.
437
- - `queue-follow-up` to save one ordered normal user prompt for after successful completion.
438
- - `remove-follow-up` to remove an unsent prompt created by the same model source.
439
- - `update` for a non-completing update from the current agent attempt.
466
+ - `list` for discovered workflow names and sources;
467
+ - `start` with a workflow name or path and structured input;
468
+ - `status` for the active run or a supplied run ID;
469
+ - `pause` and `resume` for the active session run;
470
+ - `cancel` for the active run or a supplied run ID;
471
+ - `answer` with checkpoint input and an optional run ID;
472
+ - `update` for a non-completing update from the current agent attempt;
440
473
  - `submit` for the current workflow step contract.
441
474
 
442
- A model-started run is queued until the model's current turn settles. The first
443
- workflow prompt then starts a new turn. This keeps the requesting turn outside
444
- the workflow's first attempt and prevents an early missing-submission reminder.
445
- The normal extension offers all actions. The headless RPC bridge offers only
446
- `update` and `submit`, so a workflow child cannot recursively control other
447
- runs. Direct `/workflow change-settings`, `queue-follow-up`, and
448
- `remove-follow-up` commands use verified interactive provenance. Controller
449
- code can use the matching `ctx.workflows` methods. All surfaces call the same
450
- SQLite operations.
451
-
452
- Follow-up prompts stay separate from workflow settings. Successful terminal
453
- state is saved before delivery. A final presentation settles first. The Pi
454
- extension then sends prompts in order as normal user messages without
455
- reactivating the completed run. See [Continue normal work after a workflow
456
- finishes](2026-08-25-workflow-follow-ups.md).
475
+ A direct user request to continue or resume the active workflow maps to
476
+ `resume` immediately. The model does not call `status` instead of `resume` or
477
+ use it as a prerequisite. An already active run adopts the resume request. A
478
+ paused or parked run gets a new claim generation and worker. With no resumable
479
+ run, the host rejects the request.
480
+
481
+ `status` reports the durable queue projection. A host command succeeds only
482
+ after its transaction commits. The protocol stores request fingerprints and
483
+ receipts, so an exact duplicate adopts the committed result and conflicting
484
+ reuse is rejected.
485
+
486
+ The normal extension offers these actions through the origin Pi session. The
487
+ headless RPC bridge offers only `update` and `submit`, so a controller child
488
+ cannot recursively control unrelated runs. Controller code can use its narrow
489
+ `ctx.workflows` methods for child runs, settings, and follow-up records. Those
490
+ methods also commit through the global host.
457
491
 
458
492
  ### Built-in plain summary
459
493
 
@@ -557,7 +591,7 @@ Only bounded final assistant text and safe operational facts leave a live child
557
591
 
558
592
  The workflow publishes aggregate and per-agent `pi-workflows.progress.v1` tracks under `agents/review/*` and `agents/verification/*`. Progress contains role, the verified actual model when known, state, elapsed facts, and safe phases such as `thinking` or `tool: read`. The Pi widget shows the aggregate plus failed and active children within its ten-line limit. `piw` shows every durable child track and its samples. Both views use existing progress records, so no child workflow run or new persisted schema is needed.
559
593
 
560
- Serial mode still uses two child sessions, and parallel mode still uses five. Review prompts, review areas, strict result validation, verdicts, and progress stay unchanged. After verification, an assistant-message agent shows the full bounded report verbatim. A mismatch stops before summary generation. The graph then includes `plain-summary`, which shows a short plain-language explanation with the verdict and the most important next action. The detailed response always settles before the summary starts. A final compute node returns the original strict result, so presentation cannot change the verdict. Sanity Check uses no final notification or root `presentationPrompt`.
594
+ Serial mode still uses two child sessions, and parallel mode still uses five. Review prompts, review areas, strict result validation, verdicts, and progress stay unchanged. After verification, an assistant-message agent shows the full bounded report verbatim. A mismatch stops before summary generation. The graph then includes `plain-summary`, which shows a short plain-language explanation with the verdict and the most important next action. The detailed response always settles before the summary starts. A final compute node returns the original strict result, so presentation cannot change the verdict. Sanity Check creates no extra terminal model turn.
561
595
 
562
596
  The CLI, JSON or RPC stream, temporary prompt file, standard-output cap, subprocess fallback, shared child runtime, and blanket child-extension ban are not retained. See [the Sanity Check plan](plans/2026-08-21-sanity-check-plan.md) for the selected implementation and test boundaries.
563
597
 
@@ -648,49 +682,22 @@ use an internal turn-intent contract instead of the notification outbox. See
648
682
  [WORKFLOW_STEP_MESSAGES.md](WORKFLOW_STEP_MESSAGES.md) for the step-message contract
649
683
  and [Deferred workflow turns](DEFERRED_TURNS.md) for the successor-turn contract.
650
684
 
651
- ## Result presentation
685
+ ## Visible responses
652
686
 
653
687
  Workflow nodes normally produce structured values for routing and persistence.
654
- When a person should see a normal prose response only after the root run, add
655
- `presentationPrompt` at the top level:
688
+ When a person must receive normal prose, use an agent node with
689
+ `expectedOutput: assistantMessage()`. The worker parks and records the exact
690
+ step request. The origin Pi session starts the model turn, and the visible
691
+ assistant text becomes the node output after the turn settles.
656
692
 
657
- ```typescript
658
- export default defineWorkflow({
659
- name: "report",
660
- presentationPrompt: ({ state, finalOutput }) =>
661
- state.status === "waiting"
662
- ? `Explain this recommendation and ask the user to decide: ${JSON.stringify(finalOutput)}`
663
- : "Summarize the completed result and any remaining limitations.",
664
- // ...startAt, nodes, and edges
665
- });
666
- ```
693
+ The request keeps its node and attempt ID across Pi reload. The extension first
694
+ looks for an existing session entry with the durable request ID. It inserts a
695
+ new visible message only when no adopted entry exists. A repeated submission
696
+ returns its stored receipt.
667
697
 
668
- After the final run state has been persisted, the Pi extension sends the
669
- presentation instructions and bounded final result to the model as a hidden
670
- follow-up message. The next visible message is a normal assistant response.
671
- Returning `undefined`, returning an empty string, or omitting
672
- `presentationPrompt` produces no presentation. Failed, timed-out, and cancelled
673
- runs are never presented. When one of those outcomes would otherwise strand an
674
- agent after a workflow-caused turn abort or asynchronous crash, the extension
675
- uses the deferred-turn contract to send one factual fallback after settlement.
676
- Async prompt builders have 30 seconds to finish and receive an
677
- `AbortSignal` that fires on timeout, session shutdown, or when a new workflow
678
- or normal user turn starts; stale presentations are discarded. Once a presentation message has
679
- been queued, another workflow cannot start until that assistant response
680
- settles, so results cannot interleave.
681
-
682
- An agent with `expectedOutput: assistantMessage()` is different. Its visible
683
- assistant response is the node output, can appear before later nodes, and also
684
- works inside an included workflow. A root `presentationPrompt` would add a
685
- second response, so workflows that end with assistant-message output normally
686
- omit it.
687
-
688
- Presentation is outside the workflow graph: it cannot route to another node,
689
- change the run status, or alter the SQLite run state. If prompt generation or message
690
- delivery fails, the extension reports a warning and leaves the finished run
691
- unchanged. Opting in adds one hidden custom message and one assistant response
692
- to the normal Pi session; it adds no other persistent data and uses no Pi
693
- internals.
698
+ A headless controller child cannot produce a visible assistant message without
699
+ an approved origin-session binding. Use structured agent output for detached
700
+ work. Terminal run state does not create an extra model turn.
694
701
 
695
702
  ## Runtime behavior
696
703
 
@@ -701,35 +708,31 @@ possible. Defaults worth knowing:
701
708
  - Node timeout is 15 minutes unless the node sets `timeoutMs` to a positive
702
709
  number or context callback. A timed-out node has outcome `timed_out` and can
703
710
  be routed with `$result.outcome`. A timed-out agent node also aborts its Pi
704
- turn, and late output for that attempt is rejected.
711
+ turn, and late output for that attempt is rejected. Interactive runs save the
712
+ resolved wall-clock deadline before they park. The host enforces that deadline
713
+ while Pi is closed and after host restart.
705
714
  - `maxSteps` (workflow-level, default 100) bounds loops built from cycles in
706
715
  the graph.
707
- - `/workflow pause` requests a pause: the current step finishes normally,
708
- then the run holds at the step boundary (`paused: true` in the run state,
709
- `run_paused` in the trace) until `/workflow resume` or `/workflow cancel`.
710
- Pausing never interrupts a node mid-flight.
711
- - Interrupting a turn (escape) auto-pauses the run: the pending agent step is
712
- held without nudges and the engine pauses at the next boundary. Node
713
- timeouts keep ticking while held, so a long-abandoned step still times out.
714
- `/workflow resume` re-delivers the pending step prompt.
715
- - A model-started workflow is persisted as `queued` with its final run ID before the start tool
716
- returns. Activation waits for the initiating agent turn to settle, then moves through `starting`
717
- and `running`. `workflow status` and `workflow cancel` accept the run ID before a SQLite run state
718
- exists.
719
- - If deferred activation fails, the queue stores a bounded safe error, releases the session
720
- reservation, and creates one deferred-turn intent for the initiating session. A workflow that
721
- reports `started` and then crashes before its first prompt follows the same path. The model gets
722
- one factual follow-up after settlement and can make a new explicit start call. Pi Workflows does
723
- not retry blindly.
724
- - An agent-issued `workflow cancel` aborts the current node and the current Pi turn, then creates
725
- one deferred-turn intent. The next natural workflow message resolves it when possible; otherwise
726
- one factual fallback starts after settlement. Direct `/workflow cancel` remains quiet because it
727
- is explicit user control. When no run is live but the widget still shows a parked or finished run,
728
- the command clears the widget.
729
- - One workflow runs per session at a time.
730
- - After the workflow tool accepts an agent-step submission, any assistant text that follows remains visible. The next workflow message continues the graph. A deferred intent makes a workflow prompt, presentation, and factual fallback compete to provide one successor turn, so an abort cannot produce two continuation turns.
731
- - Agent nudges: if the model ends its turn without submitting the pending
732
- step, it gets a reminder, twice by default, then the step fails.
716
+ - `/workflow pause` atomically parks the run with `paused: true`, stores the
717
+ receipt, and fences the worker before process-group shutdown. `/workflow
718
+ resume` takes a new generation and reruns only work after the last durable
719
+ boundary.
720
+ - Resuming an active run adopts the existing work. Duplicate start, control,
721
+ update, and submission messages return their stored receipts.
722
+ - A start is committed as `queued` with its final run ID before the command
723
+ reports success. Cancellation can use that run ID before its scheduled worker
724
+ starts. Active cancellation commits its terminal state and command receipt
725
+ together before worker shutdown. It cancels effects that have not started and
726
+ marks applying effects ambiguous for explicit recovery. `workflow status` and
727
+ `workflow cancel` can use the run ID immediately.
728
+ - One interactive workflow request is presented per Pi session. Other requests
729
+ remain durable and ordered.
730
+ - Each protected write renews only its exact live token and generation in the
731
+ same transaction. Claim loss does not write a failed run event.
732
+ - An uncommitted pure or idempotent node can run again after a worker crash. An
733
+ uncertain manual effect parks as ambiguous and never retries automatically.
734
+ - Host status reports safe counts and timestamps. It does not report session
735
+ IDs, project paths, prompts, payloads, tokens, process IDs, or credentials.
733
736
 
734
737
  ## Workflows started by controllers
735
738
 
@@ -754,14 +757,16 @@ if (run.state !== "succeeded") {
754
757
  }
755
758
  ```
756
759
 
757
- Child workflow completion queues the parent resource again. A running child left by a stopped host is recorded as a failed SQLite run state with a `run_interrupted` event. The controller treats that child attempt as interrupted, and the next parent reconciliation starts another immutable attempt. Consequential external mutations should use the controller effect API so uncertain results are observed before retry.
760
+ Child workflow completion queues the parent resource again. The global host runs the child through the same queue and supervised worker model as any other run. A host or worker crash resumes the existing durable run when its committed effect state makes that safe. Consequential external mutations belong in the workflow or controller effect API; an uncertain result stops for explicit recovery.
758
761
 
759
762
  See [CONTROLLERS.md](CONTROLLERS.md) for controller definitions and the full recovery contract.
760
763
 
761
- ## Using the engine outside pi
764
+ ## Using the engine outside Pi
762
765
 
763
- The engine is pi-agnostic. `WorkflowEngine` takes any `AgentStepExecutor`, so
764
- tests (and other hosts) can script agent steps:
766
+ The engine remains Pi-agnostic. `WorkflowEngine` takes any `AgentStepExecutor`,
767
+ so tests and custom library integrations can script agent steps. The package's
768
+ production extension does not use this as a selectable embedded runtime; it
769
+ always sends runs to the global host.
765
770
 
766
771
  ```typescript
767
772
  import { WorkflowEngine, type AgentStepExecutor } from "@osolmaz/pi-workflows";
@@ -774,6 +779,9 @@ const executor: AgentStepExecutor = {
774
779
  },
775
780
  };
776
781
 
777
- const engine = new WorkflowEngine({ executor, outputRoot: "/tmp/runs" });
782
+ const engine = new WorkflowEngine({
783
+ executor,
784
+ databasePath: "/tmp/workflow-state.sqlite",
785
+ });
778
786
  const { state } = await engine.run(workflow, { task: "..." });
779
787
  ```
@@ -2,6 +2,7 @@ import {
2
2
  action,
3
3
  compute,
4
4
  defineWorkflow,
5
+ manualEffect,
5
6
  runCommandBatch,
6
7
  type CommandBatchRequest,
7
8
  } from "@osolmaz/pi-workflows";
@@ -40,6 +41,7 @@ export default defineWorkflow({
40
41
  }) satisfies CommandBatchRequest,
41
42
  }),
42
43
  run: action({
44
+ effect: manualEffect("example.command-batch.run"),
43
45
  run: async (context) =>
44
46
  await runCommandBatch(context.outputs.prepare as CommandBatchRequest, {
45
47
  signal: context.signal,
@@ -1,4 +1,4 @@
1
- import { compute, defineWorkflow, shell } from "@osolmaz/pi-workflows";
1
+ import { compute, defineWorkflow, manualEffect, shell } from "@osolmaz/pi-workflows";
2
2
 
3
3
  type ShellInput = {
4
4
  text?: string;
@@ -10,6 +10,7 @@ export default defineWorkflow({
10
10
  startAt: "echo_text",
11
11
  nodes: {
12
12
  echo_text: shell({
13
+ effect: manualEffect("example.shell.echo-text"),
13
14
  exec: ({ input }) => ({
14
15
  command: "printf",
15
16
  args: ["%s", (input as ShellInput).text ?? "hello from pi-workflows"],
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "osolmaz.pi-workflows"
2
2
  name = "pi-workflows"
3
- version = "0.13.4"
3
+ version = "0.15.0"
4
4
  min_herdr_version = "0.7.0"
5
5
  description = "Open the active pi-workflows run in piw from a managed Herdr pane."
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.13.4",
3
+ "version": "0.15.0",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: autodoc
3
- description: Use when an existing selected solution or clear implementation plan must be recorded or updated in canonical documentation before implementation, including choosing the right repository and applying SimpleDoc conventions.
3
+ description: Records or updates an existing selected plan in canonical documentation without implementing it. Use only when the user explicitly asks to run autodoc.
4
4
  compatibility: Requires pi-workflows and the built-in autodoc workflow.
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: autoimplement
3
- description: Use when the user asks to implement a plan end-to-end, test it, run pi-reviewer against the base branch in a loop until no P0/P1 issues remain, and make sure CI/CD is green before finishing.
3
+ description: Implements an existing plan end to end, tests it, runs pi-reviewer until no P0/P1 issues remain, and verifies CI/CD. Use only when the user explicitly asks to run autoimplement.
4
4
  compatibility: Requires Pi Workflows and the built-in autoimplement workflow.
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: autoplan
3
- description: Use when the user asks to devise, choose, or plan the most Long term elegant and production ready solution, compare it with the Holy grail, and produce the best practical in-scope implementation plan without asking the user to resolve the gap.
3
+ description: Compares practical solutions with the ideal end state, selects the best in-scope option, and produces an implementation plan. Use only when the user explicitly asks to run autoplan.
4
4
  compatibility: Requires pi-workflows and the built-in autoplan workflow.
5
5
  ---
6
6
 
@@ -22,6 +22,8 @@ Use the smallest applicable action:
22
22
  - `update` publishes a non-completing durable update for the active step attempt.
23
23
  - `submit` completes an active submitted agent step with its required output. An assistant-message step completes through its normal visible reply instead.
24
24
 
25
+ When the user asks to continue or resume the active workflow, call `workflow` with `action: "resume"` immediately. Do not use `workflow status` as a substitute or prerequisite.
26
+
25
27
  Use `start` only once for one requested run. Before starting, load the matching workflow skill when one exists and build its complete input. Include scope, authority, constraints, identifiers, and finish criteria required by that skill. Do not start with placeholders that still need user or model repair.
26
28
 
27
29
  For a workflow without a specialized skill, inspect its input contract and make one complete call. For example:
@@ -11,6 +11,7 @@ import {
11
11
  defineWorkflow,
12
12
  includeWorkflow,
13
13
  includedResult,
14
+ manualEffect,
14
15
  } from "../workflows/definition.js";
15
16
  import { digest } from "../workflows/human-decision.js";
16
17
  import { allowSettingsPath, workflowSettings } from "../workflows/settings.js";
@@ -1768,6 +1769,7 @@ export const autoimplementWorkflow = defineWorkflow({
1768
1769
  run: selectReviewCommands,
1769
1770
  }),
1770
1771
  runReview: action({
1772
+ effect: manualEffect("pi-workflows.autoimplement.review"),
1771
1773
  statusDetail: "running pi-reviewer commands",
1772
1774
  timeoutMs: (context) => {
1773
1775
  const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
@@ -1884,6 +1886,7 @@ export const autoimplementWorkflow = defineWorkflow({
1884
1886
  validate: parseCiInspectionForPublished,
1885
1887
  }),
1886
1888
  trackCi: action({
1889
+ effect: manualEffect("pi-workflows.autoimplement.track-ci"),
1887
1890
  statusDetail: "tracking pending CI commands",
1888
1891
  timeoutMs: (context) => {
1889
1892
  const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);