@osolmaz/pi-workflows 0.1.0 → 0.3.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 +107 -26
- package/dist/builtins/monitor.workflow.d.ts +71 -0
- package/dist/builtins/monitor.workflow.js +234 -0
- package/dist/builtins/monitor.workflow.js.map +1 -0
- package/dist/controllers/conditions.d.ts +6 -0
- package/dist/controllers/conditions.js +68 -0
- package/dist/controllers/conditions.js.map +1 -0
- package/dist/controllers/definition.d.ts +6 -0
- package/dist/controllers/definition.js +45 -0
- package/dist/controllers/definition.js.map +1 -0
- package/dist/controllers/effects.d.ts +14 -0
- package/dist/controllers/effects.js +104 -0
- package/dist/controllers/effects.js.map +1 -0
- package/dist/controllers/errors.d.ts +12 -0
- package/dist/controllers/errors.js +25 -0
- package/dist/controllers/errors.js.map +1 -0
- package/dist/controllers/index.d.ts +13 -0
- package/dist/controllers/index.js +13 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/json.d.ts +5 -0
- package/dist/controllers/json.js +57 -0
- package/dist/controllers/json.js.map +1 -0
- package/dist/controllers/loader.d.ts +23 -0
- package/dist/controllers/loader.js +74 -0
- package/dist/controllers/loader.js.map +1 -0
- package/dist/controllers/manager.d.ts +58 -0
- package/dist/controllers/manager.js +399 -0
- package/dist/controllers/manager.js.map +1 -0
- package/dist/controllers/results.d.ts +5 -0
- package/dist/controllers/results.js +32 -0
- package/dist/controllers/results.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +212 -0
- package/dist/controllers/sqlite.js +1009 -0
- package/dist/controllers/sqlite.js.map +1 -0
- package/dist/controllers/store.d.ts +112 -0
- package/dist/controllers/store.js +32 -0
- package/dist/controllers/store.js.map +1 -0
- package/dist/controllers/types.d.ts +159 -0
- package/dist/controllers/types.js +2 -0
- package/dist/controllers/types.js.map +1 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
- package/dist/controllers/workflow-engine-scheduler.js +93 -0
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
- package/dist/controllers/workflows.d.ts +27 -0
- package/dist/controllers/workflows.js +109 -0
- package/dist/controllers/workflows.js.map +1 -0
- package/dist/extension/controller-host.d.ts +47 -0
- package/dist/extension/controller-host.js +110 -0
- package/dist/extension/controller-host.js.map +1 -0
- package/dist/extension/executor.d.ts +14 -1
- package/dist/extension/executor.js +12 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +1118 -89
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +91 -0
- package/dist/extension/recorder.js +537 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/widget.js +25 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +28 -0
- package/dist/extension/workflow-tool.js +33 -0
- package/dist/extension/workflow-tool.js.map +1 -0
- package/dist/host/processes.d.ts +24 -0
- package/dist/host/processes.js +114 -0
- package/dist/host/processes.js.map +1 -0
- package/dist/host/rpc-bridge.d.ts +9 -0
- package/dist/host/rpc-bridge.js +39 -0
- package/dist/host/rpc-bridge.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +38 -0
- package/dist/host/rpc-executor.js +254 -0
- package/dist/host/rpc-executor.js.map +1 -0
- package/dist/host/runner.d.ts +49 -0
- package/dist/host/runner.js +350 -0
- package/dist/host/runner.js.map +1 -0
- package/dist/render/canvas.d.ts +1 -1
- package/dist/render/canvas.js +5 -0
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +5 -0
- package/dist/render/graph-render.js +211 -48
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/cli.d.ts +7 -3
- package/dist/viewer/cli.js +150 -19
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -3
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +45 -0
- package/dist/viewer/session-reducer.js +266 -0
- package/dist/viewer/session-reducer.js.map +1 -0
- package/dist/workflows/artifacts.d.ts +40 -0
- package/dist/workflows/artifacts.js +155 -0
- package/dist/workflows/artifacts.js.map +1 -0
- package/dist/workflows/engine.d.ts +38 -0
- package/dist/workflows/engine.js +281 -20
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +23 -0
- package/dist/workflows/errors.js +38 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/graph.js +0 -5
- package/dist/workflows/graph.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -3
- package/dist/workflows/loader.js +10 -1
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.js +1 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +98 -10
- package/dist/workflows/store.js +921 -46
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +136 -3
- package/docs/CONTROLLERS.md +215 -0
- package/docs/development.md +51 -24
- package/docs/live-replay-protocol.md +155 -0
- package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
- package/docs/plans/session-event-replay-implementation-plan.md +494 -0
- package/docs/plans/tui-viewer-implementation-plan.md +64 -0
- package/docs/run-bundles.md +359 -55
- package/docs/session-event-journal.md +470 -0
- package/docs/tui-viewer.md +218 -0
- package/docs/workflows.md +131 -11
- package/examples/controllers/pull-request.controller.ts +215 -0
- package/package.json +11 -2
- package/src/builtins/monitor.workflow.ts +278 -0
- package/src/controllers/conditions.ts +110 -0
- package/src/controllers/definition.ts +65 -0
- package/src/controllers/effects.ts +123 -0
- package/src/controllers/errors.ts +27 -0
- package/src/controllers/index.ts +90 -0
- package/src/controllers/json.ts +62 -0
- package/src/controllers/loader.ts +104 -0
- package/src/controllers/manager.ts +533 -0
- package/src/controllers/results.ts +46 -0
- package/src/controllers/sqlite.ts +1427 -0
- package/src/controllers/store.ts +160 -0
- package/src/controllers/types.ts +183 -0
- package/src/controllers/workflow-engine-scheduler.ts +145 -0
- package/src/controllers/workflows.ts +152 -0
- package/src/extension/controller-host.ts +163 -0
- package/src/extension/executor.ts +29 -2
- package/src/extension/index.ts +1315 -109
- package/src/extension/recorder.ts +662 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/widget.ts +26 -24
- package/src/extension/workflow-tool.ts +59 -0
- package/src/host/processes.ts +119 -0
- package/src/host/rpc-bridge.ts +44 -0
- package/src/host/rpc-executor.ts +299 -0
- package/src/host/runner.ts +406 -0
- package/src/render/canvas.ts +19 -1
- package/src/render/graph-render.ts +277 -44
- package/src/viewer/cli.ts +167 -21
- package/src/viewer/render.ts +21 -3
- package/src/viewer/session-reducer.ts +347 -0
- package/src/workflows/artifacts.ts +188 -0
- package/src/workflows/engine.ts +365 -19
- package/src/workflows/errors.ts +45 -0
- package/src/workflows/graph.ts +0 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/loader.ts +13 -3
- package/src/workflows/schema.ts +1 -1
- package/src/workflows/store.ts +1157 -48
- package/src/workflows/types.ts +151 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Live replay protocol
|
|
2
|
+
|
|
3
|
+
The Rust viewer (`tui/`) can watch runs in two ways: by reading run bundles
|
|
4
|
+
directly from the filesystem (the default, in-process) or by connecting to a
|
|
5
|
+
`piw serve` WebSocket server. Both paths produce the same semantic state; the
|
|
6
|
+
protocol below is the network form of that state. Protocol id:
|
|
7
|
+
`pi-workflows.replay.v1`.
|
|
8
|
+
|
|
9
|
+
The server is a reader like any other: it only consumes run bundles (see
|
|
10
|
+
[run-bundles.md](run-bundles.md)) and never writes them. The protocol has no
|
|
11
|
+
authentication, so the server only accepts loopback bind addresses and refuses
|
|
12
|
+
to start on anything else; bundles contain private data, and remote viewing
|
|
13
|
+
goes through an SSH tunnel. Handshakes that
|
|
14
|
+
carry an `Origin` header are rejected: browsers always send one, and a web
|
|
15
|
+
page must not be able to read bundles by opening a WebSocket to localhost.
|
|
16
|
+
|
|
17
|
+
## Transport and framing
|
|
18
|
+
|
|
19
|
+
A single WebSocket endpoint (`/ws`). Every message is one JSON object with a
|
|
20
|
+
`type` field. Unknown message types and unknown fields must be ignored by both
|
|
21
|
+
sides. The server sends `hello` on connect; a client that does not recognize
|
|
22
|
+
the protocol id must disconnect.
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{ "type": "hello", "protocol": "pi-workflows.replay.v1" }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Run views
|
|
29
|
+
|
|
30
|
+
The unit of synchronization is the **run view**, the semantic state of one
|
|
31
|
+
run:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"manifest": { … },
|
|
36
|
+
"workflow": { … },
|
|
37
|
+
"state": { … },
|
|
38
|
+
"events": [ … ],
|
|
39
|
+
"session": {
|
|
40
|
+
"binding": { … },
|
|
41
|
+
"entries": [ … ],
|
|
42
|
+
"events": [ … ],
|
|
43
|
+
"eventsMalformed": false,
|
|
44
|
+
"eventsTornTail": false,
|
|
45
|
+
"capture": { … }
|
|
46
|
+
},
|
|
47
|
+
"live": true,
|
|
48
|
+
"possiblyInterrupted": false
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `manifest`, `workflow`, `state`, `events`, and every `session` field are the
|
|
53
|
+
bundle documents verbatim. `workflow` is the definition snapshot,
|
|
54
|
+
top-level `events` are parsed workflow trace lines, `session.entries` are
|
|
55
|
+
settled Pi entries, `session.events` are normalized temporal events, and
|
|
56
|
+
`session.capture` is capture integrity. `session.eventsMalformed` and
|
|
57
|
+
`session.eventsTornTail` are derived transport diagnostics from the live
|
|
58
|
+
tailer, not bundle documents. `session` is `null` until a binding exists.
|
|
59
|
+
- `live` is true while the run status is non-terminal and the bundle is still
|
|
60
|
+
growing. `possiblyInterrupted` is true when the status is `running` but the
|
|
61
|
+
bundle has not changed for 60 seconds.
|
|
62
|
+
- Artifact references inside the view stay references; contents are fetched
|
|
63
|
+
on demand.
|
|
64
|
+
|
|
65
|
+
Because the full trace and session history are part of the view, replay
|
|
66
|
+
scrubbing is a pure client-side operation; rewinding never requires the
|
|
67
|
+
server. Clients order session events by `seq` and use `at` only for playback
|
|
68
|
+
timing.
|
|
69
|
+
|
|
70
|
+
## Snapshot, then patches
|
|
71
|
+
|
|
72
|
+
State synchronization follows a snapshot-then-patch model. After a client
|
|
73
|
+
subscribes to a run, the server sends one `run_snapshot`, then a stream of
|
|
74
|
+
`run_patch` messages:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{ "type": "run_snapshot", "runId": "…", "revision": 3, "view": { … } }
|
|
78
|
+
{ "type": "run_patch", "runId": "…", "revision": 4, "patch": [
|
|
79
|
+
{ "op": "append", "path": "/events", "value": [ { "seq": 18, … } ] },
|
|
80
|
+
{ "op": "replace", "path": "/state", "value": { … } }
|
|
81
|
+
] }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `revision` increases by exactly 1 per patch. A client that observes a gap
|
|
85
|
+
must resubscribe and take a fresh snapshot.
|
|
86
|
+
- `patch` is JSON Patch (RFC 6902) plus one extension op: `append`, whose
|
|
87
|
+
`value` is an array of items appended to the array at `path`. Semantically
|
|
88
|
+
`append` equals a sequence of `add` ops at `/-`; it exists so that the
|
|
89
|
+
common case (trace and session growth) stays compact and readable.
|
|
90
|
+
- Session growth uses `append` at `/session/entries` and `/session/events`.
|
|
91
|
+
Capture changes use `replace` at `/session/capture`. Changes to the derived
|
|
92
|
+
tail diagnostics use `replace` at `/session/eventsMalformed` and
|
|
93
|
+
`/session/eventsTornTail`.
|
|
94
|
+
- The server waits 50 ms after a filesystem notification before refreshing,
|
|
95
|
+
so one token burst normally becomes one revision. Batch boundaries never
|
|
96
|
+
merge or alter event records.
|
|
97
|
+
- Patches are computed against the previous view revision; applying them in
|
|
98
|
+
order reproduces the server's view exactly.
|
|
99
|
+
|
|
100
|
+
## Messages
|
|
101
|
+
|
|
102
|
+
Client to server:
|
|
103
|
+
|
|
104
|
+
| type | fields | meaning |
|
|
105
|
+
| ---------------- | --------------- | ---------------------------- |
|
|
106
|
+
| `watch_runs` | — | subscribe to the run listing |
|
|
107
|
+
| `watch_run` | `runId` | subscribe to one run's view |
|
|
108
|
+
| `unwatch_run` | `runId` | end a run subscription |
|
|
109
|
+
| `fetch_artifact` | `runId`, `path` | request artifact contents |
|
|
110
|
+
|
|
111
|
+
Server to client:
|
|
112
|
+
|
|
113
|
+
| type | fields | meaning |
|
|
114
|
+
| -------------- | ---------------------------- | ----------------------------------------------- |
|
|
115
|
+
| `hello` | `protocol` | sent once on connect |
|
|
116
|
+
| `runs` | `runs` | full run listing (summaries), re-sent on change |
|
|
117
|
+
| `run_snapshot` | `runId`, `revision`, `view` | full view after subscribe |
|
|
118
|
+
| `run_patch` | `runId`, `revision`, `patch` | incremental view update |
|
|
119
|
+
| `artifact` | `runId`, `path`, `content` | artifact contents (UTF-8) |
|
|
120
|
+
| `error` | `message`, `runId?` | request failed |
|
|
121
|
+
|
|
122
|
+
Artifact requests are answered only from files below the artifact directory
|
|
123
|
+
declared by `manifest.paths.artifacts`. Paths outside that directory and
|
|
124
|
+
symlinks whose canonical targets leave it are refused. Responses are capped at
|
|
125
|
+
4 MiB of actual file size; anything else produces an `error`.
|
|
126
|
+
|
|
127
|
+
Run listing summaries are the manifest plus `live` and
|
|
128
|
+
`possiblyInterrupted`:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{ "type": "runs", "runs": [ { "manifest": { … }, "live": true, "possiblyInterrupted": false } ] }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The run listing is small and changes rarely, so it is always sent whole; only
|
|
135
|
+
run views use patches.
|
|
136
|
+
|
|
137
|
+
## Reconnection
|
|
138
|
+
|
|
139
|
+
The native client treats the run listing, selected run, and pending artifact
|
|
140
|
+
reads as desired state rather than one-shot commands. After a connection closes,
|
|
141
|
+
it keeps the cached run visible with a stale/reconnecting label, retries with
|
|
142
|
+
bounded backoff, sends `watch_runs` after the next valid `hello`, and restores
|
|
143
|
+
the current `watch_run`. A reconnect receives a fresh snapshot before later
|
|
144
|
+
patches. Pending artifact reads are resubmitted once per connection.
|
|
145
|
+
|
|
146
|
+
## Filesystem semantics behind the protocol
|
|
147
|
+
|
|
148
|
+
The server watches the runs directory (inotify with polling fallback) and
|
|
149
|
+
tails `trace.ndjson`, `session/entries.ndjson`, and `session/events.ndjson`
|
|
150
|
+
incrementally. Torn final NDJSON lines are buffered until complete. The server
|
|
151
|
+
also re-reads atomic `session/capture.json` changes. `state.json` and
|
|
152
|
+
`manifest.json` are
|
|
153
|
+
re-read on change; a `state.json` whose `traceSeq` is older than the last
|
|
154
|
+
tailed trace event is stale and is replaced when the writer catches up. After
|
|
155
|
+
a terminal status, watching stops.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add a durable controller runtime
|
|
3
|
+
author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
|
|
4
|
+
date: 2026-08-04
|
|
5
|
+
updated: 2026-08-04
|
|
6
|
+
status: implemented
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Controller runtime plan
|
|
10
|
+
|
|
11
|
+
Pi Workflows needs a controller mode for automation that spans repeated events, external state changes, and process restarts. The design in [CONTROLLERS.md](../CONTROLLERS.md) follows the Kubernetes controller pattern. Durable resources hold desired and observed state, events enqueue resource keys, and each reconciliation reads current facts before acting.
|
|
12
|
+
|
|
13
|
+
The implementation keeps the graph engine focused on finite jobs. Controllers start and observe workflows through a child-run interface. Workflow graphs keep their finite execution model.
|
|
14
|
+
|
|
15
|
+
## Shipped design
|
|
16
|
+
|
|
17
|
+
The implementation follows [CONTROLLERS.md](../CONTROLLERS.md) with these resolved choices:
|
|
18
|
+
|
|
19
|
+
- The local store uses `better-sqlite3` because the Node 22 SQLite module still emits an experimental warning.
|
|
20
|
+
- Controllers are discovered from `.pi/controllers/` and `~/.pi/agent/controllers/`.
|
|
21
|
+
- The existing `pi-workflows` executable provides read-only `controllers` and `controller` commands. Headless workers use the public `ControllerManager` API.
|
|
22
|
+
- The TypeScript CLI lists and inspects resources. The Rust viewer understands interrupted workflow runs. Resource views stay in the CLI for this release.
|
|
23
|
+
- Per-controller concurrency limits are manager configuration, while controller definitions contain reconciliation behavior and timeout only.
|
|
24
|
+
|
|
25
|
+
The persisted model was reviewed with Schemator before implementation. The review removed generic timestamps from public resources, kept provider details opaque, moved concurrency policy to the manager, and tightened event payloads to recursive JSON values. The run failed aggregate validation on two proposed structural moves, so those moves received a manual review instead of automatic application.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
The first release must provide:
|
|
30
|
+
|
|
31
|
+
- Typed controller definitions and durable resources with `spec`, `status`, generations and conditions, plus compare-and-swap versions.
|
|
32
|
+
- A durable keyed queue with deduplication, delayed wakeups, bounded concurrency, retry backoff, and expiring claims.
|
|
33
|
+
- Level-based reconciliation that receives a resource instead of an event payload.
|
|
34
|
+
- Effect records that recover safely when a process stops around an external mutation.
|
|
35
|
+
- Stable child workflow requests and completion wakeups.
|
|
36
|
+
- Deterministic mutation authority outside agent workflows.
|
|
37
|
+
- Public engine APIs, a Pi extension control surface, and local read-only status views.
|
|
38
|
+
- Tests for retries, duplicate events, conflicts, process interruption, and ambiguous external effects.
|
|
39
|
+
|
|
40
|
+
## Assumptions
|
|
41
|
+
|
|
42
|
+
The default controller store will use SQLite and local filesystem permissions that match the private run-bundle directory. The public store and queue contracts will allow another host to provide remote storage later.
|
|
43
|
+
|
|
44
|
+
The first release will support several workers in one process. Queue claims will survive process failure, but cross-host leader election will wait for a real remote deployment.
|
|
45
|
+
|
|
46
|
+
Existing workflow definitions and run bundles remain valid. Controller resources use a separate schema and store. Child workflow attempts use the current run-bundle format; their parent links live in the controller store.
|
|
47
|
+
|
|
48
|
+
## Open questions
|
|
49
|
+
|
|
50
|
+
Implementation should resolve these points before the public API is frozen:
|
|
51
|
+
|
|
52
|
+
- Confirm that the built-in Node SQLite API meets package portability and transaction requirements on every supported Node 22 release. Choose one documented dependency if it does not.
|
|
53
|
+
- Decide whether controller discovery belongs in `.pi/controllers/` and `~/.pi/agent/controllers/`, or whether controllers should be registered only from installed packages.
|
|
54
|
+
- Decide whether the existing `pi-workflows` executable should gain controller subcommands or whether the package should expose another binary.
|
|
55
|
+
- Set queue claim and retry defaults from local fault tests. Use the same evidence for retention and payload limits.
|
|
56
|
+
- Define the smallest viewer change that makes blocked and indeterminate resources easy to find.
|
|
57
|
+
|
|
58
|
+
## Source layout
|
|
59
|
+
|
|
60
|
+
Add a `src/controllers/` layer with no dependency on Pi:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
src/workflows/ finite graph execution
|
|
64
|
+
src/controllers/types.ts resources, conditions, results, definitions
|
|
65
|
+
src/controllers/store.ts store and queue interfaces
|
|
66
|
+
src/controllers/sqlite.ts local durable implementation
|
|
67
|
+
src/controllers/manager.ts claims, workers, retries, cancellation
|
|
68
|
+
src/controllers/effects.ts effect lifecycle and recovery
|
|
69
|
+
src/controllers/workflows.ts child workflow scheduler adapter
|
|
70
|
+
src/extension/ Pi commands and lifecycle hooks, plus UI
|
|
71
|
+
src/viewer/ read-only local views
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`src/workflows` must never import `src/controllers`. The controller layer may depend on exported workflow contracts through `controllers/workflows.ts`. The extension and viewer may use both layers and remain independent from each other. Update `slophammer.yml` before adding cross-layer imports.
|
|
75
|
+
|
|
76
|
+
## Work stages
|
|
77
|
+
|
|
78
|
+
### Resource contract
|
|
79
|
+
|
|
80
|
+
Define the TypeScript API, persisted schemas, condition helpers, generation rules, and compare-and-swap behavior. Review the persisted model with Schemator before implementation. Document the final decisions in [CONTROLLERS.md](../CONTROLLERS.md).
|
|
81
|
+
|
|
82
|
+
Add tests for spec updates, status-only writes, stale versions, condition transition times, deletion requests, and finalizer removal. A spec update must increase `generation`; a status update must preserve it.
|
|
83
|
+
|
|
84
|
+
### Store and queue
|
|
85
|
+
|
|
86
|
+
Implement the SQLite store in WAL mode with restrictive file permissions. Keep schema migration code explicit and fail closed on unknown schema versions.
|
|
87
|
+
|
|
88
|
+
Implement one queue row per controller and resource key. Enqueue must deduplicate, an expired claim must become available again, and a delayed requeue must survive process restart. Add bounded exponential backoff with jitter and a test clock.
|
|
89
|
+
|
|
90
|
+
Run concurrent worker tests that force compare-and-swap conflicts and claim expiry. All tests must use temporary directories.
|
|
91
|
+
|
|
92
|
+
### Controller manager
|
|
93
|
+
|
|
94
|
+
Implement `defineController`, `ControllerManager`, and the settled, immediate requeue, and delayed requeue results. Reconciliation requests carry a key; the manager reads the resource immediately before calling user code.
|
|
95
|
+
|
|
96
|
+
Enforce one active reconciliation per key. Apply returned status against the version that was read. A conflict discards the stale status and queues another pass. Add global and per-controller worker limits.
|
|
97
|
+
|
|
98
|
+
Emit structured lifecycle records with stable reconcile IDs and bounded error messages. Cancellation must stop new claims and give active reconcilers a fixed shutdown period.
|
|
99
|
+
|
|
100
|
+
### Effect recovery
|
|
101
|
+
|
|
102
|
+
Implement `ctx.effects.ensure()` around a request fingerprint and stable key. Save the pending record before calling the provider. Record each outcome as applied, rejected, or indeterminate without treating a local timeout as proof that the provider failed.
|
|
103
|
+
|
|
104
|
+
Require every effect driver to define observation and application behavior. Reusing a key with different input must fail. Add crash tests around the claim and provider request, then after the response and receipt storage.
|
|
105
|
+
|
|
106
|
+
Use a fake provider to prove that an indeterminate merge-like operation is observed before any retry. Test provider idempotency tokens and conditional request failures separately.
|
|
107
|
+
|
|
108
|
+
### Child workflows
|
|
109
|
+
|
|
110
|
+
Implement `ctx.workflows.ensure()` with a stable request key and input fingerprint. A repeated call must find the same active or completed request. A completion or interruption must enqueue the parent resource.
|
|
111
|
+
|
|
112
|
+
Keep run attempts immutable. Add an explicit interrupted outcome for an abandoned child attempt. Route action nodes through the effect interface when they participate in a controller operation. Document the recovery rule for each node type.
|
|
113
|
+
|
|
114
|
+
Test duplicate child requests, changed fingerprints, completion races, parent generation changes, and host restart between run creation and parent status update.
|
|
115
|
+
|
|
116
|
+
### Extension and viewer
|
|
117
|
+
|
|
118
|
+
Add controller discovery and a `/controller` command for listing resources, inspecting conditions, requesting reconciliation, and cancelling active local work. Use only documented Pi extension APIs.
|
|
119
|
+
|
|
120
|
+
Start local sources from `session_start` and close them idempotently during `session_shutdown`. Pi exit must leave durable resources and queue rows ready for another host. No background service is installed.
|
|
121
|
+
|
|
122
|
+
Add a resource list and detail view to the TypeScript viewer first. Extend the Rust viewer only after the text model and fixtures settle. Keep both views read-only.
|
|
123
|
+
|
|
124
|
+
### Acceptance controller
|
|
125
|
+
|
|
126
|
+
Build a local pull request controller against a fake GitHub-compatible server. Its spec names a repository, pull request, expected head, requested workflow, and approved mutations. Its status reports the observed head and child run together with check results and readiness conditions.
|
|
127
|
+
|
|
128
|
+
The controller must re-read the pull request before each effect. A changed head blocks the mutation. Duplicate webhooks and scheduled polls must converge on the same resource and child workflow request. No credential belongs in the child request. Strict credential isolation requires a separate authenticated effect broker because the Pi host and agent tools share a process environment.
|
|
129
|
+
|
|
130
|
+
Keep this controller as an example or integration package. GitHub-specific policy must stay outside the controller core.
|
|
131
|
+
|
|
132
|
+
## Acceptance criteria
|
|
133
|
+
|
|
134
|
+
The work is ready when all of the following hold:
|
|
135
|
+
|
|
136
|
+
- Ten identical enqueue calls produce one pending key and no duplicate child run.
|
|
137
|
+
- Two workers cannot reconcile the same key concurrently.
|
|
138
|
+
- A stale resource version cannot overwrite newer status.
|
|
139
|
+
- A spec change invalidates readiness from an older generation.
|
|
140
|
+
- Delayed work and expired claims recover after a fresh process opens the store.
|
|
141
|
+
- Each tested process stop around an external effect converges without an unobserved retry.
|
|
142
|
+
- A changed pull request head prevents the acceptance controller from applying its effect.
|
|
143
|
+
- The acceptance controller applies agent output only after deterministic authorization and precondition checks.
|
|
144
|
+
- Pi reload and shutdown leave no in-memory state required for later recovery.
|
|
145
|
+
- Current workflow and viewer tests keep passing, along with run-bundle tests.
|
|
146
|
+
|
|
147
|
+
## Verification
|
|
148
|
+
|
|
149
|
+
Run the focused controller tests during each stage, followed by the full repository checks:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm run check
|
|
153
|
+
npm run test:e2e
|
|
154
|
+
npx slophammer-ts@latest dry .
|
|
155
|
+
npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
|
|
156
|
+
npx -y @simpledoc/simpledoc check
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Exercise the Pi extension from source with `pi -e src/extension/index.ts`. Start a controller resource, stop Pi while work is pending, reopen the same store, and confirm that reconciliation continues from current state. Repeat the effect crash matrix with the fake provider and preserve the test output as CI evidence.
|
|
160
|
+
|
|
161
|
+
## Non-goals
|
|
162
|
+
|
|
163
|
+
The first implementation will not provide Kubernetes API compatibility, a distributed cluster scheduler, automatic service installation, or generic exactly-once execution. It will not place GitHub credentials or mutation policy in agent prompts. It will not use run bundles, comments, or viewer projections as the controller database.
|
|
164
|
+
|
|
165
|
+
## Documentation updates
|
|
166
|
+
|
|
167
|
+
Update [CONTROLLERS.md](../CONTROLLERS.md) whenever the public resource, queue, reconciliation, or effect contracts change. Add the shipped authoring surface to [workflows.md](../workflows.md), the source boundaries to [development.md](../development.md), and user-facing installation and commands to the root README after implementation.
|
|
168
|
+
|
|
169
|
+
Record any meaningful departure from this plan in this document before the implementation is considered complete.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add always-on workflow execution
|
|
3
|
+
author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
|
|
4
|
+
date: 2026-08-05
|
|
5
|
+
updated: 2026-08-05
|
|
6
|
+
status: implemented
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Always-on workflows plan
|
|
10
|
+
|
|
11
|
+
Pi Workflows should feel the same whether the user watches a run or walks away from it. In the user's words: "I might start a workflow locally in Pi then I wait for it to complete. All the while I am looking at the screen and I'm not closing the Pi window. When the workflow ends I just want to be able to continue the same Pi session like normal with a session up to date with what happened in the workflow." And: "I just want to interact by starting a workflow, closing it, and then coming back and then still being able to continue it when I open it up. It's syncing continuously or something."
|
|
12
|
+
|
|
13
|
+
These are not two modes. The user asked for "both in a single unified system." This plan makes the Pi window irrelevant to execution: closing or opening the window is a change in observation, not in the run. The work stays on one machine, uses the merged controller runtime as its foundation, and does not modify Pi core.
|
|
14
|
+
|
|
15
|
+
This revision incorporates an external design review of the first draft. The review confirmed the three design rules and found five structural gaps in the details. The Decisions section resolves each one before implementation starts.
|
|
16
|
+
|
|
17
|
+
## Design
|
|
18
|
+
|
|
19
|
+
Four rules define the system:
|
|
20
|
+
|
|
21
|
+
1. **Durable state is the only source of truth.** Run bundles, the queue, and the event log live on disk. Nothing important lives in a session's memory.
|
|
22
|
+
2. **Runners are interchangeable, but ownership is exclusive.** Any live process running the engine claims work from the durable queue. A run has exactly one owning runner at a time, proven by a claim token, and only that runner may write to the run.
|
|
23
|
+
3. **The session is always a view.** It attaches to a run's event stream and renders it. An open window sees a live tail; a reopened window catches up from the same stream. Interaction such as approvals uses durable waiting states, never prompts tied to the window's lifetime.
|
|
24
|
+
4. **Claims arbitrate every lifecycle decision.** Only the current claim holder may resume, interrupt, or write a terminal event for a run. Recovery code that finds an abandoned run goes through the queue instead of writing to the bundle directly.
|
|
25
|
+
|
|
26
|
+
The controller runtime already provides most of the machinery: a deduplicated queue with expiring claims, a structured event table, crash recovery through the trace tail, and guarded effect records. This plan extends that treatment to runs the user starts interactively and adds the view layer.
|
|
27
|
+
|
|
28
|
+
## Decisions
|
|
29
|
+
|
|
30
|
+
These points were open in the first draft. External review showed each one is load-bearing, so they are decided here.
|
|
31
|
+
|
|
32
|
+
1. **Write fencing on bundles.** Every claim records a runner ID, a token, and a lease expiry in the store. Every bundle write verifies the token inside the run lock before appending and fails the runner fast when the token no longer matches. Expiring claims alone do not stop a stalled runner from writing; fencing does.
|
|
33
|
+
2. **Explicit resume protocol.** Resume is a named operation, not a restart. It truncates a torn trace tail to the last complete line, rebuilds run state from the trace, seeds the trace sequence from the tail, accounts for already-executed steps against the step limit, and records a resume boundary event before continuing.
|
|
34
|
+
3. **Resume scope.** User-started runs get node-level resume. Controller child runs keep their current new-attempt semantics, because attempt immutability and parent-side retry already work and are tested. Both behaviors are explicit; nothing mixes silently.
|
|
35
|
+
4. **A dedicated run queue.** Interactive runs do not fit the controller queue, which is keyed to controller resources. A new `workflow_run_queue` table shares the claim, lease, and fencing pattern and adds runner affinity fields. Runs stay out of `/controller list`.
|
|
36
|
+
5. **Origin affinity.** An interactively started run is inserted and claimed in one transaction, so the session that started it owns it from birth. The standalone host takes over only when that claim is released or expires. This guarantees a watched run's conversation happens in the watching session.
|
|
37
|
+
6. **Close-to-park shutdown.** `session_shutdown` stops writing a terminal cancel event for queued runs. It aborts in-flight work without a terminal event and releases the claim, leaving a resumable bundle. A clean close releases claims explicitly; a crash relies on lease expiry.
|
|
38
|
+
7. **Per-attempt capture.** Session capture becomes segmented per attempt, keyed by attempt ID, so a run handed off to the host or a new session starts a fresh capture segment instead of failing integrity checks.
|
|
39
|
+
8. **Continuation runs for waiting.** A run that needs human input ends at a checkpoint with status `waiting`, preserving bundle immutability. An answered checkpoint starts a continuation run with a new run ID chained to its parent, carrying forward prior outputs. The parent link makes the chain inspectable.
|
|
40
|
+
9. **Source pinning.** The manifest stores a content hash of the workflow source at run start. Resume refuses to continue against changed source unless forced, and a forced resume records the mismatch in the trace.
|
|
41
|
+
10. **Snapshot catch-up.** Notifications are idempotent snapshots of run state as of a store sequence number, recomputed from the store, not a stream of one-off messages. The watermark persists per session before sending. A skipped incremental notification is subsumed by the next snapshot.
|
|
42
|
+
11. **A run-level event feed.** Run lifecycle transitions write rows into a store table, so one watermark covers both controller and run events. Tailing `trace.ndjson` is reserved for the single actively watched run.
|
|
43
|
+
12. **Store-error backoff.** Worker loops treat store errors such as `SQLITE_BUSY` as transient and back off instead of letting a worker die silently. The host's advisory lock guards host-versus-host only; the embedded runner does not take it, and a second host refuses to start.
|
|
44
|
+
13. **Orphan reaping.** The host spawns `pi --mode rpc` children in their own process group, records child PIDs in the bundle, and reaps known orphans on startup. Consequential actions stay behind guarded effects regardless.
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- Starting `/workflow run` in a Pi session creates a durable queued run claimed by that session, and shows its progress live.
|
|
49
|
+
- Closing Pi mid-run never loses the run and never writes a spurious terminal event. With the standalone host alive, the host reclaims and resumes the run. Without a host, the run waits and resumes when a runner returns.
|
|
50
|
+
- Reopening a session brings it up to date with an idempotent snapshot: what finished, what failed, what waits for input. No state is duplicated and no information is permanently lost.
|
|
51
|
+
- A run that needs human input ends at a checkpoint. The user answers with a command, and a continuation run carries the work forward. The wait survives any process lifetime.
|
|
52
|
+
- Killing any process at any point recovers without duplicate trace sequences, duplicate notifications of record, or duplicate external effects. A stalled runner that loses its claim can never write again.
|
|
53
|
+
- Everything uses documented Pi public APIs. No Pi core changes.
|
|
54
|
+
|
|
55
|
+
## Work items
|
|
56
|
+
|
|
57
|
+
1. **Run queue and fencing.** Add the `workflow_run_queue` table with claim tokens, lease expiry, and runner affinity. Gate every bundle write on the token inside the run lock. Insert-and-claim interactive starts in one transaction.
|
|
58
|
+
2. **Node-level resume.** Implement the explicit resume protocol from Decision 2, with source pinning from Decision 9. User-started runs resume at the interrupted node; controller child runs are untouched.
|
|
59
|
+
3. **Close-to-park and capture segments.** Change `session_shutdown` to abort-without-terminal plus claim release for queued runs. Split session capture into per-attempt segments the integrity checker understands.
|
|
60
|
+
4. **Continuation runs.** Chain an answered checkpoint to a new run ID with a parent link and carried-forward outputs. Render the chain as one logical run in views.
|
|
61
|
+
5. **Session sync.** Add the run-level event feed, per-session watermarks, snapshot catch-up on `session_start`, and noteworthy-event messages. Live watching tails `trace.ndjson` from a remembered byte offset with `fs.watch`, reusing the TUI viewer's file-tail path.
|
|
62
|
+
6. **Standalone host.** A `pi-workflows` CLI subcommand loads controller definitions, opens the project store, and runs claiming in a loop with store-error backoff. Conversation child nodes run in spawned headless `pi --mode rpc` sessions with orphan reaping. The host takes an advisory lock against other hosts, drains on SIGTERM, and recovers on restart. It is a foreground process the user runs in a terminal; it is not a service.
|
|
63
|
+
|
|
64
|
+
## Non-goals
|
|
65
|
+
|
|
66
|
+
- Pi core changes of any kind. Every integration uses public APIs: commands, session events, widgets, `sendUserMessage`, and `pi --mode rpc`.
|
|
67
|
+
- Multi-machine execution, a remote store, or leader election. SQLite and one machine are in scope; the store contracts leave room for a remote implementation later.
|
|
68
|
+
- Installing or configuring a system or user service. The host is a process the user starts and stops.
|
|
69
|
+
- A push channel from an external process into a live session. Polling the shared store is the mechanism, and it is fast enough.
|
|
70
|
+
- Exactly-once chat notifications. The guarantee is no duplicated state and no permanently lost information, delivered through idempotent snapshots.
|
|
71
|
+
- Exactly-once external side effects beyond the existing guarded effect records.
|
|
72
|
+
|
|
73
|
+
## Assumptions
|
|
74
|
+
|
|
75
|
+
- One machine and one user, with the store and run bundles on the local filesystem.
|
|
76
|
+
- Spawning `pi --mode rpc` per conversation child run is acceptable at the expected cadence. If startup cost proves too high, the host keeps a small pool of persistent RPC sessions instead. Both options stay outside Pi core.
|
|
77
|
+
- A polling interval of a few seconds is responsive enough for the session view. File watching covers the live tail of a watched run.
|
|
78
|
+
- The user does not need machine-sleep or power-loss coverage beyond crash recovery. A stopped machine stops work until a runner returns.
|
|
79
|
+
|
|
80
|
+
## Open questions
|
|
81
|
+
|
|
82
|
+
- Which events deserve a chat message and which belong only in the widget. The default should be quiet.
|
|
83
|
+
- The exact host command shape, for example `pi-workflows run --project <dir>` versus a subcommand under `controllers`.
|
|
84
|
+
|
|
85
|
+
## Departures from the decisions
|
|
86
|
+
|
|
87
|
+
The implementation matches the decisions above with these refinements:
|
|
88
|
+
|
|
89
|
+
- Graph validation now allows outgoing edges from checkpoint nodes. The old
|
|
90
|
+
rejection encoded terminal-forever checkpoints; continuations make those
|
|
91
|
+
edges live. This is a deliberate contract change for workflow authors.
|
|
92
|
+
- The host command is `pi-workflows host`, chosen over `run` because the
|
|
93
|
+
viewer CLI's vocabulary already uses runs for bundles.
|
|
94
|
+
- The first capture stays flat at `session/`; only binds from the second
|
|
95
|
+
recorder onward write segments under `session/segments/`. This keeps the
|
|
96
|
+
layout of every existing single-session bundle and the viewer unchanged.
|
|
97
|
+
- Resume always starts unpaused; the operator can pause again.
|
|
98
|
+
- Headless conversation children keep the exact tool contract: a bridge
|
|
99
|
+
extension loaded with `-e` registers the `workflow` tool and reports
|
|
100
|
+
submissions to the host over stderr, so no engine prompt changes were
|
|
101
|
+
needed.
|
|
102
|
+
- The host accepts explicit `storeFile` and `runsDir` options. Its defaults
|
|
103
|
+
resolve from the project and environment like every other entry point.
|
|
104
|
+
|
|
105
|
+
## Acceptance criteria
|
|
106
|
+
|
|
107
|
+
- Start a run in Pi, then close Pi while a conversation node is mid-response and the host is running. The host reclaims and resumes the run, and reopening Pi shows the catch-up snapshot and allows normal conversation about the result.
|
|
108
|
+
- The same flow without the host: the run resumes at the interrupted node when Pi reopens and completes.
|
|
109
|
+
- A stalled runner that loses its claim cannot write to the bundle afterward; fencing rejects its writes.
|
|
110
|
+
- An open session reflects host-driven progress within a few seconds. Two sessions on one project each get complete catch-up; neither starves the other.
|
|
111
|
+
- A waiting-for-input run survives closing and reopening. Answering it starts a continuation run that carries forward prior outputs.
|
|
112
|
+
- `kill -9` on the host mid-append, followed by a restart, repairs the torn trace tail and recovers without duplicate trace sequence numbers and without repeating an applied external effect. Orphaned RPC children are reaped.
|
|
113
|
+
- No changes to Pi core; the diff touches only this package.
|
|
114
|
+
|
|
115
|
+
## Verification
|
|
116
|
+
|
|
117
|
+
- `npm run check` and `npm run test:e2e`, including new real-Pi E2E tests that start a run, kill the host, restart it, and assert continuation.
|
|
118
|
+
- A fencing test with both runners alive and a forced lease loss.
|
|
119
|
+
- A two-session watermark test on one project.
|
|
120
|
+
- A `kill -9` mid-append torn-tail resume test.
|
|
121
|
+
- A checkpoint answer-to-continuation round-trip test.
|
|
122
|
+
- Extension tests with fake timers for the polling loop, watermark, and snapshot catch-up.
|
|
123
|
+
- `npx slophammer-ts@latest dry .` and the dependency-boundary check.
|
|
124
|
+
- `npx -y @simpledoc/simpledoc check` for documentation changes.
|
|
125
|
+
- Manual pass through both usage patterns from the user's request with the host in a terminal.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Let agents run and manage monitor workflows
|
|
3
|
+
author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
|
|
4
|
+
date: 2026-08-10
|
|
5
|
+
updated: 2026-08-10
|
|
6
|
+
status: implemented
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Agent-managed monitor workflows plan
|
|
10
|
+
|
|
11
|
+
The user should be able to tell an agent, "Monitor this every 30 minutes," and have the agent start the right workflow. The user must not write controller records or JSON. The existing `workflow` model tool should manage workflows instead of serving only as a step-submission tool.
|
|
12
|
+
|
|
13
|
+
A monitor is one Pi Workflows graph. It checks the target, reports a meaningful change, sleeps for the requested interval with the existing shell node, and loops. This plan does not use controllers, Unified Exec, a new wait node, or a second scheduler.
|
|
14
|
+
|
|
15
|
+
## Shipped design
|
|
16
|
+
|
|
17
|
+
The implementation follows this plan. The built-in monitor defaults to 1,000 checks, caps the interval at 24 hours, and uses a 5,010-step engine limit as a second guard. The normal Pi extension exposes all workflow tool actions. The headless RPC bridge exposes only `submit`. A real-Pi end-to-end test proves that a normal model turn can start the built-in monitor and complete its first check. The automated test inspects the exact 30-minute Node timer command and both timeout margins instead of making the test suite wait for 30 minutes. The Node timer keeps the shell action portable across Pi's supported platforms.
|
|
18
|
+
|
|
19
|
+
## User experience
|
|
20
|
+
|
|
21
|
+
A normal request looks like this:
|
|
22
|
+
|
|
23
|
+
> Monitor PR 123 every 30 minutes. Tell me when checks fail. Stop when it is merged or closed.
|
|
24
|
+
|
|
25
|
+
The agent recognizes repeated monitoring from the `workflow` tool description and starts the built-in `monitor` workflow. The tool call contains the structured input, but the user does not write it.
|
|
26
|
+
|
|
27
|
+
The monitor checks immediately, then waits between later checks. It writes a normal assistant message only when the requested report condition is met. It stops when the requested stop condition is met, when it reaches its safety limit, or when the user or agent cancels it.
|
|
28
|
+
|
|
29
|
+
Existing controls remain available:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
/workflow pause
|
|
33
|
+
/workflow resume
|
|
34
|
+
/workflow cancel
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The same operations become available to the model through the `workflow` tool. One active workflow per Pi session remains the rule. A long monitor therefore occupies that session's workflow slot.
|
|
38
|
+
|
|
39
|
+
## Workflow tool
|
|
40
|
+
|
|
41
|
+
Replace the current submit-only schema with one discriminated tool schema. Use Pi's documented `registerTool` API and `StringEnum` helper.
|
|
42
|
+
|
|
43
|
+
The tool supports these actions:
|
|
44
|
+
|
|
45
|
+
- `list`: list discovered workflows and their source.
|
|
46
|
+
- `start`: start a workflow by name or path with structured input.
|
|
47
|
+
- `status`: return a bounded summary of the active or named run.
|
|
48
|
+
- `pause`: pause the active run at its next node boundary.
|
|
49
|
+
- `resume`: resume the active paused run.
|
|
50
|
+
- `cancel`: cancel the active run.
|
|
51
|
+
- `answer`: answer a waiting checkpoint, with an optional run ID.
|
|
52
|
+
- `submit`: submit the result required by the current workflow step contract.
|
|
53
|
+
|
|
54
|
+
The `submit` form becomes:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"action": "submit",
|
|
59
|
+
"step": "check",
|
|
60
|
+
"attempt": "attempt-id",
|
|
61
|
+
"output": {}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Update workflow step prompts and reminders to include `action: "submit"`. Do not retain the old submit shape as a compatibility alias.
|
|
66
|
+
|
|
67
|
+
Slash commands and tool actions must call the same internal lifecycle functions. This avoids separate behavior for humans and models. Tool results must be structured and bounded. Errors must state whether the operation failed because no run exists, a run is already active, a checkpoint is not waiting, or the requested workflow cannot be found.
|
|
68
|
+
|
|
69
|
+
A tool call happens during an agent turn. The `start` action must validate and queue the launch, then deliver the first workflow prompt after the initiating turn settles. This prevents the new run from treating the initiating turn as a failed workflow step. It also prevents a workflow prompt from being injected in the middle of the tool call.
|
|
70
|
+
|
|
71
|
+
The headless RPC bridge keeps a submit-only version of the tool. Headless workflow children must not start or control other workflows. Its submit schema and parser still change to require `action: "submit"`.
|
|
72
|
+
|
|
73
|
+
## Agent guidance
|
|
74
|
+
|
|
75
|
+
The tool description must tell the model when to use the built-in workflow:
|
|
76
|
+
|
|
77
|
+
- Use `start` with workflow `monitor` when the user asks to watch, monitor, poll, or check something repeatedly.
|
|
78
|
+
- Start directly when the task and interval are clear.
|
|
79
|
+
- Ask one short question when the target or interval is missing.
|
|
80
|
+
- Use observation-only behavior unless the user explicitly authorizes a mutation.
|
|
81
|
+
- Do not create repeated work without a user request or an existing workflow instruction that authorizes it.
|
|
82
|
+
|
|
83
|
+
The `list` result identifies `monitor` as a built-in workflow and gives a short description. This keeps discovery inside the tool instead of adding a skill or a system-prompt injection.
|
|
84
|
+
|
|
85
|
+
## Built-in monitor workflow
|
|
86
|
+
|
|
87
|
+
Ship `monitor` as a built-in workflow in the Pi Workflows package. Built-ins have the lowest discovery precedence:
|
|
88
|
+
|
|
89
|
+
1. Project workflows under `.pi/workflows/`
|
|
90
|
+
2. Global workflows under `~/.pi/agent/workflows/`
|
|
91
|
+
3. Workflows bundled with Pi Workflows
|
|
92
|
+
|
|
93
|
+
A project or global `monitor.workflow.ts` can therefore replace the default. The built-in remains a real workflow file so run bundles can record its path and source hash with the existing rules.
|
|
94
|
+
|
|
95
|
+
The workflow input contains:
|
|
96
|
+
|
|
97
|
+
- `task`: what to inspect.
|
|
98
|
+
- `everyMinutes`: the interval between checks.
|
|
99
|
+
- `reportWhen`: changes or states that deserve a user message.
|
|
100
|
+
- `stopWhen`: the condition that ends monitoring.
|
|
101
|
+
- `maxChecks`: a hard safety limit.
|
|
102
|
+
|
|
103
|
+
The model supplies these fields from the user's request. The workflow validates the input and applies documented bounds. The first release supports intervals from 1 minute through 24 hours. It defaults `maxChecks` to a documented finite value when the request gives no end limit.
|
|
104
|
+
|
|
105
|
+
The graph is:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
prepare -> guard -> check
|
|
109
|
+
| continue quietly -> sleep -> guard
|
|
110
|
+
| continue and report -> report -> sleep -> guard
|
|
111
|
+
| stop quietly -> finish
|
|
112
|
+
| stop and report -> report-final -> finish
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`prepare` validates and normalizes the input. `guard` enforces `maxChecks`. `check` is an agent node that performs one observation and returns a validated route, a bounded observation, and an optional report. The next `check` can read the previous accepted `check` output, which Pi Workflows already keeps for looped nodes.
|
|
116
|
+
|
|
117
|
+
The report nodes write a normal assistant message and then submit an acknowledgement. Keeping reporting after accepted check output prevents the agent from showing a report before the structured result passes validation. The final presentation reports why the monitor stopped without repeating a report that the user already saw.
|
|
118
|
+
|
|
119
|
+
The sleep node uses the existing Pi Workflows shell action to launch the current Node executable with a timer. Set the node timeout above the largest supported interval because the engine default is 15 minutes. Set the shell execution timeout above the requested wait by a small fixed margin. Cancellation aborts the timer process immediately.
|
|
120
|
+
|
|
121
|
+
If the Pi TUI or standalone workflow host stops during sleep, Pi Workflows parks the run and kills the shell child. Resuming the run starts that sleep node again from the beginning. This is existing workflow behavior and is acceptable for this feature. No special timer persistence is added.
|
|
122
|
+
|
|
123
|
+
The workflow uses a high but finite `maxSteps` value as a second safety guard. Check and report values have explicit size limits so a long run cannot grow its bundle without bound.
|
|
124
|
+
|
|
125
|
+
## Repository changes
|
|
126
|
+
|
|
127
|
+
Make the feature in `osolmaz/pi-workflows`:
|
|
128
|
+
|
|
129
|
+
- Refactor workflow lifecycle operations out of the slash-command handler.
|
|
130
|
+
- Expand the normal Pi `workflow` tool and update its tests.
|
|
131
|
+
- Update the RPC bridge submit contract.
|
|
132
|
+
- Add built-in workflow discovery with project and global override precedence.
|
|
133
|
+
- Add the built-in monitor workflow and focused tests.
|
|
134
|
+
- Update `README.md` and `docs/workflows.md`.
|
|
135
|
+
|
|
136
|
+
After the upstream change is complete, update the pinned Pi Workflows commit in OnurPi's thin `packages/workflows` wrapper. Do not add a new OnurPi extension or copy a monitor file into live global state.
|
|
137
|
+
|
|
138
|
+
## State and API impact
|
|
139
|
+
|
|
140
|
+
- **Session state:** Normal workflow prompts, model replies, tool calls, and monitor reports are appended through Pi's normal session behavior.
|
|
141
|
+
- **Other persistent data:** No new data model. The feature uses existing run bundles and the existing workflow run queue.
|
|
142
|
+
- **Pi internals:** None.
|
|
143
|
+
- **Pi public API:** `registerTool`, `registerCommand`, `sendUserMessage`, and documented agent and session lifecycle events.
|
|
144
|
+
- **Pi Workflows API:** The workflow definition and run-state models do not change. The model-facing `workflow` tool contract changes, and discovery gains a lowest-priority built-in source.
|
|
145
|
+
|
|
146
|
+
## Non-goals
|
|
147
|
+
|
|
148
|
+
This work does not add cron expressions, calendar schedules, a background service, controller resources, OS notifications, concurrent workflows in one session, resumable shell processes, or guaranteed wall-clock wake times across runner shutdowns. It does not let headless child agents recursively start workflows.
|
|
149
|
+
|
|
150
|
+
## Acceptance criteria
|
|
151
|
+
|
|
152
|
+
- A user can ask for repeated monitoring in plain language, and the agent starts `monitor` through the existing `workflow` tool.
|
|
153
|
+
- The user does not type JSON or a slash command to start the monitor.
|
|
154
|
+
- The monitor checks immediately, sleeps for the requested interval, and checks again.
|
|
155
|
+
- An unchanged observation produces no normal assistant report.
|
|
156
|
+
- A matching report condition produces one concise assistant report.
|
|
157
|
+
- A matching stop condition reports as requested and ends the run.
|
|
158
|
+
- The safety limit ends a monitor that never reaches its stop condition.
|
|
159
|
+
- The model can list, start, inspect, pause, resume, cancel, answer, and submit through one tool.
|
|
160
|
+
- Slash commands and tool actions use the same lifecycle code.
|
|
161
|
+
- Starting from a model tool call does not cause an early nudge or record the initiating turn as the first workflow attempt.
|
|
162
|
+
- A 30-minute sleep is not stopped by the default 15-minute node timeout.
|
|
163
|
+
- Cancelling during sleep stops the shell child and ends the workflow.
|
|
164
|
+
- Project and global workflows override the built-in `monitor` name.
|
|
165
|
+
- Existing workflow, controller, host, viewer, and run-bundle tests continue to pass.
|
|
166
|
+
- OnurPi loads the updated wrapper and Pi starts successfully.
|
|
167
|
+
|
|
168
|
+
## Verification
|
|
169
|
+
|
|
170
|
+
Add unit and integration coverage for tool action validation, lifecycle dispatch, deferred model-start launches, built-in discovery precedence, monitor routing, quiet checks, reports, stop conditions, safety limits, cancellation during sleep, and the headless submit bridge.
|
|
171
|
+
|
|
172
|
+
Run:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm run check
|
|
176
|
+
npm run test:e2e
|
|
177
|
+
npx slophammer-ts@latest dry .
|
|
178
|
+
npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
|
|
179
|
+
npx -y @simpledoc/simpledoc check
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Test the extension from the Pi Workflows checkout with `pi -e src/extension/index.ts`. Use a short test interval in a controlled fixture, then perform one manual 30-minute monitor run to confirm that the configured node timeout does not stop it. Test plain-language startup with the normal model, then test list, status, pause, resume, cancel, and checkpoint answer actions.
|
|
183
|
+
|
|
184
|
+
After updating OnurPi, run its full checks and start Pi with the installed OnurPi package. Confirm that the model sees one `workflow` tool, discovers `monitor`, and can start it from a plain-language request.
|