@osolmaz/pi-workflows 0.3.0 → 0.5.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 +11 -7
- package/dist/builtins/catalog.d.ts +2 -0
- package/dist/builtins/catalog.js +32 -0
- package/dist/builtins/catalog.js.map +1 -0
- package/dist/builtins/monitor.workflow.d.ts +2 -2
- package/dist/builtins/monitor.workflow.js +25 -25
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +70 -9
- package/dist/controllers/sqlite.js +209 -35
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/workflow-engine-scheduler.d.ts +2 -2
- package/dist/controllers/workflow-engine-scheduler.js +3 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/extension/executor.d.ts +3 -0
- package/dist/extension/executor.js +11 -1
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.js +157 -108
- package/dist/extension/index.js.map +1 -1
- package/dist/host/runner.d.ts +1 -0
- package/dist/host/runner.js +68 -20
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +3 -0
- package/dist/render/graph-render.js.map +1 -1
- package/dist/workflows/catalog.d.ts +43 -0
- package/dist/workflows/catalog.js +79 -0
- package/dist/workflows/catalog.js.map +1 -0
- package/dist/workflows/definition.d.ts +2 -1
- package/dist/workflows/definition.js +9 -1
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +93 -33
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -3
- package/dist/workflows/index.js +2 -2
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +18 -16
- package/dist/workflows/loader.js +58 -23
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +42 -0
- package/dist/workflows/migrate-sources.js +133 -0
- package/dist/workflows/migrate-sources.js.map +1 -0
- package/dist/workflows/schema.d.ts +2 -1
- package/dist/workflows/schema.js +14 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +5 -2
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +44 -5
- package/docs/development.md +5 -3
- package/docs/plans/2026-08-12-coordinated-workflow-timeouts-plan.md +74 -0
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +97 -0
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +95 -0
- package/docs/run-bundles.md +22 -4
- package/docs/workflows.md +57 -14
- package/package.json +1 -1
- package/src/builtins/catalog.ts +32 -0
- package/src/builtins/monitor.workflow.ts +33 -26
- package/src/controllers/index.ts +1 -0
- package/src/controllers/sqlite.ts +353 -43
- package/src/controllers/workflow-engine-scheduler.ts +5 -2
- package/src/extension/executor.ts +12 -1
- package/src/extension/index.ts +181 -140
- package/src/host/runner.ts +78 -20
- package/src/render/graph-render.ts +3 -0
- package/src/workflows/catalog.ts +135 -0
- package/src/workflows/definition.ts +11 -0
- package/src/workflows/engine.ts +128 -53
- package/src/workflows/index.ts +7 -0
- package/src/workflows/loader.ts +70 -26
- package/src/workflows/migrate-sources.ts +174 -0
- package/src/workflows/schema.ts +16 -1
- package/src/workflows/store.ts +5 -2
- package/src/workflows/types.ts +43 -4
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Route workflow reports to their starting session
|
|
3
|
+
author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
|
|
4
|
+
date: 2026-08-13
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Route workflow reports to their starting session
|
|
8
|
+
|
|
9
|
+
Pi Workflows must not send one workflow's report into an unrelated conversation. A workflow started in one Pi session must report only to that session, even when another session or the standalone host executes part of the run.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Record the starting Pi session as the origin of each interactive workflow run.
|
|
14
|
+
- Keep execution events separate from user-facing notifications.
|
|
15
|
+
- Store each notification durably and address it to one Pi session.
|
|
16
|
+
- Deliver a notification only when its target session is open.
|
|
17
|
+
- Keep undelivered notifications until the target session opens again.
|
|
18
|
+
- Prevent unrelated sessions from claiming session-bound runs.
|
|
19
|
+
- Permit a detached host to execute a run without changing its report target.
|
|
20
|
+
- Give workflow authors a runtime-owned notification node instead of using an agent step to relay text.
|
|
21
|
+
- Migrate active path-based runs and existing queue rows without guessing their origin.
|
|
22
|
+
- Remove project-wide hidden-message broadcasts and the shared project watermark.
|
|
23
|
+
|
|
24
|
+
## Data model
|
|
25
|
+
|
|
26
|
+
Queue records gain an optional `origin_session_id`. Interactive runs set it from `ctx.sessionManager.getSessionId()`. Controller-created detached runs leave it empty. A session runner can claim a record only when the origin is empty or matches its current session. The standalone host can claim either form.
|
|
27
|
+
|
|
28
|
+
A `workflow_notifications` table is the durable outbox:
|
|
29
|
+
|
|
30
|
+
| Field | Meaning |
|
|
31
|
+
| --------------------------- | --------------------------------------------------- |
|
|
32
|
+
| `notification_id` | Stable unique delivery ID |
|
|
33
|
+
| `run_id` | Workflow run that created the notification |
|
|
34
|
+
| `node_id` | Node that created it |
|
|
35
|
+
| `attempt_id` | Latest node attempt that requested it |
|
|
36
|
+
| `notification_index` | Stable one-based occurrence of this node in the run |
|
|
37
|
+
| `target_session_id` | Exact Pi session that may receive it |
|
|
38
|
+
| `kind` | `progress` or `final` |
|
|
39
|
+
| `content` | Bounded plain-text report |
|
|
40
|
+
| `created_at` | Creation time |
|
|
41
|
+
| `delivery_claim_token` | Current delivery owner, or null |
|
|
42
|
+
| `delivery_claim_expires_at` | Delivery lease deadline, or null |
|
|
43
|
+
| `delivered_at` | Delivery time, or null while pending |
|
|
44
|
+
|
|
45
|
+
`(run_id, node_id, notification_index)` is unique. A crash retry reuses the same logical index, even though it gets a new attempt ID. A later loop iteration gets the next index. Delivery polling claims pending rows with a short lease before it writes to a session. This prevents two Pi processes that open the same session from sending the same notification concurrently. If a process stops, another process can reclaim the row after the lease expires. A stable notification ID is included in the custom session entry so the same session can detect a delivery completed before a crash.
|
|
46
|
+
|
|
47
|
+
The existing `run_events` table remains an execution audit feed. It does not inject conversation messages. The obsolete `session_watermarks` table is no longer used.
|
|
48
|
+
|
|
49
|
+
## Workflow API
|
|
50
|
+
|
|
51
|
+
Add a `notify(...)` node. Its message callback returns plain text. The engine calls a host-provided notification sink and persists the resulting receipt as the node output. The node does not ask the model to relay a message and does not depend on the runner's active conversation.
|
|
52
|
+
|
|
53
|
+
The built-in monitor uses `notify` for progress and final reports. Its check agent still decides whether a report is needed, but delivery always targets the origin session.
|
|
54
|
+
|
|
55
|
+
## Migration
|
|
56
|
+
|
|
57
|
+
For each active run with a queue row and no origin:
|
|
58
|
+
|
|
59
|
+
1. Read `session/binding.json` from its run bundle.
|
|
60
|
+
2. If the binding exists, set `origin_session_id` to its `piSessionId`.
|
|
61
|
+
3. If no binding exists, keep the run detached.
|
|
62
|
+
4. Never infer an origin from the working directory.
|
|
63
|
+
|
|
64
|
+
Existing lifecycle events are not converted into notifications. This prevents old completion events from appearing in unrelated sessions after the update.
|
|
65
|
+
|
|
66
|
+
## Non-goals
|
|
67
|
+
|
|
68
|
+
- Do not modify Pi core or Pi session files directly.
|
|
69
|
+
- Do not broadcast workflow reports to all sessions in a project.
|
|
70
|
+
- Do not keep compatibility aliases for the shared project watermark behavior.
|
|
71
|
+
- Do not make closed Pi sessions execute workflow steps.
|
|
72
|
+
|
|
73
|
+
## Acceptance criteria
|
|
74
|
+
|
|
75
|
+
- Two open Pi sessions in the same directory cannot receive each other's workflow reports.
|
|
76
|
+
- A report executed by the standalone host is delivered after the origin session opens.
|
|
77
|
+
- A report remains pending while its origin session is closed.
|
|
78
|
+
- A session-bound run is not claimed by a different interactive session.
|
|
79
|
+
- Duplicate polling and process restart do not duplicate a delivered notification.
|
|
80
|
+
- Monitor progress and final reports use the notification outbox.
|
|
81
|
+
- Existing active runs gain their recorded binding as origin when available.
|
|
82
|
+
- Pi starts with the released package in OnurPi.
|
|
83
|
+
|
|
84
|
+
## Verification
|
|
85
|
+
|
|
86
|
+
- `npm run check`
|
|
87
|
+
- `npm run test:e2e`
|
|
88
|
+
- `npm run slophammer`
|
|
89
|
+
- `git diff --check`
|
|
90
|
+
- `npx -y @simpledoc/simpledoc check`
|
|
91
|
+
- `cargo fmt --check --manifest-path tui/Cargo.toml`
|
|
92
|
+
- `cargo clippy --manifest-path tui/Cargo.toml --all-targets --all-features -- -D warnings`
|
|
93
|
+
- `cargo test --manifest-path tui/Cargo.toml`
|
|
94
|
+
- `pi-reviewer --base main`
|
|
95
|
+
- Start two isolated RPC Pi sessions in one directory and prove that only the matching session receives a targeted notification.
|
package/docs/run-bundles.md
CHANGED
|
@@ -123,7 +123,11 @@ Identity and pointers, kept in sync with the state on every snapshot:
|
|
|
123
123
|
"runId": "20260729T023912Z-autoimplement-3f2a9c1b",
|
|
124
124
|
"workflowName": "autoimplement",
|
|
125
125
|
"runTitle": "autoimplement: fix the flaky test",
|
|
126
|
-
"
|
|
126
|
+
"workflowSource": {
|
|
127
|
+
"kind": "file",
|
|
128
|
+
"path": "/repo/.pi/workflows/autoimplement.workflow.ts",
|
|
129
|
+
"hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
|
|
130
|
+
},
|
|
127
131
|
"startedAt": "2026-07-29T02:39:12.412Z",
|
|
128
132
|
"finishedAt": "2026-07-29T02:41:03.977Z",
|
|
129
133
|
"status": "completed",
|
|
@@ -138,6 +142,11 @@ Identity and pointers, kept in sync with the state on every snapshot:
|
|
|
138
142
|
}
|
|
139
143
|
```
|
|
140
144
|
|
|
145
|
+
`workflowSource` identifies the definition used by the run. User workflow
|
|
146
|
+
files use an absolute path and SHA-256 hash. Package-provided workflows use a
|
|
147
|
+
stable identity such as `{ "kind": "builtin", "id": "monitor", "revision": "1" }`.
|
|
148
|
+
A built-in identity does not contain an installation path.
|
|
149
|
+
|
|
141
150
|
`paths.artifacts` is declared from bundle creation so a live session-event
|
|
142
151
|
patch can safely reference a newly written artifact before the next workflow
|
|
143
152
|
state projection. The directory itself is created only when needed.
|
|
@@ -167,9 +176,10 @@ instead of failing. Resume is a named operation with strict rules:
|
|
|
167
176
|
`state.traceSeq` and the trace agree again before any new event.
|
|
168
177
|
3. Completed nodes replay from the projection. The in-flight node reruns with
|
|
169
178
|
a fresh attempt; a `run_resumed` trace event marks the boundary.
|
|
170
|
-
4. `state.
|
|
171
|
-
|
|
172
|
-
mismatch
|
|
179
|
+
4. `state.workflowSource` pins the workflow source from run start. File
|
|
180
|
+
sources require the same hash. Built-in sources require the same catalog
|
|
181
|
+
id and revision. Resume refuses a mismatch unless forced, and a forced
|
|
182
|
+
resume records the mismatch in the `run_resumed` payload.
|
|
173
183
|
|
|
174
184
|
Continuation runs (answering a checkpoint) are new bundles, not resumed ones.
|
|
175
185
|
They link back through `state.parentRunId`, carry the parent's outputs,
|
|
@@ -189,6 +199,11 @@ The full run projection (`WorkflowRunState` in
|
|
|
189
199
|
"traceSeq": 17,
|
|
190
200
|
"runId": "20260729T023912Z-autoimplement-3f2a9c1b",
|
|
191
201
|
"workflowName": "autoimplement",
|
|
202
|
+
"workflowSource": {
|
|
203
|
+
"kind": "file",
|
|
204
|
+
"path": "/repo/.pi/workflows/autoimplement.workflow.ts",
|
|
205
|
+
"hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
|
|
206
|
+
},
|
|
192
207
|
"startedAt": "…",
|
|
193
208
|
"updatedAt": "…",
|
|
194
209
|
"status": "running",
|
|
@@ -199,6 +214,9 @@ The full run projection (`WorkflowRunState` in
|
|
|
199
214
|
}
|
|
200
215
|
```
|
|
201
216
|
|
|
217
|
+
- `workflowSource` is the canonical source identity. Resuming a file requires
|
|
218
|
+
the same hash. Resuming a built-in requires the same catalog revision. A
|
|
219
|
+
mismatch refuses the resume instead of loading another definition.
|
|
202
220
|
- `status` is one of `running`, `waiting`, `completed`, `failed`, `timed_out`,
|
|
203
221
|
or `cancelled`. A controller host records an abandoned bundle as `failed`
|
|
204
222
|
with a final `run_interrupted` trace event. Before doing that, recovery checks
|
package/docs/workflows.md
CHANGED
|
@@ -16,7 +16,14 @@ Files are discovered by suffix (`.workflow.ts`, `.workflow.js`, `.workflow.mts`,
|
|
|
16
16
|
3. Workflows built into Pi Workflows
|
|
17
17
|
|
|
18
18
|
Pi Workflows includes a built-in `monitor` workflow. A project or global file
|
|
19
|
-
named `monitor.workflow.ts` replaces it.
|
|
19
|
+
named `monitor.workflow.ts` replaces it. The package registers each built-in in
|
|
20
|
+
a process-local catalog with a stable reference such as `builtin:monitor` and
|
|
21
|
+
an explicit revision. Built-ins are imported with the engine when a Pi process
|
|
22
|
+
starts. They are not read from the package directory when a run starts or
|
|
23
|
+
resumes. Updating the package on disk cannot mix a new built-in with that
|
|
24
|
+
process's old engine; reload or restart Pi to use the new built-in. A revision
|
|
25
|
+
mismatch refuses resume. Project and global workflow files still reload on
|
|
26
|
+
each run and use their path and SHA-256 hash as their source identity.
|
|
20
27
|
|
|
21
28
|
The workflow's command name is the file stem, so `.pi/workflows/triage.workflow.ts`
|
|
22
29
|
runs as `/workflow triage`. A direct path also works: `/workflow ./somewhere/x.workflow.ts`.
|
|
@@ -110,10 +117,9 @@ Conversation nodes execute in headless `pi --mode rpc` children that load a
|
|
|
110
117
|
small bridge extension; the model sees the same `workflow` tool contract as an
|
|
111
118
|
in-session run. The host is a foreground process: start it in a terminal and
|
|
112
119
|
stop it with Ctrl-C. A second host for the same project refuses to start, and
|
|
113
|
-
a host that dies has its orphaned children reaped by the next one. While the
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
updates.
|
|
120
|
+
a host that dies has its orphaned children reaped by the next one. While the host works, reports enter a durable outbox addressed to the Pi
|
|
121
|
+
session that started the run. They remain pending while that session is closed
|
|
122
|
+
and never enter another conversation in the same project.
|
|
117
123
|
|
|
118
124
|
## Node types
|
|
119
125
|
|
|
@@ -127,7 +133,10 @@ agent({
|
|
|
127
133
|
prompt: ({ outputs }) => `Review this: ${JSON.stringify(outputs.implement)}`,
|
|
128
134
|
expectedOutput: `{ "verdict": "clean" | "issues_found" }`,
|
|
129
135
|
validate: (output) => output, // optional; throw to reject the submission
|
|
130
|
-
timeoutMs:
|
|
136
|
+
timeoutMs: ({ input }) =>
|
|
137
|
+
(input as { timeoutMinutes?: number }).timeoutMinutes
|
|
138
|
+
? (input as { timeoutMinutes: number }).timeoutMinutes * 60_000
|
|
139
|
+
: 30 * 60_000, // optional number or context callback; default 15 minutes
|
|
131
140
|
statusDetail: "reviewing", // optional; shown in widget and viewer
|
|
132
141
|
});
|
|
133
142
|
```
|
|
@@ -137,7 +146,15 @@ calls the tool, the output passes through normalization (a JSON string is
|
|
|
137
146
|
parsed tolerantly) and then `validate`. If `validate` throws, the tool call
|
|
138
147
|
returns an error and the model can retry within the same step. If the agent
|
|
139
148
|
ends its turn without submitting, the extension nudges it, twice by default,
|
|
140
|
-
then fails the step.
|
|
149
|
+
then fails the step. If an agent node times out or the workflow is cancelled,
|
|
150
|
+
the extension also aborts its active Pi turn. The model cannot continue to use
|
|
151
|
+
tools after the engine has closed that attempt.
|
|
152
|
+
|
|
153
|
+
`timeoutMs` can be a finite positive number or a function of the normal node
|
|
154
|
+
context. A timeout function can use prepared outputs to select a deadline for
|
|
155
|
+
this run. It has 30 seconds to return a value. Computed timeout functions are
|
|
156
|
+
runtime code, so definition snapshots omit them; fixed numeric timeouts remain
|
|
157
|
+
in the snapshot.
|
|
141
158
|
|
|
142
159
|
### compute
|
|
143
160
|
|
|
@@ -147,6 +164,26 @@ Runs a TypeScript function inline. Use it for pure data shaping.
|
|
|
147
164
|
compute({ run: ({ outputs }) => ({ merged: { ...outputs } }) });
|
|
148
165
|
```
|
|
149
166
|
|
|
167
|
+
### notify
|
|
168
|
+
|
|
169
|
+
Queues a durable plain-text message for the Pi session that started the run.
|
|
170
|
+
The runner does not write the message into its own conversation. A standalone
|
|
171
|
+
host can execute this node, and the message still waits for the origin session.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
notify({
|
|
175
|
+
kind: "progress", // or "final"
|
|
176
|
+
message: ({ outputs }) => String(outputs.check),
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The runtime gives each logical execution of a notification node a stable index.
|
|
181
|
+
A retry after a crash reuses that index, so it cannot queue the same message
|
|
182
|
+
twice. Re-entering the node later in a loop gets the next index. A workflow
|
|
183
|
+
with a `notify` node must be an interactive queued run with an origin session.
|
|
184
|
+
Controller child workflows are detached and must report through their
|
|
185
|
+
controller resource instead.
|
|
186
|
+
|
|
150
187
|
### action
|
|
151
188
|
|
|
152
189
|
Performs a side effect. Two forms exist. The function form runs arbitrary
|
|
@@ -276,7 +313,8 @@ one looping workflow run. Its input is:
|
|
|
276
313
|
"everyMinutes": 30,
|
|
277
314
|
"reportWhen": "Checks fail or the state changes materially",
|
|
278
315
|
"stopWhen": "The pull request is merged or closed",
|
|
279
|
-
"maxChecks": 1000
|
|
316
|
+
"maxChecks": 1000,
|
|
317
|
+
"checkTimeoutMinutes": 60
|
|
280
318
|
}
|
|
281
319
|
```
|
|
282
320
|
|
|
@@ -286,8 +324,10 @@ separate agent node so its structured check result is validated before the user
|
|
|
286
324
|
sees the message. The next check can read the previous accepted observation.
|
|
287
325
|
|
|
288
326
|
Intervals must be whole minutes from 1 through 1,440. `maxChecks` defaults to
|
|
289
|
-
1,000 and cannot exceed 1,000.
|
|
290
|
-
|
|
327
|
+
1,000 and cannot exceed 1,000. `checkTimeoutMinutes` is optional and applies to
|
|
328
|
+
check and report agent nodes. It must be from 5 through 1,440 minutes. Its
|
|
329
|
+
default is the larger of 60 minutes and `everyMinutes`. The workflow also has
|
|
330
|
+
a finite step limit and bounded observation and report sizes.
|
|
291
331
|
|
|
292
332
|
The interval uses the existing shell action to launch the current Node
|
|
293
333
|
executable with a timer. This works on every platform supported by Pi. The node
|
|
@@ -344,8 +384,9 @@ After the final run state has been persisted, the Pi extension sends the
|
|
|
344
384
|
presentation instructions and bounded final result to the model as a hidden
|
|
345
385
|
follow-up message. The next visible message is a normal assistant response.
|
|
346
386
|
Returning `undefined`, returning an empty string, or omitting
|
|
347
|
-
`presentationPrompt` produces no follow-up.
|
|
348
|
-
|
|
387
|
+
`presentationPrompt` produces no follow-up. Failed, timed-out, and cancelled
|
|
388
|
+
runs are never presented; the extension reports their persisted status and
|
|
389
|
+
error directly. Async prompt builders have 30 seconds to finish and receive an
|
|
349
390
|
`AbortSignal` that fires on timeout, session shutdown, or when a new workflow
|
|
350
391
|
or normal user turn starts; stale presentations are discarded. Once a presentation message has
|
|
351
392
|
been queued, another workflow cannot start until that assistant response
|
|
@@ -364,8 +405,10 @@ Runs execute one node at a time. Every transition is persisted to the run
|
|
|
364
405
|
bundle before the engine moves on, which is what makes the live viewer
|
|
365
406
|
possible. Defaults worth knowing:
|
|
366
407
|
|
|
367
|
-
- Node timeout is 15 minutes unless the node sets `timeoutMs
|
|
368
|
-
node has outcome `timed_out` and can
|
|
408
|
+
- Node timeout is 15 minutes unless the node sets `timeoutMs` to a positive
|
|
409
|
+
number or context callback. A timed-out node has outcome `timed_out` and can
|
|
410
|
+
be routed with `$result.outcome`. A timed-out agent node also aborts its Pi
|
|
411
|
+
turn, and late output for that attempt is rejected.
|
|
369
412
|
- `maxSteps` (workflow-level, default 100) bounds loops built from cycles in
|
|
370
413
|
the graph.
|
|
371
414
|
- `/workflow pause` requests a pause: the current step finishes normally,
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BuiltinWorkflowCatalog } from "../workflows/catalog.js";
|
|
2
|
+
import monitorWorkflow from "./monitor.workflow.js";
|
|
3
|
+
|
|
4
|
+
export const builtinWorkflowCatalog = new BuiltinWorkflowCatalog([
|
|
5
|
+
{
|
|
6
|
+
id: "monitor",
|
|
7
|
+
revision: "2",
|
|
8
|
+
definition: monitorWorkflow,
|
|
9
|
+
legacySources: [
|
|
10
|
+
{
|
|
11
|
+
workflowHash: "7a22158da94d18ec1c9fe42e70d72017a4e0620d5e5142ae839d0cd6eea55c06",
|
|
12
|
+
revision: "2",
|
|
13
|
+
pathSuffixes: [
|
|
14
|
+
"/src/builtins/monitor.workflow.ts",
|
|
15
|
+
"/dist/builtins/monitor.workflow.js",
|
|
16
|
+
"/src/workflows/monitor.workflow.ts",
|
|
17
|
+
"/dist/workflows/monitor.workflow.js",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
workflowHash: "352fc09c88922c7375281b52f049c1039d05441ce37f2082f5ff07fea66d5318",
|
|
22
|
+
revision: "2",
|
|
23
|
+
pathSuffixes: ["/dist/builtins/monitor.workflow.js"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
workflowHash: "dc601e2323a8213f5d52fa555e804ae8ed0846f809b3a1e9e5073a3c9c3a114e",
|
|
27
|
+
revision: "2",
|
|
28
|
+
pathSuffixes: ["/dist/builtins/monitor.workflow.js"],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { agent, compute, defineWorkflow, shell } from "../workflows/
|
|
1
|
+
import { agent, compute, defineWorkflow, notify, shell } from "../workflows/definition.js";
|
|
2
2
|
import type { WorkflowNodeContext } from "../workflows/types.js";
|
|
3
3
|
|
|
4
4
|
const MIN_INTERVAL_MINUTES = 1;
|
|
5
5
|
const MAX_INTERVAL_MINUTES = 24 * 60;
|
|
6
|
+
const MIN_CHECK_TIMEOUT_MINUTES = 5;
|
|
7
|
+
const MAX_CHECK_TIMEOUT_MINUTES = 24 * 60;
|
|
8
|
+
const DEFAULT_MIN_CHECK_TIMEOUT_MINUTES = 60;
|
|
6
9
|
const DEFAULT_MAX_CHECKS = 1_000;
|
|
7
10
|
const MAX_CHECKS = 1_000;
|
|
8
11
|
const MAX_OBSERVATION_CHARS = 8_000;
|
|
@@ -17,6 +20,7 @@ type MonitorInput = {
|
|
|
17
20
|
reportWhen?: string;
|
|
18
21
|
stopWhen?: string;
|
|
19
22
|
maxChecks?: number;
|
|
23
|
+
checkTimeoutMinutes?: number;
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
type MonitorConfig = {
|
|
@@ -25,6 +29,7 @@ type MonitorConfig = {
|
|
|
25
29
|
reportWhen: string;
|
|
26
30
|
stopWhen: string;
|
|
27
31
|
maxChecks: number;
|
|
32
|
+
checkTimeoutMinutes: number;
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
type MonitorRoute = "continue_quiet" | "continue_report" | "stop_quiet" | "stop_report";
|
|
@@ -81,6 +86,17 @@ function prepareInput(input: unknown): MonitorConfig {
|
|
|
81
86
|
if (!Number.isInteger(maxChecks) || maxChecks <= 0 || maxChecks > MAX_CHECKS) {
|
|
82
87
|
throw new Error(`maxChecks must be an integer from 1 through ${MAX_CHECKS}`);
|
|
83
88
|
}
|
|
89
|
+
const checkTimeoutMinutes =
|
|
90
|
+
value.checkTimeoutMinutes ?? Math.max(DEFAULT_MIN_CHECK_TIMEOUT_MINUTES, value.everyMinutes);
|
|
91
|
+
if (
|
|
92
|
+
!Number.isInteger(checkTimeoutMinutes) ||
|
|
93
|
+
checkTimeoutMinutes < MIN_CHECK_TIMEOUT_MINUTES ||
|
|
94
|
+
checkTimeoutMinutes > MAX_CHECK_TIMEOUT_MINUTES
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`checkTimeoutMinutes must be an integer from ${MIN_CHECK_TIMEOUT_MINUTES} through ${MAX_CHECK_TIMEOUT_MINUTES}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
84
100
|
return {
|
|
85
101
|
task,
|
|
86
102
|
everyMinutes: value.everyMinutes,
|
|
@@ -93,6 +109,7 @@ function prepareInput(input: unknown): MonitorConfig {
|
|
|
93
109
|
? "The user cancels the monitor or it reaches its maximum check count."
|
|
94
110
|
: requireBoundedString(value.stopWhen, "stopWhen", 4_000),
|
|
95
111
|
maxChecks,
|
|
112
|
+
checkTimeoutMinutes,
|
|
96
113
|
};
|
|
97
114
|
}
|
|
98
115
|
|
|
@@ -100,6 +117,10 @@ function configFrom(outputs: Record<string, unknown>): MonitorConfig {
|
|
|
100
117
|
return outputs.prepare as MonitorConfig;
|
|
101
118
|
}
|
|
102
119
|
|
|
120
|
+
function agentTimeoutMs({ outputs }: WorkflowNodeContext): number {
|
|
121
|
+
return configFrom(outputs).checkTimeoutMinutes * 60_000;
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
function completedChecks(context: WorkflowNodeContext): number {
|
|
104
125
|
return context.state.steps.filter((step) => step.nodeId === "check" && step.outcome === "ok")
|
|
105
126
|
.length;
|
|
@@ -129,22 +150,9 @@ function validateCheck(output: unknown): MonitorCheck {
|
|
|
129
150
|
};
|
|
130
151
|
}
|
|
131
152
|
|
|
132
|
-
function
|
|
133
|
-
const value = requireRecord(output, "report acknowledgement");
|
|
134
|
-
if (value.reported !== true) {
|
|
135
|
-
throw new Error("report acknowledgement must set reported to true");
|
|
136
|
-
}
|
|
137
|
-
return { reported: true };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function reportPrompt(outputs: Record<string, unknown>): string {
|
|
153
|
+
function reportMessage(outputs: Record<string, unknown>): string {
|
|
141
154
|
const check = outputs.check as MonitorCheck;
|
|
142
|
-
return
|
|
143
|
-
"Write one concise normal assistant message to the user with this monitoring update:",
|
|
144
|
-
check.report ?? check.observation,
|
|
145
|
-
"Do not add unrelated detail.",
|
|
146
|
-
"After writing the update, submit the acknowledgement required by the workflow step contract.",
|
|
147
|
-
].join("\n\n");
|
|
155
|
+
return check.report ?? check.observation;
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
export default defineWorkflow({
|
|
@@ -190,6 +198,7 @@ export default defineWorkflow({
|
|
|
190
198
|
}),
|
|
191
199
|
check: agent({
|
|
192
200
|
statusDetail: "checking monitored target",
|
|
201
|
+
timeoutMs: agentTimeoutMs,
|
|
193
202
|
prompt: (context) => {
|
|
194
203
|
const config = configFrom(context.outputs);
|
|
195
204
|
const previous = context.outputs.check as MonitorCheck | undefined;
|
|
@@ -210,17 +219,15 @@ export default defineWorkflow({
|
|
|
210
219
|
'{ "route": "continue_quiet" | "continue_report" | "stop_quiet" | "stop_report", "observation": "current factual state", "report": "required for report routes", "reason": "short reason" }',
|
|
211
220
|
validate: (output) => validateCheck(output),
|
|
212
221
|
}),
|
|
213
|
-
report_continue:
|
|
214
|
-
statusDetail: "
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
validate: (output) => validateReportAck(output),
|
|
222
|
+
report_continue: notify({
|
|
223
|
+
statusDetail: "queueing monitor update",
|
|
224
|
+
message: ({ outputs }) => reportMessage(outputs),
|
|
225
|
+
kind: "progress",
|
|
218
226
|
}),
|
|
219
|
-
report_stop:
|
|
220
|
-
statusDetail: "
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
validate: (output) => validateReportAck(output),
|
|
227
|
+
report_stop: notify({
|
|
228
|
+
statusDetail: "queueing final monitor update",
|
|
229
|
+
message: ({ outputs }) => reportMessage(outputs),
|
|
230
|
+
kind: "final",
|
|
224
231
|
}),
|
|
225
232
|
sleep: shell({
|
|
226
233
|
statusDetail: "waiting for next monitor check",
|
package/src/controllers/index.ts
CHANGED