@osolmaz/pi-workflows 0.15.2 → 0.16.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.
- package/README.md +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +56 -3
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
|
@@ -1,175 +1,113 @@
|
|
|
1
|
-
# Live
|
|
1
|
+
# Live client protocol
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pi Workflows uses one live client protocol for the Pi extension, the TypeScript CLI, local `piw`, and remote `piw` through the loopback relay.
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The protocol ID is `pi-workflows.client.v1`. Its schema is [`protocol/client.v1.schema.json`](../protocol/client.v1.schema.json). TypeScript and Rust use the same valid and invalid fixture corpus.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The host is the only process that reads or writes the active SQLite database. A client protocol or package-version mismatch does not mean that SQLite state is incompatible. The client stops and asks for matching `pi-workflows` and `piw` packages.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Transports
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
The local transport is a user-only Unix socket on Unix systems:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
~/.pi/agent/workflows/host/host.sock
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
On Windows, the same client uses the package-derived `\\.\pipe\pi-workflows-<state-directory-hash>` named pipe. Local `piw`, `piw serve`, and the TypeScript client derive the same endpoint from the workflow state directory. A foreground TypeScript cold start keeps its retry wait referenced until the detached host becomes ready or the start deadline expires. Only background reconnect timers are unreferenced.
|
|
18
|
+
|
|
19
|
+
Each message is one canonical JSON object followed by a newline. TypeScript and Rust use the same ECMAScript number formatting and UTF-16 object-key order, including for arbitrary workflow JSON. A message can be at most 1 MiB. Unknown envelope fields, non-canonical JSON, and invalid framing close only the offending connection. If socket backpressure delays a client write, connection close, socket error, or request cancellation ends the wait instead of leaving the request pending.
|
|
20
|
+
|
|
21
|
+
`piw serve` provides the remote transport at `/ws`. It binds to loopback only. Each WebSocket connection has one matching host-socket connection, and their lifecycles are coupled. The relay forwards one canonical JSON object per text frame. It does not read SQLite, translate views, multiplex clients, or retain state.
|
|
22
|
+
|
|
23
|
+
Remote clients use an SSH tunnel to reach the loopback relay.
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
## Envelope
|
|
26
|
+
|
|
27
|
+
The protocol has four message types:
|
|
28
|
+
|
|
29
|
+
- `hello` identifies the protocol connection and package version.
|
|
30
|
+
- `request` carries one operation, request ID, client ID, idempotency key, optional run ID and revision, and payload.
|
|
31
|
+
- `response` settles one request with an outcome, optional revision, receipt, or safe error.
|
|
32
|
+
- `event` carries a revisioned run list, run snapshot, run patch, run page, session snapshot, or unavailable condition.
|
|
33
|
+
|
|
34
|
+
The host sends `hello` first:
|
|
18
35
|
|
|
19
36
|
```json
|
|
20
37
|
{
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"graphScene": {
|
|
26
|
-
"ranks": [ … ],
|
|
27
|
-
"edges": [ … ],
|
|
28
|
-
"segments": [ … ],
|
|
29
|
-
"rankOfNode": { … }
|
|
30
|
-
},
|
|
31
|
-
"graphSteps": [ … ],
|
|
32
|
-
"takenTransitions": [ "prepare->run" ],
|
|
33
|
-
"stepStart": 768,
|
|
34
|
-
"stepTotal": 1000,
|
|
35
|
-
"state": {
|
|
36
|
-
"steps": [ … ]
|
|
37
|
-
},
|
|
38
|
-
"tracePage": {
|
|
39
|
-
"presentationRevision": 42,
|
|
40
|
-
"start": 768,
|
|
41
|
-
"total": 1000,
|
|
42
|
-
"items": [ … ]
|
|
43
|
-
},
|
|
44
|
-
"session": {
|
|
45
|
-
"presentationRevision": 42,
|
|
46
|
-
"binding": { … },
|
|
47
|
-
"entryPage": {
|
|
48
|
-
"presentationRevision": 42,
|
|
49
|
-
"start": 768,
|
|
50
|
-
"total": 1000,
|
|
51
|
-
"items": [ … ]
|
|
52
|
-
},
|
|
53
|
-
"eventPage": {
|
|
54
|
-
"presentationRevision": 42,
|
|
55
|
-
"start": 768,
|
|
56
|
-
"total": 1000,
|
|
57
|
-
"items": [ … ]
|
|
58
|
-
},
|
|
59
|
-
"capture": { … }
|
|
60
|
-
},
|
|
61
|
-
"settingsScopes": [ … ],
|
|
62
|
-
"followUpQueue": { … },
|
|
63
|
-
"live": true,
|
|
64
|
-
"possiblyInterrupted": false
|
|
38
|
+
"connectionId": "connection-1",
|
|
39
|
+
"packageVersion": "0.15.3",
|
|
40
|
+
"schema": "pi-workflows.client.v1",
|
|
41
|
+
"type": "hello"
|
|
65
42
|
}
|
|
66
43
|
```
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
A snapshot does not contain complete trace or session history. A replay jump fetches the page that contains the requested zero-based cursor.
|
|
71
|
-
|
|
72
|
-
## Revisions and target patches
|
|
73
|
-
|
|
74
|
-
Each viewer-visible SQLite transaction advances the run presentation revision and commits ordered target patches with the same transaction. The server reads those patches. It does not build complete old and new run views to compare them.
|
|
45
|
+
A request uses a stable request ID and idempotency key:
|
|
75
46
|
|
|
76
47
|
```json
|
|
77
48
|
{
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"patch": [
|
|
86
|
-
{ "op": "replace", "path": "/presentationRevision", "value": 43 },
|
|
87
|
-
{ "op": "remove", "path": "/items/0" },
|
|
88
|
-
{ "op": "append", "path": "/items", "value": [ { "seq": 1001, … } ] },
|
|
89
|
-
{ "op": "replace", "path": "/start", "value": 745 },
|
|
90
|
-
{ "op": "replace", "path": "/total", "value": 1001 }
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
]
|
|
49
|
+
"clientId": "client-1",
|
|
50
|
+
"idempotencyKey": "status-1",
|
|
51
|
+
"operation": "host.status",
|
|
52
|
+
"payload": {},
|
|
53
|
+
"requestId": "request-1",
|
|
54
|
+
"schema": "pi-workflows.client.v1",
|
|
55
|
+
"type": "request"
|
|
94
56
|
}
|
|
95
57
|
```
|
|
96
58
|
|
|
97
|
-
The
|
|
59
|
+
The closed outcomes are `accepted`, `adopted`, `rejected`, `conflict`, `notFound`, `claimLost`, and `unavailable`.
|
|
98
60
|
|
|
99
|
-
|
|
61
|
+
## Run and session views
|
|
100
62
|
|
|
101
|
-
|
|
63
|
+
The host produces `pi-workflows.run-view.v1` from one consistent database read. The view contains the bounded workflow state, graph, trace, session projection, page cursors, presentation revision, and one `display` object. A terminal display includes the stored failure reason, not only its machine error code. A reason above the shared 16 KiB inline-content threshold uses a small `reason` notice and a digest-bound `reasonContent` reference. This keeps the run list below the 1 MiB frame limit and keeps the complete diagnostic available.
|
|
102
64
|
|
|
103
|
-
The
|
|
65
|
+
The closed display statuses are:
|
|
104
66
|
|
|
105
|
-
|
|
67
|
+
- `queued`
|
|
68
|
+
- `running`
|
|
69
|
+
- `waiting`
|
|
70
|
+
- `paused`
|
|
71
|
+
- `completed`
|
|
72
|
+
- `failed`
|
|
73
|
+
- `timed_out`
|
|
74
|
+
- `cancelled`
|
|
75
|
+
- `ambiguous`
|
|
106
76
|
|
|
107
|
-
A
|
|
77
|
+
Only the host computes this status. A parked queue is not a pause. `paused` requires the durable pause flag. An exact live worker or origin-session turn is `running`. An effect being applied by that live worker is still `running`; only a durable `ambiguous` effect that needs operator action is `ambiguous`. `unavailable` is a client connection condition, not a run status.
|
|
108
78
|
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
"type": "fetch_page",
|
|
112
|
-
"runId": "run-1",
|
|
113
|
-
"kind": "session_events",
|
|
114
|
-
"cursor": 20000
|
|
115
|
-
}
|
|
116
|
-
```
|
|
79
|
+
The run list uses the same display object. The host sends it as revision-bound `pi-workflows.run-list-page.v1` pages. Each page reads only lightweight run status and source facts. It does not load input, launch options, steps, trace, or session history. TypeScript and Rust clients collect all pages for one revision before they replace the visible list. If the revision changes, they discard the partial list and start from the next subscription event.
|
|
117
80
|
|
|
118
|
-
|
|
81
|
+
An origin-session subscription returns `pi-workflows.session-view.v1`, which contains the current active run view, an ordered byte-bounded window of pending interaction records, their complete count, and read-only notification and turn availability in one read. A session with no active reservation returns no run, even when that session has older terminal runs. The Pi extension assembles the active run's complete step history and hydrates its definition and referenced values before it updates the widget or delivery coordinator. It sends a durable claim command only when the matching availability fact is true. After a turn claim, it reads the claimed run by its exact run ID. It does not use the latest run in the session. An idle session does not create empty claim or status commands.
|
|
119
82
|
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
"type": "run_page",
|
|
123
|
-
"runId": "run-1",
|
|
124
|
-
"revision": 43,
|
|
125
|
-
"kind": "session_events",
|
|
126
|
-
"cursor": 20000,
|
|
127
|
-
"start": 19872,
|
|
128
|
-
"total": 48620,
|
|
129
|
-
"items": [ … ]
|
|
130
|
-
}
|
|
131
|
-
```
|
|
83
|
+
A snapshot page contains at most 256 items and also has a byte budget. `view.page` returns another byte-bounded window that contains the requested cursor. Page responses use `pi-workflows.run-page.v1` and echo the requested cursor and run-view revision. A client applies a page only when both still match its current request and snapshot. A step-centered trace request selects the exact stored attempt first and uses its node only when that attempt has no trace event. Large workflow topology has bounded node, edge, graph-step, and transition projections plus durable content references for the complete original definition and complete graph history. TypeScript clients assemble every run-history page for that revision and hydrate the complete definition and all referenced content before they emit a complete non-interactive view or update the Pi widget. TypeScript and Rust use the same verified content loader for complete graph steps and transitions. Rust also requests, verifies, and decodes the complete referenced definition before it builds the graph layout. A session-event page also carries the replay checkpoint for the exact sequence before its first item, so reducing the page does not lose earlier active messages or tool calls. The checkpoint can itself be a durable content reference. TypeScript hydrates it with the run view, and Rust requests and resolves it before replay.
|
|
132
84
|
|
|
133
|
-
|
|
85
|
+
The host counts histories first and reads only the selected SQLite ranges. An unchanged subscription uses a lightweight revision check and reuses its prior view. It does not rebuild complete histories every 250 milliseconds.
|
|
134
86
|
|
|
135
|
-
|
|
87
|
+
Large prompt, output, event, settings, follow-up, and update values use a content reference instead of making a protocol frame exceed 1 MiB. `view.content` returns the referenced UTF-8 content in verified chunks. The reference includes its media type, byte count, SHA-256 digest, and an opaque marker for host-created references. The host saves generated view content directly under its exact run ID, content digest, and media type before it advertises the reference. It does not share media metadata with general state blobs. A request for another run or media representation is unavailable. Memory-cache eviction therefore cannot make an advertised aggregate unavailable. Run pruning removes the durable content. Clients reassemble all chunks and verify the bytes against both the content response and the advertised reference before display. Opaque content is restored as user data without interpreting nested objects as host references. Other cursors and content references keep the complete logical history and result available.
|
|
136
88
|
|
|
137
|
-
|
|
89
|
+
## Subscriptions and reconnection
|
|
138
90
|
|
|
139
|
-
|
|
140
|
-
| ---------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
141
|
-
| `watch_runs` | none | Subscribe to run-list rows. |
|
|
142
|
-
| `watch_run` | `runId`, optional `revision`, `stepCursor`, `traceCursor`, `sessionEntryCursor`, `sessionEventCursor` | Subscribe or resume one run. |
|
|
143
|
-
| `unwatch_run` | `runId` | End one run subscription. |
|
|
144
|
-
| `fetch_page` | `runId`, `kind`, `cursor` | Read one bounded page. |
|
|
145
|
-
| `fetch_artifact` | `runId`, `path` | Unsupported for SQLite state; returns `error`. |
|
|
91
|
+
A client keeps one persistent connection and records its desired run-list, run, and origin-session subscriptions. A request to watch a run that does not exist returns `notFound` and does not install a subscription. TypeScript and Rust clients show that response instead of waiting for a snapshot. Explicit `piw <runId>` mode keeps that run selected even when the run list contains a newer run. After reconnection, the client sends accepted subscriptions again with its run revision. The host sends a bounded snapshot when the client needs one. The protocol also supports retained revision patches. Unsubscribing sends the subscription ID to the host for every subscription kind, so no unused snapshot work remains on a live connection.
|
|
146
92
|
|
|
147
|
-
|
|
93
|
+
A slow or disconnected client cannot stop the host, another client, claim renewal, or workflow execution. The host waits for socket drain before it publishes another snapshot to that connection. Polling coalesces while the connection is blocked, so the socket buffer cannot grow by one snapshot on every poll. When a connection closes, the host removes its subscriptions and exact origin-session activity immediately.
|
|
148
94
|
|
|
149
|
-
|
|
150
|
-
| -------------- | ------------------------------------------------------ | ---------------------------------------- |
|
|
151
|
-
| `hello` | `protocol` | Identify the protocol. |
|
|
152
|
-
| `runs` | `runs` | Send all lightweight run-list rows. |
|
|
153
|
-
| `run_snapshot` | `runId`, `revision`, `view` | Send one bounded run view. |
|
|
154
|
-
| `run_patch` | `runId`, `revision`, `targets` | Apply direct bounded target patches. |
|
|
155
|
-
| `run_page` | `runId`, `revision`, `kind`, `start`, `total`, `items` | Return one bounded page. |
|
|
156
|
-
| `artifact` | `runId`, `path`, `content` | Reserved and not sent by SQLite servers. |
|
|
157
|
-
| `error` | `message`, optional `runId` | Report a sanitized request failure. |
|
|
95
|
+
## Origin-session activity
|
|
158
96
|
|
|
159
|
-
The
|
|
97
|
+
The Pi extension reports `started`, `refresh`, and `settled` activity for the exact session, run, interaction request, delivery, and Pi session entry. Reports use a monotonic sequence. The first report on each protocol connection is `started`; only later reports on that same connection use `refresh`. Refresh happens before the connection-scoped lease expires.
|
|
160
98
|
|
|
161
|
-
|
|
99
|
+
The host accepts activity only when its session, run, request, deterministic `interaction:<request-id>` delivery ID, and presented Pi session entry match the durable presented interaction. Activity entries are keyed by connection and request, so another caller-supplied delivery label cannot create a duplicate overlay. Activity changes display only. It does not grant authority, renew a workflow claim, settle a step, or change durable pause state. A disconnect or expired activity lease removes the overlay.
|
|
162
100
|
|
|
163
|
-
|
|
101
|
+
## Commands and uncertain results
|
|
164
102
|
|
|
165
|
-
|
|
103
|
+
Durable commands use stable idempotency keys. The Pi extension routes each state-changing command through the durable client request path. If the connection closes after the host commits but before the response arrives, the client reconnects with a new request ID and adopts the stored result. A retry with the same durable identity and payload adopts the stored receipt. Reusing that identity with another payload is a conflict. The request ID identifies one transport attempt and is not part of the durable request fingerprint. A retry after a local abort uses a new request ID and keeps the durable idempotency and submission IDs, so a late response from the aborted attempt cannot settle the retry.
|
|
166
104
|
|
|
167
|
-
|
|
105
|
+
An `interaction.submit` response stays open while the supervised workflow child validates the value. The response settles only after the durable result is accepted, adopted, or rejected. A tool abort stops waiting immediately but does not undo an accepted host command. A later retry adopts the durable outcome. The host waits on the stored submission ID returned by adoption, not a different ID from the retry attempt. Clients do not poll SQLite.
|
|
168
106
|
|
|
169
|
-
The
|
|
107
|
+
The protocol does not claim exactly-once behavior for an external system that cannot prove it. An uncertain non-idempotent effect becomes ambiguous and requires explicit recovery.
|
|
170
108
|
|
|
171
|
-
##
|
|
109
|
+
## Maintenance
|
|
172
110
|
|
|
173
|
-
The
|
|
111
|
+
Active `state.status`, `state.verify`, `state.backup`, and `state.prune` requests run in the host against its existing database connection. The CLI uses one stable client identity and creates a fresh idempotency key for each backup or applied prune invocation. If that invocation loses its connection, the client reconnects once with a new request ID and the same invocation key, so the host adopts the exact in-flight or stored result. A later user invocation gets a new key and does not reuse a stale success or rejection. The host keeps an in-flight maintenance command alive after a client disconnect, stores its accepted or rejected receipt before it responds, and adopts the exact retry instead of running the operation again. Host shutdown waits for in-flight maintenance receipts. `state.status` returns the database file size and safe counts for resources, runs, controllers, decisions, settings scopes, pending interactions, pending follow-ups, active leases, and unsettled effects.
|
|
174
112
|
|
|
175
|
-
|
|
113
|
+
Only `pi-workflows state verify <inactive-backup>` opens SQLite outside the host. It uses a query-only TypeScript connection and rejects the active database, including another path to the same file.
|
package/docs/tui-viewer.md
CHANGED
|
@@ -9,11 +9,11 @@ a recorded Pi conversation, themes, and remote viewing.
|
|
|
9
9
|
|
|
10
10
|
The viewer uses the [incremental and virtualized viewer design](plans/2026-08-28-piw-incremental-viewer-plan.md).
|
|
11
11
|
|
|
12
|
-
The run browser
|
|
12
|
+
The run browser subscribes to small host-owned metadata views. It does not load trace, step, session, settings, or follow-up payloads. The host publishes a new bounded view only when its content changes.
|
|
13
13
|
|
|
14
|
-
The selected run contains bounded pages. Step, trace, session-entry,
|
|
14
|
+
The selected run contains bounded pages. Step, trace, session-entry, session-event, settings, follow-up, and update pages have both a row limit and a byte budget. Replay can jump to any position. The viewer loads the page that contains that position and keeps only the current windows. A session-event page includes the replay checkpoint immediately before its first event. A compact graph projection keeps the latest attempt for each node and the taken transitions up to the replay point.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Large values use host content references. `piw` fetches them in bounded chunks when the user opens the related detail, verifies the byte count and SHA-256 digest, and then shows the complete text or JSON value. Page and content requests run outside input and drawing through the shared client protocol. A newer page selection replaces the previous request, including when the user returns to an earlier page. A failed first read leaves the run browser usable. A failed refresh keeps the last good view and marks it stale.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -26,17 +26,15 @@ cargo install pi-workflows
|
|
|
26
26
|
|
|
27
27
|
## Modes
|
|
28
28
|
|
|
29
|
-
- `piw`
|
|
30
|
-
- `piw <runId>` opens one run
|
|
31
|
-
- `piw
|
|
32
|
-
|
|
33
|
-
loopback addresses are accepted; use an SSH tunnel for remote viewing.
|
|
29
|
+
- `piw` connects to the local package-owned workflow host. If the socket is absent, it runs the installed `pi-workflows host start` command.
|
|
30
|
+
- `piw <runId>` opens one host-owned run view.
|
|
31
|
+
- `piw <runId> --once` waits for that run, renders one complete 120 × 40 plain-text frame, and exits. It returns a nonzero status for host, protocol, missing-run, or snapshot-timeout failures.
|
|
32
|
+
- `piw serve [--bind 127.0.0.1:9377]` relays each WebSocket connection to one host socket over the [live client protocol](live-replay-protocol.md). Only loopback addresses are accepted; use an SSH tunnel for remote viewing.
|
|
34
33
|
- `piw --connect ws://…` reads from another `piw serve` process.
|
|
35
34
|
- `piw --theme <name>` selects a theme for this invocation.
|
|
36
35
|
- `piw --list-themes` prints the built-in theme names.
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
The protocol is the network form of that view.
|
|
37
|
+
Local and remote modes use the same semantic run view and the same protocol. `piw` has no SQLite mode or database schema copy.
|
|
40
38
|
|
|
41
39
|
## Herdr
|
|
42
40
|
|
|
@@ -211,11 +209,9 @@ listing and selected-run subscription after the server returns. Cached content
|
|
|
211
209
|
stays visible but is labeled reconnecting or disconnected, never current.
|
|
212
210
|
Revision gaps force a bounded snapshot.
|
|
213
211
|
|
|
214
|
-
`piw serve`
|
|
212
|
+
`piw serve` is a frame relay. It keeps no run projection or client activity after either side closes. Clients keep separate revision and page cursors, so one client's replay jump does not move another client. A lagged client gets a bounded snapshot instead of an unbounded patch backlog.
|
|
215
213
|
|
|
216
|
-
|
|
217
|
-
The local reader uses query-only mode. Remote snapshots carry the same bounded
|
|
218
|
-
semantic view and do not expose a filesystem path.
|
|
214
|
+
The host resolves expanded prompt and output fields from durable content. Local and remote snapshots carry the same bounded semantic view and do not expose a database path.
|
|
219
215
|
|
|
220
216
|
## Interaction
|
|
221
217
|
|
package/docs/workflows.md
CHANGED
|
@@ -126,6 +126,8 @@ pi-workflows host run
|
|
|
126
126
|
`host run` stays attached. The other commands start, inspect, or stop the
|
|
127
127
|
on-demand process. No command installs an operating-system service.
|
|
128
128
|
|
|
129
|
+
The extension, CLI, and `piw` use the same version-1 client protocol over a Unix socket or Windows named pipe. The host sends byte-bounded run-list and run-view pages. Clients collect a complete run list for one revision and reject stale run pages whose cursor or revision no longer matches. The host reads only the selected history ranges and reuses an unchanged subscribed view after a lightweight revision check. It waits for a slow socket to drain and removes every subscription when its client unsubscribes. Large values stay available through verified content chunks. Host-generated aggregate values are saved and linked to the run before the host advertises them.
|
|
130
|
+
|
|
129
131
|
A worker verifies the root and all mounted source identities before it loads
|
|
130
132
|
workflow modules. It then checks the resolved mounted-source map and executes
|
|
131
133
|
from committed state through a host-backed store. A source mismatch parks the
|
|
@@ -193,7 +195,10 @@ For submitted output, the engine appends the existing workflow-tool contract.
|
|
|
193
195
|
The host checks the durable transport identifiers, stores a `validating`
|
|
194
196
|
submission, and starts a supervised worker. In that worker, the output passes
|
|
195
197
|
through tolerant JSON normalization and then `validate`. The tool reports
|
|
196
|
-
success only after this check accepts the output.
|
|
198
|
+
success only after this check accepts the output. If the tool turn is aborted,
|
|
199
|
+
the client stops waiting but does not cancel the durable host command. A retry
|
|
200
|
+
uses a new transport request ID with the same durable submission identity and
|
|
201
|
+
adopts the stored result. Rejected submissions return
|
|
197
202
|
the validation error and can retry in the same step. If the model settles
|
|
198
203
|
without submitting, the durable request stays pending until it receives valid
|
|
199
204
|
output, times out, or is cancelled. For assistant-message output, the engine appends a normal-response contract,
|
|
@@ -475,8 +480,18 @@ The model sees one `workflow` tool. Its `action` field supports:
|
|
|
475
480
|
A direct user request to continue or resume the active workflow maps to
|
|
476
481
|
`resume` immediately. The model does not call `status` instead of `resume` or
|
|
477
482
|
use it as a prerequisite. An already active run adopts the resume request. A
|
|
478
|
-
paused
|
|
479
|
-
|
|
483
|
+
paused interaction remains the same durable request and resumes without a
|
|
484
|
+
worker. Other paused or parked work gets a new claim generation and worker.
|
|
485
|
+
With no resumable run, the host rejects the request.
|
|
486
|
+
|
|
487
|
+
The origin Pi session shows its active run in the workflow widget. `Shift+Up`
|
|
488
|
+
and `Shift+Down` scroll it. If Escape aborts the model turn started by a
|
|
489
|
+
presented workflow interaction, the extension detects the public context abort
|
|
490
|
+
signal and pauses that run through the host. It also accepts Pi's public
|
|
491
|
+
`aborted` stop reason. The matching `agent_end` event must contain the workflow
|
|
492
|
+
prompt, so an unrelated interrupted turn cannot pause old pending work. The
|
|
493
|
+
paused run does not accept updates, submissions, or decision answers until
|
|
494
|
+
`resume`.
|
|
480
495
|
|
|
481
496
|
`status` reports the durable queue projection. A host command succeeds only
|
|
482
497
|
after its transaction commits. The protocol stores request fingerprints and
|
|
@@ -785,3 +800,41 @@ const engine = new WorkflowEngine({
|
|
|
785
800
|
});
|
|
786
801
|
const { state } = await engine.run(workflow, { task: "..." });
|
|
787
802
|
```
|
|
803
|
+
|
|
804
|
+
## Test the installed package
|
|
805
|
+
|
|
806
|
+
The installed-package end-to-end test packs this repository and installs the
|
|
807
|
+
archive with production dependencies in a temporary consumer project. It then
|
|
808
|
+
starts the repository-pinned base Pi with only that Pi Workflows installation.
|
|
809
|
+
The test checks command isolation, host startup, widget and status changes,
|
|
810
|
+
pause, resume, completion, and `piw <runId> --once` output.
|
|
811
|
+
|
|
812
|
+
Run the deterministic phase without a model call:
|
|
813
|
+
|
|
814
|
+
```bash
|
|
815
|
+
npm run test:e2e:live -- --runtime-only
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
A real-model phase is manual. Supply the exact Pi provider and model as separate
|
|
819
|
+
values. The runner does not select a default or accept model fallback:
|
|
820
|
+
|
|
821
|
+
```bash
|
|
822
|
+
npm run test:e2e:live -- \
|
|
823
|
+
--provider openai \
|
|
824
|
+
--model gpt-5.6-luna
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
For subscription authentication, use a dedicated Pi profile that has no other
|
|
828
|
+
extensions or resources:
|
|
829
|
+
|
|
830
|
+
```bash
|
|
831
|
+
npm run test:e2e:live -- \
|
|
832
|
+
--profile ~/.config/pi-workflows-e2e/openai-codex \
|
|
833
|
+
--provider openai-codex \
|
|
834
|
+
--model gpt-5.6-luna
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
The profile and normal provider environment remain operator-owned. The runner
|
|
838
|
+
does not read, copy, print, or save credentials. It uses one guarded temporary
|
|
839
|
+
root and removes that root after success or failure. Use `--keep` only when you
|
|
840
|
+
need the isolated files for diagnosis.
|
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osolmaz/pi-workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"examples",
|
|
25
25
|
"docs",
|
|
26
26
|
"schemas",
|
|
27
|
+
"protocol",
|
|
27
28
|
"plugins/herdr",
|
|
28
29
|
"herdr-plugin.toml",
|
|
29
30
|
"README.md",
|
|
@@ -39,6 +40,10 @@
|
|
|
39
40
|
"types": "./dist/extension/index.d.ts",
|
|
40
41
|
"default": "./dist/extension/index.js"
|
|
41
42
|
},
|
|
43
|
+
"./client": {
|
|
44
|
+
"types": "./dist/client/index.d.ts",
|
|
45
|
+
"default": "./dist/client/index.js"
|
|
46
|
+
},
|
|
42
47
|
"./controllers": {
|
|
43
48
|
"types": "./dist/controllers/index.d.ts",
|
|
44
49
|
"default": "./dist/controllers/index.js"
|
|
@@ -62,11 +67,13 @@
|
|
|
62
67
|
"test": "vitest run",
|
|
63
68
|
"test:coverage": "vitest run --coverage",
|
|
64
69
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
70
|
+
"test:e2e:live": "node scripts/live-e2e.mjs",
|
|
65
71
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
66
72
|
},
|
|
67
73
|
"dependencies": {
|
|
68
74
|
"better-sqlite3": "^13.0.2",
|
|
69
|
-
"jiti": "^2.7.0"
|
|
75
|
+
"jiti": "^2.7.0",
|
|
76
|
+
"tsx": "^4.23.13"
|
|
70
77
|
},
|
|
71
78
|
"devDependencies": {
|
|
72
79
|
"@earendil-works/pi-ai": "0.84.2",
|
|
@@ -77,7 +84,6 @@
|
|
|
77
84
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
78
85
|
"oxfmt": "^0.59.0",
|
|
79
86
|
"oxlint": "^1.74.0",
|
|
80
|
-
"tsx": "^4.23.1",
|
|
81
87
|
"typebox": "^1.3.6",
|
|
82
88
|
"typescript": "^7.0.2",
|
|
83
89
|
"vitest": "^4.1.10"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/osolmaz/pi-workflows/blob/main/protocol/client.v1.schema.json",
|
|
4
|
+
"title": "Pi Workflows client protocol version 1",
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": ["schema", "type", "connectionId", "packageVersion"],
|
|
10
|
+
"properties": {
|
|
11
|
+
"schema": { "const": "pi-workflows.client.v1" },
|
|
12
|
+
"type": { "const": "hello" },
|
|
13
|
+
"connectionId": { "$ref": "#/$defs/opaqueId" },
|
|
14
|
+
"packageVersion": { "type": "string", "minLength": 1 }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": [
|
|
21
|
+
"schema",
|
|
22
|
+
"type",
|
|
23
|
+
"requestId",
|
|
24
|
+
"clientId",
|
|
25
|
+
"operation",
|
|
26
|
+
"idempotencyKey",
|
|
27
|
+
"payload"
|
|
28
|
+
],
|
|
29
|
+
"properties": {
|
|
30
|
+
"schema": { "const": "pi-workflows.client.v1" },
|
|
31
|
+
"type": { "const": "request" },
|
|
32
|
+
"requestId": { "$ref": "#/$defs/opaqueId" },
|
|
33
|
+
"clientId": { "$ref": "#/$defs/opaqueId" },
|
|
34
|
+
"operation": {
|
|
35
|
+
"enum": [
|
|
36
|
+
"run.start",
|
|
37
|
+
"run.pause",
|
|
38
|
+
"run.resume",
|
|
39
|
+
"run.cancel",
|
|
40
|
+
"run.status",
|
|
41
|
+
"run.list",
|
|
42
|
+
"checkpoint.answer",
|
|
43
|
+
"decision.answer",
|
|
44
|
+
"interaction.submit",
|
|
45
|
+
"interaction.update",
|
|
46
|
+
"notification.claim",
|
|
47
|
+
"notification.deliver",
|
|
48
|
+
"turn.claim",
|
|
49
|
+
"turn.resolve",
|
|
50
|
+
"controller.list",
|
|
51
|
+
"controller.get",
|
|
52
|
+
"controller.apply",
|
|
53
|
+
"controller.reconcile",
|
|
54
|
+
"controller.delete",
|
|
55
|
+
"host.status",
|
|
56
|
+
"host.stop",
|
|
57
|
+
"view.runs.watch",
|
|
58
|
+
"view.runs.page",
|
|
59
|
+
"view.run.get",
|
|
60
|
+
"view.run.watch",
|
|
61
|
+
"view.run.unwatch",
|
|
62
|
+
"view.page",
|
|
63
|
+
"view.content",
|
|
64
|
+
"view.session.watch",
|
|
65
|
+
"activity.report",
|
|
66
|
+
"state.status",
|
|
67
|
+
"state.verify",
|
|
68
|
+
"state.backup",
|
|
69
|
+
"state.prune"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"idempotencyKey": { "$ref": "#/$defs/opaqueId" },
|
|
73
|
+
"runId": { "$ref": "#/$defs/opaqueId" },
|
|
74
|
+
"expectedRevision": { "$ref": "#/$defs/revision" },
|
|
75
|
+
"payload": {}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"required": ["schema", "type", "requestId", "outcome"],
|
|
82
|
+
"properties": {
|
|
83
|
+
"schema": { "const": "pi-workflows.client.v1" },
|
|
84
|
+
"type": { "const": "response" },
|
|
85
|
+
"requestId": { "$ref": "#/$defs/opaqueId" },
|
|
86
|
+
"outcome": {
|
|
87
|
+
"enum": [
|
|
88
|
+
"accepted",
|
|
89
|
+
"adopted",
|
|
90
|
+
"rejected",
|
|
91
|
+
"conflict",
|
|
92
|
+
"notFound",
|
|
93
|
+
"claimLost",
|
|
94
|
+
"unavailable"
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"revision": { "$ref": "#/$defs/revision" },
|
|
98
|
+
"receipt": {},
|
|
99
|
+
"error": { "type": "string", "minLength": 1 }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "object",
|
|
104
|
+
"additionalProperties": false,
|
|
105
|
+
"required": ["schema", "type", "subscriptionId", "event", "payload"],
|
|
106
|
+
"properties": {
|
|
107
|
+
"schema": { "const": "pi-workflows.client.v1" },
|
|
108
|
+
"type": { "const": "event" },
|
|
109
|
+
"subscriptionId": { "$ref": "#/$defs/opaqueId" },
|
|
110
|
+
"event": {
|
|
111
|
+
"enum": [
|
|
112
|
+
"runs",
|
|
113
|
+
"run_snapshot",
|
|
114
|
+
"run_patch",
|
|
115
|
+
"run_page",
|
|
116
|
+
"session_snapshot",
|
|
117
|
+
"unavailable"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"revision": { "$ref": "#/$defs/revision" },
|
|
121
|
+
"runId": { "$ref": "#/$defs/opaqueId" },
|
|
122
|
+
"payload": {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"$defs": {
|
|
127
|
+
"opaqueId": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"minLength": 1,
|
|
130
|
+
"maxLength": 256
|
|
131
|
+
},
|
|
132
|
+
"revision": {
|
|
133
|
+
"type": "integer",
|
|
134
|
+
"minimum": 0
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|