@osolmaz/pi-workflows 0.1.0 → 0.2.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 (62) hide show
  1. package/README.md +36 -21
  2. package/dist/extension/executor.d.ts +14 -1
  3. package/dist/extension/executor.js +11 -1
  4. package/dist/extension/executor.js.map +1 -1
  5. package/dist/extension/index.js +79 -7
  6. package/dist/extension/index.js.map +1 -1
  7. package/dist/extension/recorder.d.ts +85 -0
  8. package/dist/extension/recorder.js +525 -0
  9. package/dist/extension/recorder.js.map +1 -0
  10. package/dist/extension/session-events.d.ts +134 -0
  11. package/dist/extension/session-events.js +60 -0
  12. package/dist/extension/session-events.js.map +1 -0
  13. package/dist/extension/widget.js +25 -24
  14. package/dist/extension/widget.js.map +1 -1
  15. package/dist/render/canvas.d.ts +1 -1
  16. package/dist/render/canvas.js +5 -0
  17. package/dist/render/canvas.js.map +1 -1
  18. package/dist/render/graph-render.d.ts +5 -0
  19. package/dist/render/graph-render.js +211 -48
  20. package/dist/render/graph-render.js.map +1 -1
  21. package/dist/viewer/render.js +19 -3
  22. package/dist/viewer/render.js.map +1 -1
  23. package/dist/viewer/session-reducer.d.ts +45 -0
  24. package/dist/viewer/session-reducer.js +266 -0
  25. package/dist/viewer/session-reducer.js.map +1 -0
  26. package/dist/workflows/artifacts.d.ts +40 -0
  27. package/dist/workflows/artifacts.js +155 -0
  28. package/dist/workflows/artifacts.js.map +1 -0
  29. package/dist/workflows/engine.d.ts +2 -0
  30. package/dist/workflows/engine.js +38 -7
  31. package/dist/workflows/engine.js.map +1 -1
  32. package/dist/workflows/index.d.ts +3 -2
  33. package/dist/workflows/index.js +2 -1
  34. package/dist/workflows/index.js.map +1 -1
  35. package/dist/workflows/store.d.ts +53 -9
  36. package/dist/workflows/store.js +523 -43
  37. package/dist/workflows/store.js.map +1 -1
  38. package/dist/workflows/types.d.ts +126 -3
  39. package/docs/development.md +43 -19
  40. package/docs/live-replay-protocol.md +155 -0
  41. package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
  42. package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
  43. package/docs/plans/session-event-replay-implementation-plan.md +494 -0
  44. package/docs/plans/tui-viewer-implementation-plan.md +64 -0
  45. package/docs/run-bundles.md +320 -55
  46. package/docs/session-event-journal.md +470 -0
  47. package/docs/tui-viewer.md +218 -0
  48. package/package.json +2 -1
  49. package/src/extension/executor.ts +28 -1
  50. package/src/extension/index.ts +87 -7
  51. package/src/extension/recorder.ts +633 -0
  52. package/src/extension/session-events.ts +119 -0
  53. package/src/extension/widget.ts +26 -24
  54. package/src/render/canvas.ts +19 -1
  55. package/src/render/graph-render.ts +277 -44
  56. package/src/viewer/render.ts +21 -3
  57. package/src/viewer/session-reducer.ts +347 -0
  58. package/src/workflows/artifacts.ts +188 -0
  59. package/src/workflows/engine.ts +39 -7
  60. package/src/workflows/index.ts +15 -0
  61. package/src/workflows/store.ts +649 -49
  62. package/src/workflows/types.ts +141 -3
@@ -1,40 +1,117 @@
1
1
  # Run bundle format
2
2
 
3
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,
5
- including the bundled terminal viewer. This document specifies the format so
6
- other tools can consume it.
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.
7
11
 
8
12
  ## Location and layout
9
13
 
10
14
  Bundles live under `~/.pi/agent/workflows/runs/` by default. The
11
15
  `PI_WORKFLOWS_RUNS_DIR` environment variable overrides the location for both
12
- the engine and the viewer, which is how the test suite keeps runs inside
16
+ the engine and all viewers, which is how the test suite keeps runs inside
13
17
  temporary directories.
14
18
 
15
19
  ```
16
20
  ~/.pi/agent/workflows/runs/
17
- 20260719T023912Z-autoimplement-3f2a9c1b/
21
+ 20260729T023912Z-autoimplement-3f2a9c1b/
18
22
  manifest.json # pi-workflows.run-bundle.v1
19
23
  workflow.json # pi-workflows.definition-snapshot.v1
20
- state.json # full run projection
21
- trace.ndjson # pi-workflows.trace-event.v1, append-only
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
22
33
  ```
23
34
 
24
35
  Run ids are `<UTC timestamp>-<workflow slug>-<8 hex chars>`, so lexical order
25
36
  is chronological order.
26
37
 
27
- ## Write discipline
38
+ Bundle directories are created with mode `0700` and files with mode `0600`.
39
+ Bundles can contain prompts, model output, shell commands, environment
40
+ details, and absolute paths; treat them as private data and review before
41
+ exporting.
42
+
43
+ ## Source of truth and write discipline
44
+
45
+ `trace.ndjson` is the source of truth for workflow execution. Final Pi
46
+ conversation entries and temporal session history have separate authority in
47
+ `session/entries.ndjson` and `session/events.ndjson`. `session/capture.json`
48
+ reports whether temporal capture is complete. These sequence spaces are
49
+ independent and must not be compared.
50
+
51
+ Write order for every transition:
52
+
53
+ 1. Append the trace event (one JSON object per line, appends serialized per
54
+ file, `seq` starting at 1 and increasing by exactly 1).
55
+ 2. Atomically replace `state.json`, carrying `traceSeq` = the `seq` of the
56
+ trace event it reflects (write to a temp file in the same directory, then
57
+ rename).
58
+ 3. Atomically replace `manifest.json`.
59
+
60
+ Consequences for readers:
61
+
62
+ - A reader never sees a partial JSON document; a torn final trace line must be
63
+ ignored.
64
+ - `state.json` with `traceSeq` older than the last trace line is a stale
65
+ projection: either re-read after the writer catches up or fold the trace
66
+ tail on top of it.
67
+ - Before the engine writes a terminal workflow event, session recording stops,
68
+ drains accepted entries and events, and atomically writes `capture.json`.
69
+ - After a run reaches a terminal status (`completed`, `failed`, `timed_out`,
70
+ `cancelled`, or `waiting`), the bundle no longer changes, and
71
+ `state.traceSeq` equals the final trace `seq`.
72
+ - A bundle whose state is `running` but whose files have stopped growing may
73
+ be an interrupted run (crash, reboot); viewers should label it as possibly
74
+ interrupted rather than live.
75
+
76
+ ## Externalized values and artifacts
77
+
78
+ Large payloads are stored once, content-addressed, under `artifacts/` and
79
+ referenced from the documents that use them. This applies uniformly to every
80
+ **persisted value position**: `input`, `outputs.*`, `results.*.output`,
81
+ `steps[*].prompt`, `steps[*].output`, `finalOutput`, and trace event payload
82
+ values.
83
+
84
+ Encoding rule, applied recursively to a persisted value:
85
+
86
+ - A string leaf whose UTF-8 encoding is larger than 4096 bytes is written to
87
+ `artifacts/sha256-<digest>.txt` (UTF-8, digest over the exact bytes) and
88
+ replaced by an artifact reference:
89
+
90
+ ```json
91
+ {
92
+ "$artifact": {
93
+ "path": "artifacts/sha256-2b1f….txt",
94
+ "mediaType": "text/plain",
95
+ "bytes": 18342,
96
+ "sha256": "2b1f…"
97
+ }
98
+ }
99
+ ```
100
+
101
+ - Any user object that has an own key `$artifact` or `$escaped` is wrapped as
102
+ `{ "$escaped": <object> }` so the sentinel stays unambiguous. Decoders
103
+ unwrap `$escaped` one level and resolve `$artifact` refs.
104
+ - Everything else is stored inline. Small values are never externalized.
28
105
 
29
- Every JSON file in the bundle is written atomically (write to a temp file in
30
- the same directory, then rename), so a reader never sees a partial document. `trace.ndjson` is append-only, one JSON object
31
- per line, with writes serialized per file. After a run reaches a terminal
32
- status (`completed`, `failed`, `timed_out`, `cancelled`, or `waiting`), the
33
- bundle no longer changes.
106
+ Artifact rules:
34
107
 
35
- A live viewer needs only two behaviors. Treat `state.json` as the current
36
- projection and re-read it on any file change, and treat `trace.ndjson` as the
37
- event timeline when history matters.
108
+ - `path` is bundle-relative; a reference never points outside the bundle.
109
+ - Artifacts are immutable once written and deduplicate by content hash.
110
+ - Readers must tolerate unknown `mediaType` values.
111
+
112
+ Because the same output can legitimately appear in `outputs`, `results`,
113
+ `steps`, and the trace, externalization makes that duplication cheap: each
114
+ copy is the same small reference.
38
115
 
39
116
  ## manifest.json
40
117
 
@@ -43,72 +120,260 @@ Identity and pointers, kept in sync with the state on every snapshot:
43
120
  ```json
44
121
  {
45
122
  "schema": "pi-workflows.run-bundle.v1",
46
- "runId": "20260719T023912Z-autoimplement-3f2a9c1b",
123
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
47
124
  "workflowName": "autoimplement",
48
125
  "runTitle": "autoimplement: fix the flaky test",
49
126
  "workflowPath": "/repo/.pi/workflows/autoimplement.workflow.ts",
50
- "startedAt": "2026-07-19T02:39:12.412Z",
51
- "finishedAt": "2026-07-19T02:41:03.977Z",
127
+ "startedAt": "2026-07-29T02:39:12.412Z",
128
+ "finishedAt": "2026-07-29T02:41:03.977Z",
52
129
  "status": "completed",
53
130
  "traceSchema": "pi-workflows.trace-event.v1",
54
- "paths": { "workflow": "workflow.json", "state": "state.json", "trace": "trace.ndjson" }
131
+ "paths": {
132
+ "workflow": "workflow.json",
133
+ "state": "state.json",
134
+ "trace": "trace.ndjson",
135
+ "session": "session",
136
+ "artifacts": "artifacts"
137
+ }
55
138
  }
56
139
  ```
57
140
 
141
+ `paths.artifacts` is declared from bundle creation so a live session-event
142
+ patch can safely reference a newly written artifact before the next workflow
143
+ state projection. The directory itself is created only when needed.
144
+ `paths.session` appears when the run binds to Pi. Readers must start from
145
+ `manifest.json`, check `schema`, skip bundles they do not understand, resolve
146
+ files through `paths`, and reject any path that escapes the bundle directory.
147
+
58
148
  ## workflow.json
59
149
 
60
- A serializable snapshot of the graph taken at run start. Functions such as
61
- prompts and validators are not serialized. Each node keeps only its metadata
62
- (`nodeType`, `timeoutMs`, `statusDetail`, `expectedOutput`, `summary`,
63
- `actionExecution`), and edges are copied verbatim. The snapshot is what lets
64
- the viewer draw all nodes, including ones that have not run yet.
150
+ A serializable snapshot of the graph taken at run start
151
+ (`pi-workflows.definition-snapshot.v1`). Functions such as prompts and
152
+ validators are not serialized. Each node keeps only its metadata (`nodeType`,
153
+ `timeoutMs`, `statusDetail`, `expectedOutput`, `summary`, `actionExecution`),
154
+ and edges are copied verbatim. The snapshot is what lets viewers draw all
155
+ nodes, including ones that have not run yet. It is immutable after run start.
65
156
 
66
157
  ## state.json
67
158
 
68
159
  The full run projection (`WorkflowRunState` in
69
- [`src/workflows/types.ts`](../src/workflows/types.ts)). The `status` field is
70
- one of `running`, `waiting`, `completed`, `failed`, `timed_out`, or
71
- `cancelled`. While a node is executing, `currentNode`, `currentNodeType`,
72
- `currentNodeStartedAt`, and `statusDetail` describe it, and they disappear
73
- when the node finishes. While a pause request holds the run at a step
74
- boundary, `paused` is `true` (with matching `run_paused`/`run_resumed` trace
75
- events); it disappears when the run resumes or ends.
76
-
77
- Per-node data lives in `outputs` (the accepted output of each finished node,
78
- where the latest attempt wins on loops) and in `results` (the full result
79
- record including the outcome and timing). The ordered history is `steps`,
80
- with one record per node execution that includes the prompt text for agent
81
- steps and an action receipt with the command, exit code, and duration for
82
- action steps. When a run pauses at a checkpoint, `waitingOn` names the
83
- checkpoint node. Terminal runs carry `finalOutput` on success and `error` on
84
- failure.
160
+ [`src/workflows/types.ts`](../src/workflows/types.ts)), schema
161
+ `pi-workflows.run-state.v1`:
162
+
163
+ ```json
164
+ {
165
+ "schema": "pi-workflows.run-state.v1",
166
+ "traceSeq": 17,
167
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
168
+ "workflowName": "autoimplement",
169
+ "startedAt": "…",
170
+ "updatedAt": "…",
171
+ "status": "running",
172
+ "input": { "task": "fix the flaky test" },
173
+ "outputs": {},
174
+ "results": {},
175
+ "steps": []
176
+ }
177
+ ```
178
+
179
+ - `status` is one of `running`, `waiting`, `completed`, `failed`, `timed_out`,
180
+ or `cancelled`.
181
+ - While a node is executing, `currentNode`, `currentAttemptId`,
182
+ `currentNodeStartedAt`, and `statusDetail` describe it; they disappear when
183
+ the node finishes. The executing node's type comes from the definition
184
+ snapshot, not from the state.
185
+ - While a pause request holds the run at a step boundary, `paused` is `true`
186
+ (with matching `run_paused`/`run_resumed` trace events); it disappears when
187
+ the run resumes or ends.
188
+ - Per-node data lives in `outputs` (the accepted output of each finished node,
189
+ latest attempt wins on loops) and `results` (the full result record of the
190
+ latest attempt, including outcome and timing).
191
+ - `steps` is the ordered history, one record per node attempt:
192
+
193
+ ```json
194
+ {
195
+ "attemptId": "d81f…",
196
+ "nodeId": "implement",
197
+ "nodeType": "agent",
198
+ "outcome": "ok",
199
+ "startedAt": "…",
200
+ "finishedAt": "…",
201
+ "prompt": {
202
+ "$artifact": {
203
+ "path": "artifacts/sha256-….txt",
204
+ "mediaType": "text/plain",
205
+ "bytes": 9120,
206
+ "sha256": "…"
207
+ }
208
+ },
209
+ "output": { "summary": "…" },
210
+ "conversation": { "firstEntryId": "a1b2c3d4", "lastEntryId": "c3d4e5f6" }
211
+ }
212
+ ```
213
+
214
+ - `prompt` is the full prompt text for agent steps (`null` for other node
215
+ types), subject to value externalization.
216
+ - `conversation` is present on agent steps recorded inside a Pi conversation:
217
+ the inclusive range of Pi session entry ids in `session/entries.ndjson`
218
+ produced by this attempt, from prompt delivery through accepted submission.
219
+ Viewers must use this explicit linkage and never infer it heuristically.
220
+ - Action steps carry an `action` receipt with `actionType`
221
+ (`shell`/`function`) and, for shell actions, `command`, `args`, `cwd`,
222
+ `exitCode`, `signal`, and `durationMs`. Shell stdout/stderr live in the
223
+ step output (the parsed or raw shell result) and are externalized when
224
+ large.
225
+ - When a run pauses at a checkpoint, `waitingOn` names the checkpoint node.
226
+ Terminal runs carry `finalOutput` on success and `error` on failure.
85
227
 
86
228
  ## trace.ndjson
87
229
 
88
- One event per line, monotonically sequenced per run:
230
+ One event per line, monotonically sequenced per run, schema
231
+ `pi-workflows.trace-event.v1`:
89
232
 
90
233
  ```json
91
234
  {
92
235
  "seq": 3,
93
- "at": "2026-07-19T02:39:14.101Z",
236
+ "at": "2026-07-29T02:39:14.101Z",
94
237
  "scope": "agent",
95
238
  "type": "agent_prompt_sent",
96
- "runId": "...",
239
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
97
240
  "nodeId": "implement",
98
- "attemptId": "...",
99
- "payload": { "prompt": "..." }
241
+ "attemptId": "d81f…",
242
+ "payload": { "prompt": "" }
243
+ }
244
+ ```
245
+
246
+ `scope` is one of `run`, `node`, `agent`, `action`, or `session`. `nodeId` and
247
+ `attemptId` are present on node-scoped and agent-scoped events. Consumers must
248
+ ignore unknown event types and unknown payload fields so new ones can be added
249
+ within the same schema version.
250
+
251
+ The trace alone is sufficient to reconstruct the run: outputs and receipts are
252
+ part of the terminal node events, not only of `state.json`.
253
+
254
+ Event catalog and payload contracts:
255
+
256
+ | type | scope | payload |
257
+ | ------------------- | ------- | ------------------------------------------------------------------- |
258
+ | `run_started` | run | `workflowName`, `runTitle?`, `input` |
259
+ | `session_bound` | session | `piSessionId` |
260
+ | `node_started` | node | `nodeType`, `statusDetail?` |
261
+ | `agent_prompt_sent` | agent | `prompt` |
262
+ | `node_finished` | node | `outcome: "ok"`, `durationMs`, `output`, `conversation?`, `action?` |
263
+ | `node_failed` | node | `outcome`, `durationMs`, `error`, `conversation?`, `action?` |
264
+ | `run_paused` | run | _(empty)_ |
265
+ | `run_resumed` | run | _(empty)_ |
266
+ | `run_completed` | run | `status`, `finalOutput` |
267
+ | `run_waiting` | run | `status`, `waitingOn`, `finalOutput` |
268
+ | `run_failed` | run | `status`, `error` |
269
+ | `run_timed_out` | run | `status`, `error` |
270
+ | `run_cancelled` | run | `status`, `error?` |
271
+
272
+ Invariants:
273
+
274
+ - every node attempt has exactly one `node_started` and exactly one terminal
275
+ `node_finished`/`node_failed` with the same `attemptId`;
276
+ - `attemptId` values are unique within a run;
277
+ - a terminal `run_*` event is the last event of the run;
278
+ - events are never rewritten or deleted.
279
+
280
+ ## session/
281
+
282
+ Present when the run executed inside a Pi conversation. The extension records
283
+ the conversation into the bundle so replay never depends on Pi's global
284
+ session store.
285
+
286
+ ### binding.json
287
+
288
+ Written once when the run binds to the conversation
289
+ (`pi-workflows.session-binding.v1`):
290
+
291
+ ```json
292
+ {
293
+ "schema": "pi-workflows.session-binding.v1",
294
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
295
+ "piSessionId": "019fad89-…",
296
+ "piSessionFile": "/home/user/.pi/agent/sessions/--repo--/2026-07-29….jsonl",
297
+ "cwd": "/repo",
298
+ "boundAt": "2026-07-29T02:39:12.412Z"
299
+ }
300
+ ```
301
+
302
+ `piSessionFile` is provenance only and absent for in-memory sessions; replay
303
+ readers must not read it.
304
+
305
+ ### entries.ndjson
306
+
307
+ Append-only copies of the Pi session entries produced on the current branch
308
+ while the run was active, schema `pi-workflows.session-entry.v1`:
309
+
310
+ ```json
311
+ {
312
+ "seq": 1,
313
+ "at": "2026-07-29T02:39:12.902Z",
314
+ "entry": {
315
+ "type": "message",
316
+ "id": "a1b2c3d4",
317
+ "parentId": "9f8e7d6c",
318
+ "timestamp": "…",
319
+ "message": { "role": "user", "content": "…" }
320
+ }
321
+ }
322
+ ```
323
+
324
+ - `seq` is strictly increasing within the file, starting at 1.
325
+ - `entry` is the verbatim Pi session entry (Pi's own versioned format),
326
+ including user messages, assistant messages, tool results, model changes,
327
+ and compaction entries. Nothing is normalized or rewritten.
328
+ - Entries include everything that happened in the conversation during the run:
329
+ workflow prompts, nudges, and user interruptions are all part of the record.
330
+ - `conversation` ranges in step records and `node_finished` events address
331
+ entries by Pi entry id (`entry.id`).
332
+
333
+ ### events.ndjson
334
+
335
+ The temporal journal records documented Pi `turn_*`, `message_*`, and
336
+ `tool_execution_*` hooks with schema `pi-workflows.session-event.v1`. Each
337
+ record has a per-file `seq`, timestamp, `nodeId`, `attemptId`, optional
338
+ `turnId`, `messageId`, and `toolCallId`, a normalized `type`, and `payload`.
339
+ The full contract and event catalog are in
340
+ [session-event-journal.md](session-event-journal.md).
341
+
342
+ Events preserve semantic deltas. Assistant `partial`, terminal `message`, and
343
+ terminal `error` snapshots are never stored. Tool update records omit Pi's
344
+ cumulative `partialResult`. Final `message_finished` records link to settled
345
+ Pi entries with `entryId`; after that linkage, `entries.ndjson` is the
346
+ verbatim content authority.
347
+
348
+ Readers process events by `seq`. Timestamps schedule playback but never reorder
349
+ records. A torn final line is buffered while capture is `recording`; malformed
350
+ complete lines, sequence gaps, and terminal torn tails are integrity failures.
351
+
352
+ ### capture.json
353
+
354
+ `capture.json` is an atomically replaced integrity projection:
355
+
356
+ ```json
357
+ {
358
+ "schema": "pi-workflows.session-capture.v1",
359
+ "eventSchema": "pi-workflows.session-event.v1",
360
+ "status": "complete",
361
+ "eventCount": 241,
362
+ "entryCount": 7,
363
+ "lastEventSeq": 241
100
364
  }
101
365
  ```
102
366
 
103
- Event types: `run_started`, `node_started`, `agent_prompt_sent`,
104
- `node_finished`, `node_failed`, and a terminal `run_<status>`. The `scope`
105
- field (`run`, `node`, `agent`, `action`) groups them. Consumers should ignore
106
- unknown event types so new ones can be added within the same schema version.
367
+ `status` is `recording`, `complete`, or `failed`. Failed capture adds
368
+ `failure` with `failedAt`, `code`, and `message`. Capture failure is visible to
369
+ readers but does not fail the workflow. Terminal readers verify the counts,
370
+ last sequence, schemas, and contiguous event sequence. Missing temporal files
371
+ in a session-bound bundle are invalid, not an older supported layout.
107
372
 
108
373
  ## Versioning
109
374
 
110
- Each file carries a versioned schema identifier such as
111
- `pi-workflows.run-bundle.v1`, and the identifier changes only on breaking
112
- shape changes. Readers should check `manifest.json`'s `schema` field and skip
113
- bundles they do not understand, which is exactly what the bundled viewer does
114
- with unreadable directories.
375
+ Each file carries a versioned schema identifier, and the identifier changes
376
+ only on breaking shape changes. Readers check `manifest.json`'s `schema` field
377
+ and skip bundles they do not understand. Within a version, additions of new
378
+ fields and new trace event types are allowed; readers must ignore what they do
379
+ not know.