@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.
Files changed (171) hide show
  1. package/README.md +107 -26
  2. package/dist/builtins/monitor.workflow.d.ts +71 -0
  3. package/dist/builtins/monitor.workflow.js +234 -0
  4. package/dist/builtins/monitor.workflow.js.map +1 -0
  5. package/dist/controllers/conditions.d.ts +6 -0
  6. package/dist/controllers/conditions.js +68 -0
  7. package/dist/controllers/conditions.js.map +1 -0
  8. package/dist/controllers/definition.d.ts +6 -0
  9. package/dist/controllers/definition.js +45 -0
  10. package/dist/controllers/definition.js.map +1 -0
  11. package/dist/controllers/effects.d.ts +14 -0
  12. package/dist/controllers/effects.js +104 -0
  13. package/dist/controllers/effects.js.map +1 -0
  14. package/dist/controllers/errors.d.ts +12 -0
  15. package/dist/controllers/errors.js +25 -0
  16. package/dist/controllers/errors.js.map +1 -0
  17. package/dist/controllers/index.d.ts +13 -0
  18. package/dist/controllers/index.js +13 -0
  19. package/dist/controllers/index.js.map +1 -0
  20. package/dist/controllers/json.d.ts +5 -0
  21. package/dist/controllers/json.js +57 -0
  22. package/dist/controllers/json.js.map +1 -0
  23. package/dist/controllers/loader.d.ts +23 -0
  24. package/dist/controllers/loader.js +74 -0
  25. package/dist/controllers/loader.js.map +1 -0
  26. package/dist/controllers/manager.d.ts +58 -0
  27. package/dist/controllers/manager.js +399 -0
  28. package/dist/controllers/manager.js.map +1 -0
  29. package/dist/controllers/results.d.ts +5 -0
  30. package/dist/controllers/results.js +32 -0
  31. package/dist/controllers/results.js.map +1 -0
  32. package/dist/controllers/sqlite.d.ts +212 -0
  33. package/dist/controllers/sqlite.js +1009 -0
  34. package/dist/controllers/sqlite.js.map +1 -0
  35. package/dist/controllers/store.d.ts +112 -0
  36. package/dist/controllers/store.js +32 -0
  37. package/dist/controllers/store.js.map +1 -0
  38. package/dist/controllers/types.d.ts +159 -0
  39. package/dist/controllers/types.js +2 -0
  40. package/dist/controllers/types.js.map +1 -0
  41. package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
  42. package/dist/controllers/workflow-engine-scheduler.js +93 -0
  43. package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
  44. package/dist/controllers/workflows.d.ts +27 -0
  45. package/dist/controllers/workflows.js +109 -0
  46. package/dist/controllers/workflows.js.map +1 -0
  47. package/dist/extension/controller-host.d.ts +47 -0
  48. package/dist/extension/controller-host.js +110 -0
  49. package/dist/extension/controller-host.js.map +1 -0
  50. package/dist/extension/executor.d.ts +14 -1
  51. package/dist/extension/executor.js +12 -2
  52. package/dist/extension/executor.js.map +1 -1
  53. package/dist/extension/index.d.ts +7 -0
  54. package/dist/extension/index.js +1118 -89
  55. package/dist/extension/index.js.map +1 -1
  56. package/dist/extension/recorder.d.ts +91 -0
  57. package/dist/extension/recorder.js +537 -0
  58. package/dist/extension/recorder.js.map +1 -0
  59. package/dist/extension/session-events.d.ts +134 -0
  60. package/dist/extension/session-events.js +60 -0
  61. package/dist/extension/session-events.js.map +1 -0
  62. package/dist/extension/widget.js +25 -24
  63. package/dist/extension/widget.js.map +1 -1
  64. package/dist/extension/workflow-tool.d.ts +28 -0
  65. package/dist/extension/workflow-tool.js +33 -0
  66. package/dist/extension/workflow-tool.js.map +1 -0
  67. package/dist/host/processes.d.ts +24 -0
  68. package/dist/host/processes.js +114 -0
  69. package/dist/host/processes.js.map +1 -0
  70. package/dist/host/rpc-bridge.d.ts +9 -0
  71. package/dist/host/rpc-bridge.js +39 -0
  72. package/dist/host/rpc-bridge.js.map +1 -0
  73. package/dist/host/rpc-executor.d.ts +38 -0
  74. package/dist/host/rpc-executor.js +254 -0
  75. package/dist/host/rpc-executor.js.map +1 -0
  76. package/dist/host/runner.d.ts +49 -0
  77. package/dist/host/runner.js +350 -0
  78. package/dist/host/runner.js.map +1 -0
  79. package/dist/render/canvas.d.ts +1 -1
  80. package/dist/render/canvas.js +5 -0
  81. package/dist/render/canvas.js.map +1 -1
  82. package/dist/render/graph-render.d.ts +5 -0
  83. package/dist/render/graph-render.js +211 -48
  84. package/dist/render/graph-render.js.map +1 -1
  85. package/dist/viewer/cli.d.ts +7 -3
  86. package/dist/viewer/cli.js +150 -19
  87. package/dist/viewer/cli.js.map +1 -1
  88. package/dist/viewer/render.js +19 -3
  89. package/dist/viewer/render.js.map +1 -1
  90. package/dist/viewer/session-reducer.d.ts +45 -0
  91. package/dist/viewer/session-reducer.js +266 -0
  92. package/dist/viewer/session-reducer.js.map +1 -0
  93. package/dist/workflows/artifacts.d.ts +40 -0
  94. package/dist/workflows/artifacts.js +155 -0
  95. package/dist/workflows/artifacts.js.map +1 -0
  96. package/dist/workflows/engine.d.ts +38 -0
  97. package/dist/workflows/engine.js +281 -20
  98. package/dist/workflows/engine.js.map +1 -1
  99. package/dist/workflows/errors.d.ts +23 -0
  100. package/dist/workflows/errors.js +38 -0
  101. package/dist/workflows/errors.js.map +1 -1
  102. package/dist/workflows/graph.js +0 -5
  103. package/dist/workflows/graph.js.map +1 -1
  104. package/dist/workflows/index.d.ts +3 -2
  105. package/dist/workflows/index.js +2 -1
  106. package/dist/workflows/index.js.map +1 -1
  107. package/dist/workflows/loader.d.ts +5 -3
  108. package/dist/workflows/loader.js +10 -1
  109. package/dist/workflows/loader.js.map +1 -1
  110. package/dist/workflows/schema.js +1 -1
  111. package/dist/workflows/schema.js.map +1 -1
  112. package/dist/workflows/store.d.ts +98 -10
  113. package/dist/workflows/store.js +921 -46
  114. package/dist/workflows/store.js.map +1 -1
  115. package/dist/workflows/types.d.ts +136 -3
  116. package/docs/CONTROLLERS.md +215 -0
  117. package/docs/development.md +51 -24
  118. package/docs/live-replay-protocol.md +155 -0
  119. package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
  120. package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
  121. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
  122. package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
  123. package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
  124. package/docs/plans/session-event-replay-implementation-plan.md +494 -0
  125. package/docs/plans/tui-viewer-implementation-plan.md +64 -0
  126. package/docs/run-bundles.md +359 -55
  127. package/docs/session-event-journal.md +470 -0
  128. package/docs/tui-viewer.md +218 -0
  129. package/docs/workflows.md +131 -11
  130. package/examples/controllers/pull-request.controller.ts +215 -0
  131. package/package.json +11 -2
  132. package/src/builtins/monitor.workflow.ts +278 -0
  133. package/src/controllers/conditions.ts +110 -0
  134. package/src/controllers/definition.ts +65 -0
  135. package/src/controllers/effects.ts +123 -0
  136. package/src/controllers/errors.ts +27 -0
  137. package/src/controllers/index.ts +90 -0
  138. package/src/controllers/json.ts +62 -0
  139. package/src/controllers/loader.ts +104 -0
  140. package/src/controllers/manager.ts +533 -0
  141. package/src/controllers/results.ts +46 -0
  142. package/src/controllers/sqlite.ts +1427 -0
  143. package/src/controllers/store.ts +160 -0
  144. package/src/controllers/types.ts +183 -0
  145. package/src/controllers/workflow-engine-scheduler.ts +145 -0
  146. package/src/controllers/workflows.ts +152 -0
  147. package/src/extension/controller-host.ts +163 -0
  148. package/src/extension/executor.ts +29 -2
  149. package/src/extension/index.ts +1315 -109
  150. package/src/extension/recorder.ts +662 -0
  151. package/src/extension/session-events.ts +119 -0
  152. package/src/extension/widget.ts +26 -24
  153. package/src/extension/workflow-tool.ts +59 -0
  154. package/src/host/processes.ts +119 -0
  155. package/src/host/rpc-bridge.ts +44 -0
  156. package/src/host/rpc-executor.ts +299 -0
  157. package/src/host/runner.ts +406 -0
  158. package/src/render/canvas.ts +19 -1
  159. package/src/render/graph-render.ts +277 -44
  160. package/src/viewer/cli.ts +167 -21
  161. package/src/viewer/render.ts +21 -3
  162. package/src/viewer/session-reducer.ts +347 -0
  163. package/src/workflows/artifacts.ts +188 -0
  164. package/src/workflows/engine.ts +365 -19
  165. package/src/workflows/errors.ts +45 -0
  166. package/src/workflows/graph.ts +0 -5
  167. package/src/workflows/index.ts +15 -0
  168. package/src/workflows/loader.ts +13 -3
  169. package/src/workflows/schema.ts +1 -1
  170. package/src/workflows/store.ts +1157 -48
  171. package/src/workflows/types.ts +151 -3
package/README.md CHANGED
@@ -29,8 +29,16 @@ Or try the npm package without installing it:
29
29
  pi -e npm:@osolmaz/pi-workflows
30
30
  ```
31
31
 
32
- The `pi-workflows` viewer binary is part of the same package. To get it on
33
- your PATH, clone the repo and run `npm install && npm run build && npm link`,
32
+ Install the interactive terminal viewer separately from crates.io. The crate
33
+ is named `pi-workflows`; its command is `piw`:
34
+
35
+ ```bash
36
+ cargo install pi-workflows
37
+ piw
38
+ ```
39
+
40
+ The npm package also includes the simpler `pi-workflows` snapshot viewer. To
41
+ link that command from a clone, run `npm install && npm run build && npm link`,
34
42
  or run it in place with `npx tsx src/viewer/cli.ts`.
35
43
 
36
44
  ## Quick start
@@ -63,16 +71,16 @@ Then, from any pi conversation:
63
71
  ```
64
72
 
65
73
  `/workflow` with no arguments lists discovered workflows. `/workflow pause`
66
- lets the current step finish and then holds the run before the next node
67
- useful when you want to interject in the conversation mid-workflow
68
- and `/workflow resume` continues it. Pressing escape to interrupt a turn
74
+ lets the current step finish and then holds the run before the next node. This
75
+ is useful when you want to interject in the conversation mid-workflow.
76
+ `/workflow resume` continues it. Pressing escape to interrupt a turn
69
77
  pauses the workflow automatically, so the run never nudges the model while
70
78
  you have taken the conversation back; `/workflow resume` re-delivers the
71
79
  pending step prompt. `/workflow cancel` stops the active run; if the last run
72
80
  already ended (for example parked at a checkpoint), it clears the leftover
73
81
  widget instead. Trailing text becomes `{ task: "..." }`; pass arbitrary input
74
- with `--input-json {"key": "value"}`. The names `cancel`, `list`, `pause`,
75
- and `resume` are reserved and rejected as workflow names.
82
+ with `--input-json {"key": "value"}`. The names `answer`, `cancel`, `list`, `pause`, `resume`, and `status` are
83
+ reserved and rejected as workflow names.
76
84
 
77
85
  While a run is on screen, the footer status bar shows a compact
78
86
  `wf <name> [status] <node>` indicator alongside the widget.
@@ -82,6 +90,26 @@ structured run ends to request one normal, human-readable assistant response.
82
90
  Workflows without it remain silent after their final structured output, which
83
91
  keeps shell-only and machine-consumed workflows model-free.
84
92
 
93
+ ## Agent-managed workflows
94
+
95
+ The model can use the same `workflow` tool to list, start, inspect, pause,
96
+ resume, cancel, and answer workflows. Step contracts use the tool's `submit`
97
+ action. Slash commands and model actions share one lifecycle implementation.
98
+
99
+ Pi Workflows includes a `monitor` workflow for plain-language requests such as:
100
+
101
+ > Monitor PR 123 every 30 minutes. Report failed checks. Stop when it is merged or closed.
102
+
103
+ The monitor checks immediately, reports only the states requested by the user,
104
+ waits with a normal shell action, and loops until its stop condition or check
105
+ limit. Its input supports `task`, `everyMinutes`, `reportWhen`, `stopWhen`, and
106
+ `maxChecks`. Project and global workflows can replace the built-in `monitor`
107
+ by using the same file name.
108
+
109
+ A monitor occupies the session's one active workflow slot. If its Pi runner
110
+ stops during the shell wait, the run parks and repeats that wait node when a
111
+ runner resumes it.
112
+
85
113
  Because the workflow runs in your current conversation, you can have a long
86
114
  discussion first and then trigger a workflow that builds on it. The
87
115
  `elegant-solution` example does exactly that. It asks the model for the most
@@ -105,30 +133,37 @@ pi-workflows view --once # print a snapshot and exit (good for scripts)
105
133
  ```
106
134
 
107
135
  The run detail view draws the workflow as a boxed graph, like the acpx replay
108
- viewer: every node sits in a box (heavy border for the active node), branches
109
- carry their case labels, the taken path is highlighted, and loops route
110
- through a gutter on the right back into their target from above. `←/→` scrubs
136
+ viewer. Every card has a centered step-name header and a divider above its
137
+ structured metadata. Border characters keep the graph background, the body
138
+ surface begins inside the border, and the header interior uses a separate
139
+ surface. Node type, status, attempts, and timing use compact symbol rows; start
140
+ and terminal markers sit outside the card. Node types have distinct
141
+ semantic colors, active cards use a heavy border, branches carry their case
142
+ labels, the taken path is highlighted, and loops route through a gutter on the
143
+ right back into their target from above. `←/→` scrubs
111
144
  backwards and forwards through the recorded steps and re-derives every node's
112
145
  status as of that step, with the selected step's full output shown below;
113
146
  scrubbing to the end snaps back to following the run live.
114
147
 
148
+ The Rust `piw` viewer under `tui/` adds a Catppuccin interface, selectable
149
+ themes, centered active-node following, draggable browser and inspector sizes,
150
+ detailed trace and conversation inspection, temporal replay, and reconnecting
151
+ remote viewing. Full cards have one fixed graph-wide size, so streaming,
152
+ selection, timer ticks, and replay never move nodes or edges. Live conversation
153
+ capture shows text, thinking, tool calls, and tool execution as they happen,
154
+ then reconciles settled messages to verbatim Pi entries. See
155
+ [the piw guide](docs/tui-viewer.md).
156
+
115
157
  ```
116
- │ ┌──────────────────┐
117
- ▼ ▼ │
118
- ┌──────────────────────────┐ │
119
- verify [action] 8.0s ×2 │ │
120
- └──────────────────────────┘ │
121
- │ │
122
- ▼ │
123
- ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
124
- ┃ ◐ review [agent] 12s ×2 ┃ │
125
- ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
126
- ┌─ clean ─┘ └─ issues ─┐ │
127
- ▼ ▼ │
128
- ┌───────────┐ ┌───────────────┐ │
129
- │ · done │ │ ✓ fix [agent] │ │
130
- └───────────┘ └───────────────┘ │
131
- └──────────┘
158
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
159
+ ┃ review ┃
160
+ ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
161
+ agent ◐ running
162
+ ┃ ↻ 2 ◷ 12s ┃
163
+ ┃ ◇ clean ┃
164
+ ┃ ◇ issues_found
165
+ ┃ … reviewing implementation ┃
166
+ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
132
167
  ```
133
168
 
134
169
  Inside pi, a widget above the editor shows the same boxed graph while a
@@ -153,6 +188,48 @@ the model to pick from a fixed set of choices and validates the answer, and
153
188
  See [docs/workflows.md](docs/workflows.md) for the full authoring reference
154
189
  and [docs/run-bundles.md](docs/run-bundles.md) for the on-disk run format.
155
190
 
191
+ ## Controllers
192
+
193
+ Controllers keep long-running automation aligned with current external state. They store desired state in `spec`, report observed state through conditions and `status`, and reconcile a deduplicated resource key whenever an event or retry makes it ready.
194
+
195
+ Put `*.controller.ts` files in `.pi/controllers/` or `~/.pi/agent/controllers/`. Import the API from `@osolmaz/pi-workflows/controllers`:
196
+
197
+ ```typescript
198
+ import { conditionTrue, defineController } from "@osolmaz/pi-workflows/controllers";
199
+
200
+ export default defineController({
201
+ name: "example",
202
+ initialStatus: () => ({ phase: "new" }),
203
+ reconcile: (ctx, resource) =>
204
+ ctx.settled({
205
+ controllerStatus: { phase: "done" },
206
+ conditions: [conditionTrue("Ready", "Complete")],
207
+ }),
208
+ });
209
+ ```
210
+
211
+ Apply and inspect resources from Pi:
212
+
213
+ ```text
214
+ /controller apply example item-1 {"enabled":true}
215
+ /controller get example item-1
216
+ /controller reconcile example item-1
217
+ ```
218
+
219
+ The standalone CLI provides read-only views with `pi-workflows controllers` and `pi-workflows controller <controller> <key>`. See [docs/CONTROLLERS.md](docs/CONTROLLERS.md) for reconciliation, queue, effect, and child workflow semantics.
220
+
221
+ ## Always-on workflows
222
+
223
+ Runs do not depend on the Pi window. Every `/workflow` run is claimed through a durable queue, so closing Pi mid-run **parks** the run instead of cancelling it, and any runner can **resume** it later at the node it stopped on. Reopening Pi resumes parked runs itself and catches the session up through the shared event feed; a checkpointed run waits durably until you answer it with `/workflow answer <json>`, which continues the graph in a linked run.
224
+
225
+ For runs that must continue while Pi is closed, keep the standalone host running:
226
+
227
+ ```bash
228
+ pi-workflows host --project /path/to/project
229
+ ```
230
+
231
+ The host claims parked runs and reconciles controllers without a Pi session. Conversation nodes execute in headless `pi --mode rpc` children that expose the same `workflow` tool contract. It is a foreground process — stop it with Ctrl-C; a crashed host's leftovers are reaped by the next one. See [docs/workflows.md](docs/workflows.md#durable-runs-parking-and-resume) for the model and [docs/run-bundles.md](docs/run-bundles.md) for the on-disk rules.
232
+
156
233
  ## Examples
157
234
 
158
235
  The [examples/workflows/](examples/workflows/) directory mirrors the acpx
@@ -177,6 +254,10 @@ example set. Copy any of them into `.pi/workflows/` to use them:
177
254
  result plateaus or a diverse generation all fails, then conclusions are
178
255
  written before the winner is promoted out of the loop directory.
179
256
 
257
+ The controller example at `examples/controllers/pull-request.controller.ts`
258
+ shows child repair work and check polling. It also uses expected-head guards
259
+ and recoverable merge effects.
260
+
180
261
  ## License
181
262
 
182
263
  [MIT](LICENSE)
@@ -0,0 +1,71 @@
1
+ declare const _default: {
2
+ name: string;
3
+ title: ({ input }: {
4
+ input: unknown;
5
+ workflowName: string;
6
+ }) => string;
7
+ presentationPrompt: ({ finalOutput }: import("../workflows/types.js").WorkflowPresentationContext) => string | undefined;
8
+ startAt: string;
9
+ maxSteps: number;
10
+ nodes: {
11
+ prepare: import("../workflows/types.js").ComputeNodeDefinition;
12
+ guard: import("../workflows/types.js").ComputeNodeDefinition;
13
+ continue_guard: import("../workflows/types.js").ComputeNodeDefinition;
14
+ check: import("../workflows/types.js").AgentNodeDefinition;
15
+ report_continue: import("../workflows/types.js").AgentNodeDefinition;
16
+ report_stop: import("../workflows/types.js").AgentNodeDefinition;
17
+ sleep: import("../workflows/types.js").ShellActionNodeDefinition;
18
+ finish: import("../workflows/types.js").ComputeNodeDefinition;
19
+ };
20
+ edges: ({
21
+ from: string;
22
+ to: string;
23
+ switch?: never;
24
+ } | {
25
+ to?: never;
26
+ from: string;
27
+ switch: {
28
+ on: string;
29
+ cases: {
30
+ check: string;
31
+ stop: string;
32
+ continue_quiet?: never;
33
+ continue_report?: never;
34
+ stop_quiet?: never;
35
+ stop_report?: never;
36
+ sleep?: never;
37
+ };
38
+ };
39
+ } | {
40
+ to?: never;
41
+ from: string;
42
+ switch: {
43
+ on: string;
44
+ cases: {
45
+ check?: never;
46
+ continue_quiet: string;
47
+ continue_report: string;
48
+ stop_quiet: string;
49
+ stop_report: string;
50
+ sleep?: never;
51
+ stop?: never;
52
+ };
53
+ };
54
+ } | {
55
+ to?: never;
56
+ from: string;
57
+ switch: {
58
+ on: string;
59
+ cases: {
60
+ check?: never;
61
+ continue_quiet?: never;
62
+ continue_report?: never;
63
+ stop_quiet?: never;
64
+ stop_report?: never;
65
+ sleep: string;
66
+ stop: string;
67
+ };
68
+ };
69
+ })[];
70
+ };
71
+ export default _default;
@@ -0,0 +1,234 @@
1
+ import { agent, compute, defineWorkflow, shell } from "../workflows/index.js";
2
+ const MIN_INTERVAL_MINUTES = 1;
3
+ const MAX_INTERVAL_MINUTES = 24 * 60;
4
+ const DEFAULT_MAX_CHECKS = 1_000;
5
+ const MAX_CHECKS = 1_000;
6
+ const MAX_OBSERVATION_CHARS = 8_000;
7
+ const MAX_REPORT_CHARS = 4_000;
8
+ const MAX_REASON_CHARS = 2_000;
9
+ const SLEEP_TIMEOUT_MARGIN_MS = 60_000;
10
+ const NODE_TIMEOUT_MARGIN_MS = 2 * 60_000;
11
+ const MONITOR_ROUTES = new Set([
12
+ "continue_quiet",
13
+ "continue_report",
14
+ "stop_quiet",
15
+ "stop_report",
16
+ ]);
17
+ function requireRecord(value, label) {
18
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
19
+ throw new Error(`${label} must be an object`);
20
+ }
21
+ return value;
22
+ }
23
+ function requireBoundedString(value, label, maxChars) {
24
+ if (typeof value !== "string") {
25
+ throw new Error(`${label} must be a string`);
26
+ }
27
+ const trimmed = value.trim();
28
+ if (trimmed.length === 0) {
29
+ throw new Error(`${label} must not be empty`);
30
+ }
31
+ if (trimmed.length > maxChars) {
32
+ throw new Error(`${label} must be at most ${maxChars} characters`);
33
+ }
34
+ return trimmed;
35
+ }
36
+ function prepareInput(input) {
37
+ const value = requireRecord(input, "monitor input");
38
+ const task = requireBoundedString(value.task, "task", 8_000);
39
+ if (typeof value.everyMinutes !== "number" ||
40
+ !Number.isInteger(value.everyMinutes) ||
41
+ value.everyMinutes < MIN_INTERVAL_MINUTES ||
42
+ value.everyMinutes > MAX_INTERVAL_MINUTES) {
43
+ throw new Error(`everyMinutes must be an integer from ${MIN_INTERVAL_MINUTES} through ${MAX_INTERVAL_MINUTES}`);
44
+ }
45
+ const maxChecks = value.maxChecks ?? DEFAULT_MAX_CHECKS;
46
+ if (!Number.isInteger(maxChecks) || maxChecks <= 0 || maxChecks > MAX_CHECKS) {
47
+ throw new Error(`maxChecks must be an integer from 1 through ${MAX_CHECKS}`);
48
+ }
49
+ return {
50
+ task,
51
+ everyMinutes: value.everyMinutes,
52
+ reportWhen: value.reportWhen === undefined
53
+ ? "The observed state changes materially or needs the user's attention."
54
+ : requireBoundedString(value.reportWhen, "reportWhen", 4_000),
55
+ stopWhen: value.stopWhen === undefined
56
+ ? "The user cancels the monitor or it reaches its maximum check count."
57
+ : requireBoundedString(value.stopWhen, "stopWhen", 4_000),
58
+ maxChecks,
59
+ };
60
+ }
61
+ function configFrom(outputs) {
62
+ return outputs.prepare;
63
+ }
64
+ function completedChecks(context) {
65
+ return context.state.steps.filter((step) => step.nodeId === "check" && step.outcome === "ok")
66
+ .length;
67
+ }
68
+ function validateCheck(output) {
69
+ const value = requireRecord(output, "monitor check output");
70
+ if (typeof value.route !== "string" || !MONITOR_ROUTES.has(value.route)) {
71
+ throw new Error(`route must be one of ${[...MONITOR_ROUTES].join(", ")}`);
72
+ }
73
+ const route = value.route;
74
+ const observation = requireBoundedString(value.observation, "observation", MAX_OBSERVATION_CHARS);
75
+ const reason = requireBoundedString(value.reason, "reason", MAX_REASON_CHARS);
76
+ const reports = route === "continue_report" || route === "stop_report";
77
+ const report = value.report === undefined
78
+ ? undefined
79
+ : requireBoundedString(value.report, "report", MAX_REPORT_CHARS);
80
+ if (reports && report === undefined) {
81
+ throw new Error(`route ${route} requires a report`);
82
+ }
83
+ return {
84
+ route,
85
+ observation,
86
+ ...(report !== undefined ? { report } : {}),
87
+ reason,
88
+ };
89
+ }
90
+ function validateReportAck(output) {
91
+ const value = requireRecord(output, "report acknowledgement");
92
+ if (value.reported !== true) {
93
+ throw new Error("report acknowledgement must set reported to true");
94
+ }
95
+ return { reported: true };
96
+ }
97
+ function reportPrompt(outputs) {
98
+ const check = outputs.check;
99
+ return [
100
+ "Write one concise normal assistant message to the user with this monitoring update:",
101
+ check.report ?? check.observation,
102
+ "Do not add unrelated detail.",
103
+ "After writing the update, submit the acknowledgement required by the workflow step contract.",
104
+ ].join("\n\n");
105
+ }
106
+ export default defineWorkflow({
107
+ name: "monitor",
108
+ title: ({ input }) => {
109
+ try {
110
+ const task = prepareInput(input).task;
111
+ return `monitor: ${task.slice(0, 80)}`;
112
+ }
113
+ catch {
114
+ return "monitor";
115
+ }
116
+ },
117
+ presentationPrompt: ({ finalOutput }) => {
118
+ const result = requireRecord(finalOutput, "monitor result");
119
+ if (result.reported === true) {
120
+ return undefined;
121
+ }
122
+ return `Tell the user concisely why this monitor stopped: ${String(result.reason ?? "monitor ended")}`;
123
+ },
124
+ startAt: "prepare",
125
+ maxSteps: 5_010,
126
+ nodes: {
127
+ prepare: compute({
128
+ run: ({ input }) => prepareInput(input),
129
+ }),
130
+ guard: compute({
131
+ run: (context) => {
132
+ const config = configFrom(context.outputs);
133
+ const checks = completedChecks(context);
134
+ return checks >= config.maxChecks
135
+ ? { route: "stop", checks, reason: `Reached the ${config.maxChecks}-check limit.` }
136
+ : { route: "check", checks };
137
+ },
138
+ }),
139
+ continue_guard: compute({
140
+ run: (context) => {
141
+ const config = configFrom(context.outputs);
142
+ const checks = completedChecks(context);
143
+ return checks >= config.maxChecks
144
+ ? { route: "stop", checks, reason: `Reached the ${config.maxChecks}-check limit.` }
145
+ : { route: "sleep", checks };
146
+ },
147
+ }),
148
+ check: agent({
149
+ statusDetail: "checking monitored target",
150
+ prompt: (context) => {
151
+ const config = configFrom(context.outputs);
152
+ const previous = context.outputs.check;
153
+ const checkNumber = completedChecks(context) + 1;
154
+ return [
155
+ `Perform monitoring check ${checkNumber} of at most ${config.maxChecks}.`,
156
+ `Task: ${config.task}`,
157
+ `Report when: ${config.reportWhen}`,
158
+ `Stop when: ${config.stopWhen}`,
159
+ previous === undefined
160
+ ? "There is no previous observation. Report the initial state only when the report condition calls for it."
161
+ : `Previous accepted observation: ${previous.observation}`,
162
+ "Use available tools to inspect the current state. Observe only unless the task explicitly authorizes a mutation.",
163
+ "Choose continue_quiet, continue_report, stop_quiet, or stop_report. A report route requires concise report text.",
164
+ ].join("\n\n");
165
+ },
166
+ expectedOutput: '{ "route": "continue_quiet" | "continue_report" | "stop_quiet" | "stop_report", "observation": "current factual state", "report": "required for report routes", "reason": "short reason" }',
167
+ validate: (output) => validateCheck(output),
168
+ }),
169
+ report_continue: agent({
170
+ statusDetail: "reporting monitor update",
171
+ prompt: ({ outputs }) => reportPrompt(outputs),
172
+ expectedOutput: '{ "reported": true }',
173
+ validate: (output) => validateReportAck(output),
174
+ }),
175
+ report_stop: agent({
176
+ statusDetail: "reporting final monitor update",
177
+ prompt: ({ outputs }) => reportPrompt(outputs),
178
+ expectedOutput: '{ "reported": true }',
179
+ validate: (output) => validateReportAck(output),
180
+ }),
181
+ sleep: shell({
182
+ statusDetail: "waiting for next monitor check",
183
+ timeoutMs: MAX_INTERVAL_MINUTES * 60_000 + NODE_TIMEOUT_MARGIN_MS,
184
+ exec: ({ outputs }) => {
185
+ const config = configFrom(outputs);
186
+ const sleepMs = config.everyMinutes * 60_000;
187
+ return {
188
+ command: process.execPath,
189
+ args: ["-e", "setTimeout(() => {}, Number(process.argv[1]))", String(sleepMs)],
190
+ timeoutMs: sleepMs + SLEEP_TIMEOUT_MARGIN_MS,
191
+ maxOutputChars: 1_024,
192
+ };
193
+ },
194
+ parse: (_result, { outputs }) => ({ waitedMinutes: configFrom(outputs).everyMinutes }),
195
+ }),
196
+ finish: compute({
197
+ run: ({ outputs }) => {
198
+ const check = outputs.check;
199
+ const guard = outputs.guard;
200
+ const continueGuard = outputs.continue_guard;
201
+ return {
202
+ reason: continueGuard?.reason ?? guard?.reason ?? check?.reason ?? "Monitor finished.",
203
+ observation: check?.observation ?? null,
204
+ reported: outputs.report_stop !== undefined ||
205
+ (check !== undefined && check.route === "stop_report"),
206
+ };
207
+ },
208
+ }),
209
+ },
210
+ edges: [
211
+ { from: "prepare", to: "guard" },
212
+ { from: "guard", switch: { on: "$.route", cases: { check: "check", stop: "finish" } } },
213
+ {
214
+ from: "check",
215
+ switch: {
216
+ on: "$.route",
217
+ cases: {
218
+ continue_quiet: "continue_guard",
219
+ continue_report: "report_continue",
220
+ stop_quiet: "finish",
221
+ stop_report: "report_stop",
222
+ },
223
+ },
224
+ },
225
+ { from: "report_continue", to: "continue_guard" },
226
+ { from: "report_stop", to: "finish" },
227
+ {
228
+ from: "continue_guard",
229
+ switch: { on: "$.route", cases: { sleep: "sleep", stop: "finish" } },
230
+ },
231
+ { from: "sleep", to: "guard" },
232
+ ],
233
+ });
234
+ //# sourceMappingURL=monitor.workflow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.workflow.js","sourceRoot":"","sources":["../../src/builtins/monitor.workflow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { ControllerCondition, ControllerConditionInput, ControllerResourceStatus, ControllerStatusPatch } from "./types.js";
2
+ export declare function conditionTrue(type: string, reason: string, message?: string): ControllerConditionInput;
3
+ export declare function conditionFalse(type: string, reason: string, message?: string): ControllerConditionInput;
4
+ export declare function conditionUnknown(type: string, reason: string, message?: string): ControllerConditionInput;
5
+ export declare function applyStatusPatch<TStatus>(current: ControllerResourceStatus<TStatus>, patch: ControllerStatusPatch<TStatus> | undefined, generation: number, now: string): ControllerResourceStatus<TStatus>;
6
+ export declare function mergeConditions(current: ControllerCondition[], updates: ControllerConditionInput[], generation: number, now: string): ControllerCondition[];
@@ -0,0 +1,68 @@
1
+ export function conditionTrue(type, reason, message) {
2
+ return condition(type, true, reason, message);
3
+ }
4
+ export function conditionFalse(type, reason, message) {
5
+ return condition(type, false, reason, message);
6
+ }
7
+ export function conditionUnknown(type, reason, message) {
8
+ return condition(type, "unknown", reason, message);
9
+ }
10
+ export function applyStatusPatch(current, patch, generation, now) {
11
+ return {
12
+ observedGeneration: generation,
13
+ conditions: mergeConditions(current.conditions, patch?.conditions ?? [], generation, now),
14
+ controllerStatus: patch !== undefined && Object.hasOwn(patch, "controllerStatus")
15
+ ? patch.controllerStatus
16
+ : current.controllerStatus,
17
+ ...(patch?.workflowRun === null
18
+ ? {}
19
+ : patch?.workflowRun !== undefined
20
+ ? { workflowRun: patch.workflowRun }
21
+ : current.workflowRun !== undefined
22
+ ? { workflowRun: current.workflowRun }
23
+ : {}),
24
+ };
25
+ }
26
+ export function mergeConditions(current, updates, generation, now) {
27
+ const byType = new Map(current.map((item) => [item.type, item]));
28
+ const seen = new Set();
29
+ for (const update of updates) {
30
+ validateCondition(update);
31
+ if (seen.has(update.type)) {
32
+ throw new Error(`Condition ${JSON.stringify(update.type)} was updated more than once`);
33
+ }
34
+ seen.add(update.type);
35
+ const previous = byType.get(update.type);
36
+ const next = {
37
+ ...update,
38
+ observedGeneration: generation,
39
+ lastTransitionTime: previous !== undefined && previous.status === update.status
40
+ ? previous.lastTransitionTime
41
+ : now,
42
+ };
43
+ byType.set(update.type, next);
44
+ }
45
+ return [...byType.values()];
46
+ }
47
+ function condition(type, status, reason, message) {
48
+ const value = {
49
+ type,
50
+ status,
51
+ reason,
52
+ ...(message !== undefined ? { message } : {}),
53
+ };
54
+ validateCondition(value);
55
+ return value;
56
+ }
57
+ function validateCondition(value) {
58
+ if (value.type.trim().length === 0) {
59
+ throw new Error("Condition type must not be empty");
60
+ }
61
+ if (value.reason.trim().length === 0) {
62
+ throw new Error("Condition reason must not be empty");
63
+ }
64
+ if (value.status !== true && value.status !== false && value.status !== "unknown") {
65
+ throw new Error(`Invalid condition status for ${JSON.stringify(value.type)}`);
66
+ }
67
+ }
68
+ //# sourceMappingURL=conditions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditions.js","sourceRoot":"","sources":["../../src/controllers/conditions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { AnyControllerDefinition, ControllerDefinition, ControllerResource } from "./types.js";
2
+ export declare function defineController<TSpec, TStatus>(definition: ControllerDefinition<TSpec, TStatus>): ControllerDefinition<TSpec, TStatus>;
3
+ export declare function isControllerDefinition(value: unknown): value is AnyControllerDefinition;
4
+ export declare function assertValidControllerDefinition<TSpec, TStatus>(definition: ControllerDefinition<TSpec, TStatus>): void;
5
+ export declare function asAnyControllerDefinition<TSpec, TStatus>(definition: ControllerDefinition<TSpec, TStatus>): AnyControllerDefinition;
6
+ export declare function asTypedResource<TSpec, TStatus>(resource: ControllerResource): ControllerResource<TSpec, TStatus>;
@@ -0,0 +1,45 @@
1
+ const CONTROLLER_DEFINITION_BRAND = Symbol.for("pi-workflows.controller-definition");
2
+ const CONTROLLER_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
3
+ export function defineController(definition) {
4
+ assertValidControllerDefinition(definition);
5
+ if (isControllerDefinition(definition)) {
6
+ return definition;
7
+ }
8
+ Object.defineProperty(definition, CONTROLLER_DEFINITION_BRAND, {
9
+ value: true,
10
+ enumerable: false,
11
+ configurable: false,
12
+ writable: false,
13
+ });
14
+ return definition;
15
+ }
16
+ export function isControllerDefinition(value) {
17
+ return (value !== null &&
18
+ typeof value === "object" &&
19
+ value[CONTROLLER_DEFINITION_BRAND] === true);
20
+ }
21
+ export function assertValidControllerDefinition(definition) {
22
+ if (definition === null || typeof definition !== "object") {
23
+ throw new Error("Invalid controller definition: expected an object");
24
+ }
25
+ if (!CONTROLLER_NAME_PATTERN.test(definition.name)) {
26
+ throw new Error(`Invalid controller definition: name ${JSON.stringify(definition.name)} must match ${CONTROLLER_NAME_PATTERN.source}`);
27
+ }
28
+ if (typeof definition.initialStatus !== "function") {
29
+ throw new Error("Invalid controller definition: initialStatus must be a function");
30
+ }
31
+ if (typeof definition.reconcile !== "function") {
32
+ throw new Error("Invalid controller definition: reconcile must be a function");
33
+ }
34
+ if (definition.timeoutMs !== undefined &&
35
+ (!Number.isSafeInteger(definition.timeoutMs) || definition.timeoutMs <= 0)) {
36
+ throw new Error("Invalid controller definition: timeoutMs must be a positive safe integer");
37
+ }
38
+ }
39
+ export function asAnyControllerDefinition(definition) {
40
+ return definition;
41
+ }
42
+ export function asTypedResource(resource) {
43
+ return resource;
44
+ }
45
+ //# sourceMappingURL=definition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definition.js","sourceRoot":"","sources":["../../src/controllers/definition.ts"],"names":[],"mappings":"AAEA,MAAM,2BAA2B,GAAG,MAAM,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AACrF,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAEpD,MAAM,UAAU,gBAAgB,CAC9B,UAAgD;IAEhD,+BAA+B,CAAC,UAAU,CAAC,CAAC;IAC5C,IAAI,sBAAsB,CAAC,UAAU,CAAC,EAAE,CAAC;QACvC,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,2BAA2B,EAAE;QAC7D,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,KAAK;QACjB,YAAY,EAAE,KAAK;QACnB,QAAQ,EAAE,KAAK;KAChB,CAAC,CAAC;IACH,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAO,CACL,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACxB,KAAsC,CAAC,2BAA2B,CAAC,KAAK,IAAI,CAC9E,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,+BAA+B,CAC7C,UAAgD;IAEhD,IAAI,UAAU,KAAK,IAAI,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,uCAAuC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,eAAe,uBAAuB,CAAC,MAAM,EAAE,CACtH,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,UAAU,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACrF,CAAC;IACD,IAAI,OAAO,UAAU,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,IACE,UAAU,CAAC,SAAS,KAAK,SAAS;QAClC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,SAAS,IAAI,CAAC,CAAC,EAC1E,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;AACH,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,UAAgD;IAEhD,OAAO,UAA+D,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,QAA4B;IAE5B,OAAO,QAA8C,CAAC;AACxD,CAAC"}