@osolmaz/pi-workflows 0.12.1 → 0.13.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 (233) hide show
  1. package/README.md +59 -33
  2. package/dist/builtins/autodoc.workflow.d.ts +194 -7
  3. package/dist/builtins/autodoc.workflow.js +156 -30
  4. package/dist/builtins/autodoc.workflow.js.map +1 -1
  5. package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
  6. package/dist/builtins/autoimplement-command-batches.js +29 -31
  7. package/dist/builtins/autoimplement-command-batches.js.map +1 -1
  8. package/dist/builtins/autoimplement.workflow.d.ts +1619 -97
  9. package/dist/builtins/autoimplement.workflow.js +416 -153
  10. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  11. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  12. package/dist/builtins/autoplan.workflow.js +200 -29
  13. package/dist/builtins/autoplan.workflow.js.map +1 -1
  14. package/dist/builtins/catalog.js +7 -5
  15. package/dist/builtins/catalog.js.map +1 -1
  16. package/dist/builtins/change-verification.workflow.d.ts +110 -0
  17. package/dist/builtins/change-verification.workflow.js +860 -0
  18. package/dist/builtins/change-verification.workflow.js.map +1 -0
  19. package/dist/builtins/index.d.ts +2 -1
  20. package/dist/builtins/index.js +1 -0
  21. package/dist/builtins/index.js.map +1 -1
  22. package/dist/builtins/monitor.workflow.d.ts +56 -21
  23. package/dist/builtins/monitor.workflow.js +499 -217
  24. package/dist/builtins/monitor.workflow.js.map +1 -1
  25. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  26. package/dist/builtins/plain-summary.workflow.js +134 -0
  27. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  28. package/dist/builtins/plan-change.workflow.d.ts +527 -26
  29. package/dist/builtins/plan-change.workflow.js +26 -0
  30. package/dist/builtins/plan-change.workflow.js.map +1 -1
  31. package/dist/builtins/sanity-check.workflow.d.ts +45 -3
  32. package/dist/builtins/sanity-check.workflow.js +45 -7
  33. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  34. package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
  35. package/dist/builtins/workspace-preparation.workflow.js +498 -0
  36. package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
  37. package/dist/controllers/effects.d.ts +3 -2
  38. package/dist/controllers/effects.js +8 -1
  39. package/dist/controllers/effects.js.map +1 -1
  40. package/dist/controllers/index.d.ts +1 -1
  41. package/dist/controllers/index.js +1 -1
  42. package/dist/controllers/index.js.map +1 -1
  43. package/dist/controllers/manager.d.ts +1 -0
  44. package/dist/controllers/manager.js +41 -17
  45. package/dist/controllers/manager.js.map +1 -1
  46. package/dist/controllers/sqlite.d.ts +107 -85
  47. package/dist/controllers/sqlite.js +1611 -1486
  48. package/dist/controllers/sqlite.js.map +1 -1
  49. package/dist/controllers/store.d.ts +9 -8
  50. package/dist/controllers/store.js +1 -31
  51. package/dist/controllers/store.js.map +1 -1
  52. package/dist/controllers/types.d.ts +3 -0
  53. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  54. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  55. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  56. package/dist/controllers/workflows.d.ts +2 -2
  57. package/dist/controllers/workflows.js +11 -10
  58. package/dist/controllers/workflows.js.map +1 -1
  59. package/dist/extension/controller-host.js +2 -2
  60. package/dist/extension/controller-host.js.map +1 -1
  61. package/dist/extension/decision-channels.d.ts +2 -0
  62. package/dist/extension/decision-channels.js +311 -118
  63. package/dist/extension/decision-channels.js.map +1 -1
  64. package/dist/extension/executor.d.ts +9 -2
  65. package/dist/extension/executor.js +90 -6
  66. package/dist/extension/executor.js.map +1 -1
  67. package/dist/extension/herdr-viewer.d.ts +0 -1
  68. package/dist/extension/herdr-viewer.js +0 -4
  69. package/dist/extension/herdr-viewer.js.map +1 -1
  70. package/dist/extension/index.js +156 -117
  71. package/dist/extension/index.js.map +1 -1
  72. package/dist/extension/recorder.d.ts +3 -10
  73. package/dist/extension/recorder.js +13 -22
  74. package/dist/extension/recorder.js.map +1 -1
  75. package/dist/extension/step-message.d.ts +7 -2
  76. package/dist/extension/step-message.js +94 -2
  77. package/dist/extension/step-message.js.map +1 -1
  78. package/dist/extension/widget.js +6 -0
  79. package/dist/extension/widget.js.map +1 -1
  80. package/dist/host/rpc-executor.d.ts +3 -0
  81. package/dist/host/rpc-executor.js +2 -0
  82. package/dist/host/rpc-executor.js.map +1 -1
  83. package/dist/host/runner.d.ts +3 -6
  84. package/dist/host/runner.js +83 -32
  85. package/dist/host/runner.js.map +1 -1
  86. package/dist/render/graph-render.js +14 -8
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.d.ts +35 -0
  89. package/dist/state/database.js +287 -0
  90. package/dist/state/database.js.map +1 -0
  91. package/dist/state/index.d.ts +4 -0
  92. package/dist/state/index.js +5 -0
  93. package/dist/state/index.js.map +1 -0
  94. package/dist/state/json.d.ts +6 -0
  95. package/dist/state/json.js +38 -0
  96. package/dist/state/json.js.map +1 -0
  97. package/dist/state/mutation.d.ts +57 -0
  98. package/dist/state/mutation.js +255 -0
  99. package/dist/state/mutation.js.map +1 -0
  100. package/dist/state/schema.d.ts +6 -0
  101. package/dist/state/schema.js +473 -0
  102. package/dist/state/schema.js.map +1 -0
  103. package/dist/viewer/cli.d.ts +2 -2
  104. package/dist/viewer/cli.js +142 -122
  105. package/dist/viewer/cli.js.map +1 -1
  106. package/dist/viewer/render.d.ts +4 -4
  107. package/dist/viewer/render.js +2 -17
  108. package/dist/viewer/render.js.map +1 -1
  109. package/dist/viewer/session-reducer.d.ts +1 -1
  110. package/dist/viewer/session-reducer.js +1 -1
  111. package/dist/viewer/tui.d.ts +3 -3
  112. package/dist/viewer/tui.js +15 -13
  113. package/dist/viewer/tui.js.map +1 -1
  114. package/dist/viewer/watch.d.ts +2 -5
  115. package/dist/viewer/watch.js +13 -13
  116. package/dist/viewer/watch.js.map +1 -1
  117. package/dist/workflows/composition.js +12 -2
  118. package/dist/workflows/composition.js.map +1 -1
  119. package/dist/workflows/definition.d.ts +6 -2
  120. package/dist/workflows/definition.js +17 -0
  121. package/dist/workflows/definition.js.map +1 -1
  122. package/dist/workflows/engine.d.ts +6 -6
  123. package/dist/workflows/engine.js +183 -103
  124. package/dist/workflows/engine.js.map +1 -1
  125. package/dist/workflows/errors.d.ts +3 -3
  126. package/dist/workflows/errors.js +3 -3
  127. package/dist/workflows/human-decision.d.ts +29 -4
  128. package/dist/workflows/human-decision.js +547 -283
  129. package/dist/workflows/human-decision.js.map +1 -1
  130. package/dist/workflows/index.d.ts +4 -5
  131. package/dist/workflows/index.js +3 -4
  132. package/dist/workflows/index.js.map +1 -1
  133. package/dist/workflows/schema.js +17 -1
  134. package/dist/workflows/schema.js.map +1 -1
  135. package/dist/workflows/store.d.ts +83 -103
  136. package/dist/workflows/store.js +902 -951
  137. package/dist/workflows/store.js.map +1 -1
  138. package/dist/workflows/types.d.ts +55 -81
  139. package/docs/CONTROLLERS.md +10 -8
  140. package/docs/DEFERRED_TURNS.md +2 -2
  141. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  142. package/docs/HUMAN_DECISIONS.md +13 -14
  143. package/docs/MONITOR.md +179 -231
  144. package/docs/SQLITE_STATE.md +219 -0
  145. package/docs/WORKFLOW_COMPOSITION.md +27 -19
  146. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  147. package/docs/WORKFLOW_UPDATES.md +10 -9
  148. package/docs/development.md +9 -9
  149. package/docs/live-replay-protocol.md +28 -37
  150. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  151. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  152. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  153. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
  154. package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
  155. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  156. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  157. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  158. package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
  159. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  160. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  161. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  162. package/docs/session-event-journal.md +63 -434
  163. package/docs/tui-viewer.md +13 -15
  164. package/docs/workflows.md +111 -62
  165. package/examples/workflows/plain-summary.workflow.ts +1 -0
  166. package/herdr-plugin.toml +1 -1
  167. package/package.json +1 -1
  168. package/plugins/herdr/viewer.mjs +1 -11
  169. package/skills/autodoc/SKILL.md +7 -0
  170. package/skills/autoimplement/SKILL.md +4 -0
  171. package/skills/autoplan/SKILL.md +5 -4
  172. package/skills/monitor/SKILL.md +91 -163
  173. package/skills/pi-workflows/SKILL.md +7 -7
  174. package/skills/sanity-check/SKILL.md +1 -1
  175. package/src/builtins/autodoc.workflow.ts +184 -33
  176. package/src/builtins/autoimplement-command-batches.ts +39 -33
  177. package/src/builtins/autoimplement.workflow.ts +483 -175
  178. package/src/builtins/autoplan.workflow.ts +290 -40
  179. package/src/builtins/catalog.ts +7 -5
  180. package/src/builtins/change-verification.workflow.ts +1143 -0
  181. package/src/builtins/index.ts +7 -1
  182. package/src/builtins/monitor.workflow.ts +667 -250
  183. package/src/builtins/plain-summary.workflow.ts +185 -0
  184. package/src/builtins/plan-change.workflow.ts +35 -0
  185. package/src/builtins/sanity-check.workflow.ts +62 -7
  186. package/src/builtins/workspace-preparation.workflow.ts +668 -0
  187. package/src/controllers/effects.ts +7 -0
  188. package/src/controllers/index.ts +0 -6
  189. package/src/controllers/manager.ts +72 -32
  190. package/src/controllers/sqlite.ts +2420 -1948
  191. package/src/controllers/store.ts +17 -38
  192. package/src/controllers/types.ts +3 -0
  193. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  194. package/src/controllers/workflows.ts +45 -21
  195. package/src/extension/controller-host.ts +1 -2
  196. package/src/extension/decision-channels.ts +427 -144
  197. package/src/extension/executor.ts +106 -6
  198. package/src/extension/herdr-viewer.ts +0 -5
  199. package/src/extension/index.ts +180 -132
  200. package/src/extension/recorder.ts +13 -22
  201. package/src/extension/step-message.ts +106 -4
  202. package/src/extension/widget.ts +8 -0
  203. package/src/host/rpc-executor.ts +4 -0
  204. package/src/host/runner.ts +93 -41
  205. package/src/render/graph-render.ts +11 -4
  206. package/src/state/database.ts +375 -0
  207. package/src/state/index.ts +29 -0
  208. package/src/state/json.ts +42 -0
  209. package/src/state/mutation.ts +435 -0
  210. package/src/state/schema.ts +475 -0
  211. package/src/viewer/cli.ts +151 -144
  212. package/src/viewer/render.ts +12 -24
  213. package/src/viewer/session-reducer.ts +1 -1
  214. package/src/viewer/tui.ts +18 -17
  215. package/src/viewer/watch.ts +13 -17
  216. package/src/workflows/composition.ts +12 -3
  217. package/src/workflows/definition.ts +36 -3
  218. package/src/workflows/engine.ts +212 -116
  219. package/src/workflows/errors.ts +3 -3
  220. package/src/workflows/human-decision.ts +795 -340
  221. package/src/workflows/index.ts +15 -19
  222. package/src/workflows/schema.ts +19 -1
  223. package/src/workflows/store.ts +1295 -1163
  224. package/src/workflows/types.ts +61 -81
  225. package/dist/workflows/artifacts.d.ts +0 -40
  226. package/dist/workflows/artifacts.js +0 -155
  227. package/dist/workflows/artifacts.js.map +0 -1
  228. package/dist/workflows/migrate-sources.d.ts +0 -42
  229. package/dist/workflows/migrate-sources.js +0 -133
  230. package/dist/workflows/migrate-sources.js.map +0 -1
  231. package/docs/run-bundles.md +0 -481
  232. package/src/workflows/artifacts.ts +0 -188
  233. package/src/workflows/migrate-sources.ts +0 -178
@@ -1,481 +0,0 @@
1
- # Run bundle format
2
-
3
- Every workflow run persists to its own directory, called a run bundle. The
4
- bundle is the contract between the engine and anything that observes runs: the
5
- bundled terminal viewer, the Rust TUI, and any external tool. A bundle is
6
- **self-contained for replay**: a reader never needs access to Pi's global
7
- session store or any other file outside the bundle directory.
8
-
9
- This document is the authoritative specification. There is exactly one format
10
- version; older layouts are not read and no compatibility paths exist.
11
-
12
- ## Location and layout
13
-
14
- Bundles live under `~/.pi/agent/workflows/runs/` by default. The
15
- `PI_WORKFLOWS_RUNS_DIR` environment variable overrides the location for both
16
- the engine and all viewers, which is how the test suite keeps runs inside
17
- temporary directories.
18
-
19
- ```
20
- ~/.pi/agent/workflows/runs/
21
- 20260729T023912Z-autoimplement-3f2a9c1b/
22
- manifest.json # pi-workflows.run-bundle.v1
23
- workflow.json # pi-workflows.definition-snapshot.v1
24
- state.json # pi-workflows.run-state.v1, derived projection
25
- trace.ndjson # pi-workflows.trace-event.v1, append-only source of truth
26
- session/ # present when the run executed inside a Pi conversation
27
- binding.json # pi-workflows.session-binding.v1
28
- entries.ndjson # pi-workflows.session-entry.v1, append-only
29
- events.ndjson # pi-workflows.session-event.v1, append-only
30
- capture.json # pi-workflows.session-capture.v1, atomic projection
31
- artifacts/ # present when any persisted value was externalized
32
- sha256-<64 hex>.txt
33
- ```
34
-
35
- Human decision records use a separate additive directory next to `runs/` so a waiting run bundle remains immutable:
36
-
37
- ```text
38
- ~/.pi/agent/workflows/decisions/
39
- <decision-id>/
40
- request.json
41
- deliveries/<channel>/<attempt-id>.json
42
- answers/<attempt-id>.json
43
- resolution.json # atomic resolved-or-cancelled fence
44
- accepted.json # human or timeout response with explicit provenance
45
- cancelled.json # terminal cancellation tombstone
46
- settlements/<channel>/<attempt-id>.json
47
- continuation.json
48
- ```
49
-
50
- The request links to the waiting run, node, attempt, workflow source, and canonical request digest. The single v1 request stores the canonical subject and a separate normalized operator presentation. Its subject, presentation, revision, choices, input prompts, optional deadline, and optional automatic response are bound to the request digest. Accepted records and redacted continuation receipts preserve the subject and presentation digests and state `human` or `timeout` provenance. A timeout record has no human source. Final records use no-replace creation and adopt only identical retries. `resolution.json` is the first resolved-or-cancelled fence. It materializes either `accepted.json` or `cancelled.json`; a crash can rebuild that detail from the resolution. A cancellation tombstone prevents later automatic continuation. `continuation.json` binds one resolved response and its provenance to one deterministic continuation run. Delivery and settlement records cannot change the result. Older human-decision record shapes are incompatible alpha state and require reset; there is no migration reader.
51
-
52
- Telegram multipart delivery uses additive v2 delivery records for the overall intent, each part, and completion. Part records contain only recipient indexes, part indexes, counts, and content digests. Telegram chat and message IDs remain in the private disposable channel projection and never enter run or decision bundles. An ambiguous part remains unknown and is not retried blindly.
53
-
54
- A human-decision continuation preserves the parent's original workflow input and replaces the carried checkpoint output with the resolved typed response for routing. Its `humanDecision` state is a redacted receipt with `human` or `timeout` provenance. A v2 receipt includes the subject digest, presentation digest, and revision, but not the subject itself. Verified human actor, channel, event, and idempotency details remain in the private sibling decision records and are not copied into the run bundle. Ordinary checkpoint continuations keep using the answer as the continuation input. This alpha contract changes current v1 and v2 field sets in place; old active definitions refuse resume instead of using a compatibility path.
55
-
56
- Run ids are `<UTC timestamp>-<workflow slug>-<8 hex chars>`, so lexical order
57
- is chronological order.
58
-
59
- Bundle directories are created with mode `0700` and files with mode `0600`.
60
- Bundles can contain prompts, model output, shell commands, environment
61
- details, and absolute paths; treat them as private data and review before
62
- exporting.
63
-
64
- ## Source of truth and write discipline
65
-
66
- `trace.ndjson` is the source of truth for workflow execution. Final Pi
67
- conversation entries and temporal session history have separate authority in
68
- `session/entries.ndjson` and `session/events.ndjson`. `session/capture.json`
69
- reports whether temporal capture is complete. These sequence spaces are
70
- independent and must not be compared.
71
-
72
- Write order for every transition:
73
-
74
- 1. Append the trace event (one JSON object per line, appends serialized per
75
- file, `seq` starting at 1 and increasing by exactly 1).
76
- 2. Atomically replace `state.json`, carrying `traceSeq` = the `seq` of the
77
- trace event it reflects (write to a temp file in the same directory, then
78
- rename).
79
- 3. Atomically replace `manifest.json`.
80
-
81
- Consequences for readers:
82
-
83
- - A reader never sees a partial JSON document; a torn final trace line must be
84
- ignored.
85
- - `state.json` with `traceSeq` older than the last trace line is a stale
86
- projection: either re-read after the writer catches up or fold the trace
87
- tail on top of it.
88
- - Before the engine writes a terminal workflow event, session recording stops,
89
- drains accepted entries and events, and atomically writes `capture.json`.
90
- - After a run reaches a terminal status (`completed`, `failed`, `timed_out`,
91
- `cancelled`, or `waiting`), the bundle no longer changes, and
92
- `state.traceSeq` equals the final trace `seq`.
93
- - A bundle whose state is `running` but whose files have stopped growing may
94
- be an interrupted run (crash, reboot); viewers should label it as possibly
95
- interrupted rather than live.
96
-
97
- ## Externalized values and artifacts
98
-
99
- Large payloads are stored once, content-addressed, under `artifacts/` and
100
- referenced from the documents that use them. This applies uniformly to every
101
- **persisted value position**. These positions include `input`, `outputs.*`,
102
- `results.*.output`, `steps[*].prompt`, `steps[*].output`, and `finalOutput`.
103
- Trace event payload values follow the same rule.
104
-
105
- Encoding rule, applied recursively to a persisted value:
106
-
107
- - A string leaf whose UTF-8 encoding is larger than 4096 bytes is written to
108
- `artifacts/sha256-<digest>.txt` (UTF-8, digest over the exact bytes) and
109
- replaced by an artifact reference:
110
-
111
- ```json
112
- {
113
- "$artifact": {
114
- "path": "artifacts/sha256-2b1f….txt",
115
- "mediaType": "text/plain",
116
- "bytes": 18342,
117
- "sha256": "2b1f…"
118
- }
119
- }
120
- ```
121
-
122
- - Any user object that has an own key `$artifact` or `$escaped` is wrapped as
123
- `{ "$escaped": <object> }` so the sentinel stays unambiguous. Decoders
124
- unwrap `$escaped` one level and resolve `$artifact` refs.
125
- - Everything else is stored inline. Small values are never externalized.
126
-
127
- Artifact rules:
128
-
129
- - `path` is bundle-relative; a reference never points outside the bundle.
130
- - Artifacts are immutable once written and deduplicate by content hash.
131
- - Readers must tolerate unknown `mediaType` values.
132
-
133
- The same output can legitimately appear in several places. Externalization
134
- keeps each copy in `outputs`, `results`, `steps`, or the trace as the same small
135
- reference.
136
-
137
- ## manifest.json
138
-
139
- Identity and pointers, kept in sync with the state on every snapshot:
140
-
141
- ```json
142
- {
143
- "schema": "pi-workflows.run-bundle.v1",
144
- "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
145
- "workflowName": "autoimplement",
146
- "runTitle": "autoimplement: fix the flaky test",
147
- "workflowSource": {
148
- "kind": "file",
149
- "path": "/repo/.pi/workflows/autoimplement.workflow.ts",
150
- "hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
151
- },
152
- "startedAt": "2026-07-29T02:39:12.412Z",
153
- "finishedAt": "2026-07-29T02:41:03.977Z",
154
- "status": "completed",
155
- "traceSchema": "pi-workflows.trace-event.v1",
156
- "paths": {
157
- "workflow": "workflow.json",
158
- "state": "state.json",
159
- "trace": "trace.ndjson",
160
- "session": "session",
161
- "artifacts": "artifacts"
162
- }
163
- }
164
- ```
165
-
166
- `workflowSource` identifies the root definition used by the run. User workflow
167
- files use an absolute path and SHA-256 hash. Package-provided workflows use a
168
- stable identity such as `{ "kind": "builtin", "id": "monitor", "revision": "4" }`.
169
- A built-in identity does not contain an installation path.
170
-
171
- A composed run also records `workflowSources`, sorted by mount path, and `definitionDigest`. Each mounted source has `mountPath`, `workflowName`, and the same file or built-in source identity. The digest is SHA-256 over the resolved definition snapshot.
172
-
173
- `paths.artifacts` is declared from bundle creation so a live session-event
174
- patch can safely reference a newly written artifact before the next workflow
175
- state projection. The directory itself is created only when needed.
176
- `paths.session` appears when the run binds to Pi. Readers must start from
177
- `manifest.json`, check `schema`, skip bundles they do not understand, resolve
178
- files through `paths`, and reject any path that escapes the bundle directory.
179
-
180
- ## workflow.json
181
-
182
- A serializable snapshot of the graph taken at run start
183
- (`pi-workflows.definition-snapshot.v1`). Functions such as prompts and
184
- validators are not serialized. Each node keeps only its metadata (`nodeType`,
185
- `timeoutMs`, `statusDetail`, `expectedOutput`, `summary`, `actionExecution`),
186
- and edges are copied verbatim. A fixed `timeoutMs: null` is preserved and means
187
- that the node has no wall-clock deadline. Timeout callbacks remain omitted.
188
- A human-decision snapshot records fixed `onTimeout` duration and response values. A dynamic timeout callback is omitted and marked as dynamic. Included nodes also record `mountPath`, `localNodeId`, and internal entry or exit status. The top-level `composition.mounts` list records every mount, entry, named exit, and child step limit. The snapshot is what lets viewers draw all nodes, including ones that have not run yet. It is immutable after run start.
189
-
190
- ## Resume and repair
191
-
192
- An interrupted run (status `running` with no terminal trace event) can resume
193
- instead of failing. Resume is a named operation with strict rules:
194
-
195
- 1. The caller must hold the run's queue claim. Only the current claim holder
196
- may resume or interrupt a bundle, and every bundle write verifies the
197
- claim token first (write fencing).
198
- 2. A torn trace tail (a crash mid-append) is truncated to the last complete
199
- line. Trace events the state projection never recorded are dropped, so
200
- `state.traceSeq` and the trace agree again before any new event.
201
- 3. Completed nodes replay from the projection. The in-flight node reruns with
202
- a fresh attempt; a `run_resumed` trace event marks the boundary.
203
- 4. `state.workflowSource` pins the root workflow source from run start. File
204
- sources require the same hash. Built-in sources require the same catalog
205
- id and revision.
206
- 5. Composed runs also require the same sorted `workflowSources` and
207
- `definitionDigest`. A changed or missing child refuses normal resume.
208
- 6. A forced resume records the identity mismatch in the `run_resumed` payload.
209
-
210
- Continuation runs (answering a checkpoint) are new bundles, not resumed ones.
211
- They link back through `state.parentRunId`, carry the parent's outputs,
212
- results, and step records forward, and note `continuedFrom` in their
213
- `run_started` payload. Bundles stay append-only; a continuation is the only
214
- way work follows a terminal `waiting` state.
215
-
216
- ## state.json
217
-
218
- The full run projection (`WorkflowRunState` in
219
- [`src/workflows/types.ts`](../src/workflows/types.ts)), schema
220
- `pi-workflows.run-state.v1`:
221
-
222
- ```json
223
- {
224
- "schema": "pi-workflows.run-state.v1",
225
- "traceSeq": 17,
226
- "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
227
- "workflowName": "autoimplement",
228
- "workflowSource": {
229
- "kind": "file",
230
- "path": "/repo/.pi/workflows/autoimplement.workflow.ts",
231
- "hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
232
- },
233
- "workflowSources": [
234
- {
235
- "mountPath": ["redesign"],
236
- "workflowName": "autoplan",
237
- "source": { "kind": "builtin", "id": "autoplan", "revision": "1" }
238
- }
239
- ],
240
- "definitionDigest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
241
- "startedAt": "…",
242
- "updatedAt": "…",
243
- "status": "running",
244
- "input": { "task": "fix the flaky test" },
245
- "outputs": {},
246
- "results": {},
247
- "steps": [],
248
- "updates": []
249
- }
250
- ```
251
-
252
- - `workflowSource` is the canonical root source identity. Resuming a file requires
253
- the same hash. Resuming a built-in requires the same catalog revision.
254
- - `workflowSources` and `definitionDigest` attest the complete composed graph.
255
- A mismatch refuses resume instead of loading another child definition.
256
- - `status` is one of `running`, `waiting`, `completed`, `failed`, `timed_out`,
257
- or `cancelled`. A controller host records an abandoned bundle as `failed`
258
- with a final `run_interrupted` trace event. Before doing that, recovery checks
259
- the trace tail and repairs a stale projection when the terminal event was
260
- already appended. The controller store can then retry a genuinely abandoned
261
- child attempt without changing this schema. If startup stopped before a
262
- manifest existed, the scheduler preserves that incomplete directory as a
263
- hidden sibling before creating the reserved run.
264
- - While a node is executing, `currentNode` and `currentAttemptId` identify it.
265
- `currentNodeStartedAt` and `statusDetail` add timing and display context.
266
- These fields disappear when the node finishes. The executing node's type comes from the definition
267
- snapshot, not from the state.
268
- - While a pause request holds the run at a step boundary, `paused` is `true`
269
- (with matching `run_paused`/`run_resumed` trace events); it disappears when
270
- the run resumes or ends.
271
- - Per-node data lives in `outputs` (the accepted output of each finished node,
272
- latest attempt wins on loops) and `results` (the full result record of the
273
- latest attempt, including outcome and timing).
274
- - `updates` contains the latest durable record for each `(type, key)` pair,
275
- sorted by trace sequence. New runs start with an empty array. Older bundles
276
- can omit it. Resume keeps it; checkpoint continuation starts a new empty
277
- projection. The trace keeps the complete update history.
278
- - `steps` is the ordered history, one record per node attempt:
279
-
280
- ```json
281
- {
282
- "attemptId": "d81f…",
283
- "nodeId": "implement",
284
- "nodeType": "agent",
285
- "outcome": "ok",
286
- "startedAt": "…",
287
- "finishedAt": "…",
288
- "prompt": {
289
- "$artifact": {
290
- "path": "artifacts/sha256-….txt",
291
- "mediaType": "text/plain",
292
- "bytes": 9120,
293
- "sha256": "…"
294
- }
295
- },
296
- "output": { "summary": "…" },
297
- "conversation": { "firstEntryId": "a1b2c3d4", "lastEntryId": "c3d4e5f6" }
298
- }
299
- ```
300
-
301
- - `prompt` is the full prompt text for agent steps (`null` for other node
302
- types), subject to value externalization.
303
- - `conversation` is present on agent steps recorded inside a Pi conversation:
304
- the inclusive range of Pi session entry ids in `session/entries.ndjson`
305
- produced by this attempt, from prompt delivery through accepted submission.
306
- Viewers must use this explicit linkage and never infer it heuristically.
307
- - Action steps carry an `action` receipt with `actionType`
308
- (`shell`/`function`). Shell actions also record `command`, `args`, `cwd`,
309
- `exitCode`, `signal`, and `durationMs`. Shell stdout/stderr live in the step
310
- output (the parsed or raw shell result) and are externalized when large.
311
- - When a run pauses at a checkpoint, `waitingOn` names the checkpoint node.
312
- Terminal runs carry `finalOutput` on success and `error` on failure.
313
-
314
- ## trace.ndjson
315
-
316
- One event per line, monotonically sequenced per run, schema
317
- `pi-workflows.trace-event.v1`:
318
-
319
- ```json
320
- {
321
- "seq": 3,
322
- "at": "2026-07-29T02:39:14.101Z",
323
- "scope": "agent",
324
- "type": "agent_prompt_sent",
325
- "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
326
- "nodeId": "implement",
327
- "attemptId": "d81f…",
328
- "payload": { "prompt": "…" }
329
- }
330
- ```
331
-
332
- `scope` is one of `run`, `node`, `agent`, `action`, or `session`.
333
- A node-scoped `update_published` event carries the runtime update ID, type, key, and data;
334
- its event sequence and timestamp are the update sequence and timestamp. See
335
- [WORKFLOW_UPDATES.md](WORKFLOW_UPDATES.md) for the full contract. `nodeId` and
336
- `attemptId` are present on node-scoped and agent-scoped events. Consumers must
337
- ignore unknown event types and unknown payload fields so new ones can be added
338
- within the same schema version.
339
-
340
- The trace alone is sufficient to reconstruct the run because terminal node
341
- events carry outputs and receipts.
342
-
343
- Event catalog and payload contracts:
344
-
345
- | type | scope | payload |
346
- | ------------------- | ------- | ------------------------------------------------------------------- |
347
- | `run_started` | run | `workflowName`, `runTitle?`, `input` |
348
- | `session_bound` | session | `piSessionId` |
349
- | `node_started` | node | `nodeType`, `statusDetail?` |
350
- | `agent_prompt_sent` | agent | `prompt` |
351
- | `node_finished` | node | `outcome: "ok"`, `durationMs`, `output`, `conversation?`, `action?` |
352
- | `node_failed` | node | `outcome`, `durationMs`, `error`, `conversation?`, `action?` |
353
- | `include_entered` | run | `mountPath`, `workflowName`, `invocation` |
354
- | `include_exited` | run | `mountPath`, `workflowName`, `invocation`, `exit`, `output` |
355
- | `run_paused` | run | _(empty)_ |
356
- | `run_resumed` | run | _(empty)_ |
357
- | `run_completed` | run | `status`, `finalOutput` |
358
- | `run_waiting` | run | `status`, `waitingOn`, `finalOutput` |
359
- | `run_failed` | run | `status`, `error` |
360
- | `run_timed_out` | run | `status`, `error` |
361
- | `run_cancelled` | run | `status`, `error?` |
362
- | `run_interrupted` | run | `error` |
363
-
364
- Invariants:
365
-
366
- - every node attempt has exactly one `node_started` and exactly one terminal
367
- `node_finished`/`node_failed` with the same `attemptId`;
368
- - `attemptId` values are unique within a run;
369
- - a terminal `run_*` event is the last event of the run;
370
- - events are never rewritten or deleted.
371
-
372
- ## session/
373
-
374
- Present when the run executed inside a Pi conversation. The extension records
375
- the conversation into the bundle so replay never depends on Pi's global
376
- session store.
377
-
378
- A run that outlives its first session (parked, then resumed by another
379
- session or the host) gains a second capture under
380
- `session/segments/<attemptId>/` with the same file layout (`binding.json`,
381
- `entries.ndjson`, `events.ndjson`, `capture.json`). The first capture stays
382
- flat at `session/`; only captures from the second bind onward become
383
- segments, so readers that predate segments keep working on single-session
384
- bundles. An interrupted run finalizes any segment still `recording` as
385
- `failed` with the interruption reason.
386
-
387
- ### binding.json
388
-
389
- Written once when the run binds to the conversation
390
- (`pi-workflows.session-binding.v1`):
391
-
392
- ```json
393
- {
394
- "schema": "pi-workflows.session-binding.v1",
395
- "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
396
- "piSessionId": "019fad89-…",
397
- "piSessionFile": "/home/user/.pi/agent/sessions/--repo--/2026-07-29….jsonl",
398
- "cwd": "/repo",
399
- "boundAt": "2026-07-29T02:39:12.412Z"
400
- }
401
- ```
402
-
403
- `piSessionFile` is provenance only and absent for in-memory sessions; replay
404
- readers must not read it.
405
-
406
- ### entries.ndjson
407
-
408
- Append-only copies of the Pi session entries produced on the current branch
409
- while the run was active, schema `pi-workflows.session-entry.v1`:
410
-
411
- ```json
412
- {
413
- "seq": 1,
414
- "at": "2026-07-29T02:39:12.902Z",
415
- "entry": {
416
- "type": "message",
417
- "id": "a1b2c3d4",
418
- "parentId": "9f8e7d6c",
419
- "timestamp": "…",
420
- "message": { "role": "user", "content": "…" }
421
- }
422
- }
423
- ```
424
-
425
- - `seq` is strictly increasing within the file, starting at 1.
426
- - `entry` is the verbatim Pi session entry (Pi's own versioned format),
427
- including user messages, assistant messages, tool results, model changes,
428
- and compaction entries. Nothing is normalized or rewritten.
429
- - Entries include everything that happened in the conversation during the run.
430
- This includes workflow prompts and nudges together with user interruptions.
431
- - `conversation` ranges in step records and `node_finished` events address
432
- entries by Pi entry id (`entry.id`).
433
-
434
- ### events.ndjson
435
-
436
- The temporal journal records documented Pi `turn_*` and `message_*` hooks plus
437
- `tool_execution_*` hooks with schema `pi-workflows.session-event.v1`. Each
438
- record has a per-file `seq`, timestamp, `nodeId`, and `attemptId`. Optional
439
- turn, message, and tool call IDs link related records. A normalized `type` and
440
- `payload` carry the event data.
441
- The full contract and event catalog are in
442
- [session-event-journal.md](session-event-journal.md).
443
-
444
- Events preserve semantic deltas. Assistant `partial` snapshots are never
445
- stored, and neither are terminal `message` or `error` snapshots. Tool update records omit Pi's
446
- cumulative `partialResult`. Final `message_finished` records link to settled
447
- Pi entries with `entryId`; after that linkage, `entries.ndjson` is the
448
- verbatim content authority.
449
-
450
- Readers process events by `seq`. Timestamps schedule playback but never reorder
451
- records. A torn final line is buffered while capture is `recording`; malformed
452
- complete lines, sequence gaps, and terminal torn tails are integrity failures.
453
-
454
- ### capture.json
455
-
456
- `capture.json` is an atomically replaced integrity projection:
457
-
458
- ```json
459
- {
460
- "schema": "pi-workflows.session-capture.v1",
461
- "eventSchema": "pi-workflows.session-event.v1",
462
- "status": "complete",
463
- "eventCount": 241,
464
- "entryCount": 7,
465
- "lastEventSeq": 241
466
- }
467
- ```
468
-
469
- `status` starts as `recording` and ends as `complete` or `failed`. Failed
470
- capture adds `failure` with `failedAt` plus a code and message. Capture failure
471
- is visible to readers but does not fail the workflow. Terminal readers verify
472
- the counts and last sequence, then check schemas and contiguous event order. Missing temporal files
473
- in a session-bound bundle are invalid, not an older supported layout.
474
-
475
- ## Versioning
476
-
477
- Each file carries a versioned schema identifier, and the identifier changes
478
- only on breaking shape changes. Readers check `manifest.json`'s `schema` field
479
- and skip bundles they do not understand. Within a version, additions of new
480
- fields and new trace event types are allowed; readers must ignore what they do
481
- not know.
@@ -1,188 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import type { ArtifactRef, ArtifactValue } from "./types.js";
5
-
6
- /**
7
- * Value externalization for run bundles (see docs/run-bundles.md): string
8
- * leaves larger than the threshold are written once, content-addressed, under
9
- * `artifacts/` and replaced by `{ "$artifact": ref }`. Because artifacts are
10
- * immutable and deduplicated by hash, the same output appearing in `outputs`,
11
- * `results`, `steps`, and the trace costs one file plus small references.
12
- */
13
-
14
- export const ARTIFACT_THRESHOLD_BYTES = 4096;
15
- export const ARTIFACTS_DIR = "artifacts";
16
-
17
- const ARTIFACT_KEY = "$artifact";
18
- const ESCAPED_KEY = "$escaped";
19
-
20
- export function isArtifactValue(value: unknown): value is ArtifactValue {
21
- return (
22
- typeof value === "object" &&
23
- value !== null &&
24
- !Array.isArray(value) &&
25
- Object.keys(value).length === 1 &&
26
- ARTIFACT_KEY in value
27
- );
28
- }
29
-
30
- function isEscapedValue(value: unknown): value is { $escaped: Record<string, unknown> } {
31
- return (
32
- typeof value === "object" &&
33
- value !== null &&
34
- !Array.isArray(value) &&
35
- Object.keys(value).length === 1 &&
36
- ESCAPED_KEY in value
37
- );
38
- }
39
-
40
- /** True for a single-key object that would be misread as a sentinel. */
41
- function needsEscape(value: Record<string, unknown>): boolean {
42
- const keys = Object.keys(value);
43
- return keys.length === 1 && (keys[0] === ARTIFACT_KEY || keys[0] === ESCAPED_KEY);
44
- }
45
-
46
- /**
47
- * Writes externalized string leaves for one run bundle. Instances serialize
48
- * their writes and deduplicate by content hash, so encoding the same large
49
- * string in several value positions touches the filesystem once.
50
- */
51
- export class ArtifactWriter {
52
- private readonly runDir: string;
53
- private readonly written = new Set<string>();
54
- private chain: Promise<unknown> = Promise.resolve();
55
- private dirCreated = false;
56
-
57
- constructor(runDir: string) {
58
- this.runDir = runDir;
59
- }
60
-
61
- /** True once any artifact exists for this run. */
62
- get hasArtifacts(): boolean {
63
- return this.written.size > 0;
64
- }
65
-
66
- async externalize(text: string): Promise<ArtifactValue> {
67
- const bytes = Buffer.from(text, "utf8");
68
- const sha256 = createHash("sha256").update(bytes).digest("hex");
69
- const relativePath = `${ARTIFACTS_DIR}/sha256-${sha256}.txt`;
70
- const task = this.chain.then(async () => {
71
- if (this.written.has(sha256)) {
72
- return;
73
- }
74
- if (!this.dirCreated) {
75
- await fs.mkdir(path.join(this.runDir, ARTIFACTS_DIR), { recursive: true, mode: 0o700 });
76
- this.dirCreated = true;
77
- }
78
- const filePath = path.join(this.runDir, relativePath);
79
- // Content-addressed files are immutable; an existing file is complete
80
- // unless a previous crash left a partial write, which the temp-rename
81
- // pattern prevents.
82
- const tempPath = `${filePath}.${process.pid}.tmp`;
83
- await fs.writeFile(tempPath, bytes, { mode: 0o600 });
84
- await fs.rename(tempPath, filePath);
85
- this.written.add(sha256);
86
- });
87
- this.chain = task.catch(() => undefined);
88
- await task;
89
- return {
90
- $artifact: {
91
- path: relativePath,
92
- mediaType: "text/plain",
93
- bytes: bytes.byteLength,
94
- sha256,
95
- },
96
- };
97
- }
98
- }
99
-
100
- /**
101
- * Encode a persisted value: externalize large string leaves and escape
102
- * single-key `$artifact`/`$escaped` objects so the sentinel stays
103
- * unambiguous. Returns the original value when nothing changed.
104
- */
105
- export async function encodeValue(
106
- value: unknown,
107
- writer: ArtifactWriter,
108
- thresholdBytes: number = ARTIFACT_THRESHOLD_BYTES,
109
- ): Promise<unknown> {
110
- if (typeof value === "string") {
111
- if (Buffer.byteLength(value, "utf8") <= thresholdBytes) {
112
- return value;
113
- }
114
- return await writer.externalize(value);
115
- }
116
- if (Array.isArray(value)) {
117
- const encoded = await Promise.all(
118
- value.map((item) => encodeValue(item, writer, thresholdBytes)),
119
- );
120
- return encoded.some((item, index) => item !== value[index]) ? encoded : value;
121
- }
122
- if (typeof value === "object" && value !== null) {
123
- const record = value as Record<string, unknown>;
124
- const entries = await Promise.all(
125
- Object.entries(record).map(
126
- async ([key, item]) => [key, await encodeValue(item, writer, thresholdBytes)] as const,
127
- ),
128
- );
129
- const changed = entries.some(([key, item]) => item !== record[key]);
130
- const encoded = changed ? Object.fromEntries(entries) : record;
131
- return needsEscape(record) ? { [ESCAPED_KEY]: encoded } : encoded;
132
- }
133
- return value;
134
- }
135
-
136
- /**
137
- * Walk a persisted value, replacing every `$artifact` sentinel via `resolve`
138
- * and unwrapping `$escaped` objects. `resolve` may return the artifact
139
- * contents or a placeholder; it receives the reference untouched.
140
- */
141
- export function decodeValueWith(value: unknown, resolve: (ref: ArtifactRef) => unknown): unknown {
142
- if (isArtifactValue(value)) {
143
- return resolve(value.$artifact);
144
- }
145
- if (isEscapedValue(value)) {
146
- // The unwrapped object is user data: process its children but do not
147
- // re-test the object itself as a sentinel.
148
- return decodeChildren(value.$escaped, resolve);
149
- }
150
- if (Array.isArray(value)) {
151
- return value.map((item) => decodeValueWith(item, resolve));
152
- }
153
- if (typeof value === "object" && value !== null) {
154
- return decodeChildren(value as Record<string, unknown>, resolve);
155
- }
156
- return value;
157
- }
158
-
159
- function decodeChildren(
160
- record: Record<string, unknown>,
161
- resolve: (ref: ArtifactRef) => unknown,
162
- ): Record<string, unknown> {
163
- return Object.fromEntries(
164
- Object.entries(record).map(([key, item]) => [key, decodeValueWith(item, resolve)]),
165
- );
166
- }
167
-
168
- /** Resolve every artifact reference in `value` by reading the bundle files. */
169
- export async function resolveArtifacts(value: unknown, runDir: string): Promise<unknown> {
170
- const refs: ArtifactRef[] = [];
171
- decodeValueWith(value, (ref) => {
172
- refs.push(ref);
173
- return null;
174
- });
175
- const contents = new Map<string, string>();
176
- for (const ref of refs) {
177
- if (contents.has(ref.path)) {
178
- continue;
179
- }
180
- const resolved = path.resolve(runDir, ref.path);
181
- // A reference must never escape the bundle directory.
182
- if (!resolved.startsWith(path.resolve(runDir) + path.sep)) {
183
- throw new Error(`Artifact path escapes the bundle: ${ref.path}`);
184
- }
185
- contents.set(ref.path, await fs.readFile(resolved, "utf8"));
186
- }
187
- return decodeValueWith(value, (ref) => contents.get(ref.path) ?? null);
188
- }