pi-workflow-engine 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/pi-workflow-engine/index.ts +199 -78
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +18 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +34 -7
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +9 -3
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +51 -8
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +14 -0
- package/.pi/extensions/pi-workflow-engine/src/codemode-slots.ts +73 -0
- package/.pi/extensions/pi-workflow-engine/src/config-file.ts +52 -0
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +127 -58
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +114 -31
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +74 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +5 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +22 -4
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +9 -47
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +32 -4
- package/.pi/extensions/pi-workflow-engine/src/prompt-sections.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +4 -4
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/types.ts +44 -9
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +9 -3
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +69 -18
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +17 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +8 -3
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +16 -8
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +17 -6
- package/.pi/extensions/pi-workflow-engine/src/workflow-catalog.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +6 -2
- package/.pi/extensions/pi-workflow-engine/src/workflow-input.ts +13 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +21 -4
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +69 -13
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +23 -4
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +27 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-seal.ts +83 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +30 -4
- package/.pi/extensions/pi-workflow-engine/src/workflow-script-file.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-settings.ts +147 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-tool-request.ts +61 -0
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +12 -4
- package/README.md +11 -3
- package/USAGE.md +121 -22
- package/package.json +13 -4
- package/skills/workflow-authoring/SKILL.md +160 -0
package/USAGE.md
CHANGED
|
@@ -40,10 +40,11 @@ Useful flags:
|
|
|
40
40
|
/workflow code-review --budget=50000 # output-token ceiling for subagents
|
|
41
41
|
/workflow code-review --resume <run-id> # replay matching completed agent calls
|
|
42
42
|
/workflow code-review --resume <run-id> --resume-edited # reuse unchanged calls after a workflow edit
|
|
43
|
-
/workflow code-review --refresh #
|
|
43
|
+
/workflow code-review --refresh # reload .ts drop-ins (saved .js scripts are always reread)
|
|
44
44
|
/workflow:runs # browse recent durable project runs
|
|
45
45
|
/workflow:inspector <run-id> # inspect one retained run directly
|
|
46
46
|
/workflow:models # inspect small/medium/big model routes
|
|
47
|
+
/workflow:size # show or set the workflow size guideline
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
## Workflow model profiles
|
|
@@ -83,7 +84,11 @@ secrets stay in pi's provider configuration. The engine never ranks models by
|
|
|
83
84
|
price, model-name substrings, or assumed capability.
|
|
84
85
|
|
|
85
86
|
Routing is resolved per field: an agent's explicit `model` or `thinkingLevel`
|
|
86
|
-
wins, then its requested profile, then
|
|
87
|
+
wins, then its requested profile, then the profile of its declared phase, then
|
|
88
|
+
host inheritance. A phase profile comes from `meta.phases` (`{ title, profile }`)
|
|
89
|
+
and applies only to an agent that passes that title as its `phase` option and
|
|
90
|
+
sets neither `model` nor `profile`; the global `phase()` call never selects one,
|
|
91
|
+
because concurrent branches share it. A configured profile
|
|
87
92
|
without `thinkingLevel` inherits the host effort. An entirely unconfigured tier
|
|
88
93
|
uses the host model and the built-in bounded effort for that tier (`low`,
|
|
89
94
|
`medium`, or `high` respectively); `/workflow:models` labels that route as a host
|
|
@@ -115,12 +120,14 @@ because only those runs can reach the host's MCP tools.
|
|
|
115
120
|
|
|
116
121
|
The workflow bounds decomposition to four lanes and verification to twelve
|
|
117
122
|
claims, while the engine's concurrency, total-agent, timeout, cancellation, and
|
|
118
|
-
token-budget limits still apply.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
token-budget limits still apply. When gatherers find more than twelve distinct
|
|
124
|
+
claims, the most important are verified, and both the run log and the report's
|
|
125
|
+
limitations say how many lower-priority claims the cap left unverified. Gatherers retain direct page
|
|
126
|
+
titles and URLs; search-results URLs are rejected, sources are canonicalized and
|
|
127
|
+
deduplicated, and separate verifier agents classify claims as supported,
|
|
128
|
+
conflicting, uncertain, inferred, or rejected before synthesis. External
|
|
129
|
+
research calls use `resume: "off"` because live web evidence, time, and service
|
|
130
|
+
state are outside the journal's deterministic replay surface.
|
|
124
131
|
|
|
125
132
|
## Use Dynamax for custom workflows
|
|
126
133
|
|
|
@@ -141,6 +148,7 @@ dynamax use parallel agents to review this design and report the risks
|
|
|
141
148
|
Sticky mode and inspector shortcut:
|
|
142
149
|
|
|
143
150
|
```text
|
|
151
|
+
/workflow:dynamax # toggle sticky mode
|
|
144
152
|
/workflow:dynamax on
|
|
145
153
|
/workflow:dynamax off
|
|
146
154
|
/workflow:dynamax status
|
|
@@ -148,7 +156,7 @@ Sticky mode and inspector shortcut:
|
|
|
148
156
|
|
|
149
157
|
The workflow inspector and code-review results viewer are also registered as first-class shortcuts shown by `/hotkeys`. The defaults are `ctrl+shift+m` for the inspector and `ctrl+shift+r` for the latest review results. `ctrl+o` is intentionally not used because pi already uses it for tool-output expansion and tree filtering.
|
|
150
158
|
|
|
151
|
-
When only the literal `dynamax` token is used, the opt-in is one-shot: the next agent run receives a `<dynamax>` system prompt section that permits the workflow tool (pi appends it as a section patch, so the cached prompt head is kept), stays visibly active for that run, then clears after the run ends. When `/workflow:dynamax on` is used, the opt-in is sticky for the current pi session until `/workflow:dynamax off
|
|
159
|
+
When only the literal `dynamax` token is used, the opt-in is one-shot: the next agent run receives a `<dynamax>` system prompt section that permits the workflow tool (pi appends it as a section patch, so the cached prompt head is kept), stays visibly active for that run, then clears after the run ends. When `/workflow:dynamax on` (or `/workflow:dynamax` while sticky mode is off) is used, the opt-in is sticky for the current pi session until `/workflow:dynamax off` (or `/workflow:dynamax` again). Sticky mode is a standing default rather than a one-request permission: its `<dynamax>` section tells the host agent to orchestrate every substantive task with the workflow tool, scouting inline first and then running one single-phase workflow per phase (understand, design, implement, review), reading each result before choosing the next, and to work without a workflow only for conversational turns and trivial or already-verified edits. It applies to every run in the session, including runs that a custom message starts, such as a background workflow waking the agent; a one-shot is consumed only by the user prompt that carried it. The footer status states the mode compactly: `◆ dynamax` while sticky, `◆ dynamax · next prompt` while a one-shot waits for its prompt, and `◆ dynamax · this turn` while that run is active; off mode clears it. A running workflow names itself in its own status next to it.
|
|
152
160
|
|
|
153
161
|
### Prompt editor cue
|
|
154
162
|
|
|
@@ -191,6 +199,24 @@ Set either shortcut to `null` to disable it while keeping `/workflow:inspector`
|
|
|
191
199
|
|
|
192
200
|
With Dynamax enabled, the host agent usually calls the `workflow` tool with `script`: a one-off inline workflow tailored to your prompt. It can still call a saved workflow by `name` when one already fits.
|
|
193
201
|
|
|
202
|
+
Every run that can call the `workflow` tool also carries a `<workflows>` system prompt section: the catalog of registered workflows, one line each with its name, `description`, and `whenToUse`, sorted by name. It is built once per pi session from workflow discovery and reused, so its text is identical on every turn and keeps the provider's prompt cache; start a new session or `/reload` to list workflows added since.
|
|
203
|
+
|
|
204
|
+
### Workflow size guideline
|
|
205
|
+
|
|
206
|
+
The catalog ends with one line telling the host agent how large to make each workflow it authors:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
/workflow:size # show the current guideline and where it comes from
|
|
210
|
+
/workflow:size small # keep each workflow under 5 agents
|
|
211
|
+
/workflow:size medium # under 10 agents (the default)
|
|
212
|
+
/workflow:size large --project # under 25 agents, for this project only
|
|
213
|
+
/workflow:size off # leave the line out
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The line reads, for example, `Workflow size guideline: medium — keep each workflow under 10 agents unless the user asks for more; the user changes this with /workflow:size.` The setting is stored like the model profiles: `~/.pi/agent/workflow-settings.json` for the user, overridden by `<project>/.pi/workflow-settings.json`; setting a size writes the user file unless `--project` is given. A minimal file is `{ "size": "small" }`. Changing it rebuilds the current session's catalog once.
|
|
217
|
+
|
|
218
|
+
The guideline only shapes what the host agent writes. The `maxAgents` run limit (default `64`, see [Runtime knobs](#runtime-knobs)) stays the separate hard cap: once a run has started that many live agents, further `agent()` calls fail with `WorkflowAgentLimitError`, whatever the guideline says.
|
|
219
|
+
|
|
194
220
|
### Adaptive multi-pass authoring
|
|
195
221
|
|
|
196
222
|
A single fan-out is still the default when it can answer the question. When the first pass may reveal gaps, conflicts, weak claims, or missing evidence, an inline workflow can inspect those intermediate results and commission only the follow-up work the LLM says is needed:
|
|
@@ -249,6 +275,8 @@ During a run, a widget above the editor shows the workflow's progress bar, finis
|
|
|
249
275
|
|
|
250
276
|
The inspector has Overview, Agents, Findings, and Logs sections, plus Result for a retained run opened from `/workflow:runs`. Switch sections with `tab`/`shift+tab`, `←`/`→`, or `1`–`5`; move with `↑`/`↓` or `j`/`k`, jump with `g`/`G`, expand an agent or finding with `enter`, and close with `q` or `esc`. Expanded agents show the full error, last tool, model, and token usage.
|
|
251
277
|
|
|
278
|
+
While a run is live, `s` on the Agents tab skips the selected running agent: press it twice on the same agent (like stopping a run in the runs browser); the key is listed in the footer only while the selection can take it. Skipping aborts only that agent's session; the run and its sibling agents carry on. The skipped `agent()` call rejects with a recoverable `WorkflowAgentSkippedError` (code `WORKFLOW_AGENT_SKIPPED`), so `parallel()` and `pipeline()` turn it into a `null` slot while a direct `await` sees the error. A skipped agent is never restarted by `agentRetries` and never written to the replay journal, so a resumed run asks it again; the tokens it already spent still count toward usage and the budget. The widget, inspector and run record show the agent as `skipped`. Skipping works for a background run too when you open its live inspector with `/workflow:inspector` or the shortcut; a retained run opened from `/workflow:runs` is read-only.
|
|
279
|
+
|
|
252
280
|
### Durable run records
|
|
253
281
|
|
|
254
282
|
Every workflow run writes a versioned project-local record to `.pi/.workflow-runs/<run-id>.run.json`; its agent replay journal remains `.pi/.workflow-runs/<run-id>.jsonl`. The same run ID appears in execution metadata, progress snapshots, result details, and both filenames. Records distinguish `queued`, `running`, `completed`, `failed`, `stopped`, and `paused` lifecycle states and retain resolved scalar options, timestamps, compact progress, summary usage, and the final result or error.
|
|
@@ -257,11 +285,17 @@ Record replacement is atomic, corrupt or unsupported-version files are ignored i
|
|
|
257
285
|
|
|
258
286
|
Persistence is intentionally bounded and private by default. Raw workflow args, prompts, subagent transcripts, progress logs, lane details, agent errors, environment data, credentials, and tool-output-shaped fields are not stored. Final results are limited to bounded plain JSON; credential- and transcript-shaped fields are redacted, and unsupported or cyclic results are marked unavailable instead of executing accessors or breaking the workflow. Loading history only parses these data files and never imports or executes workflow code.
|
|
259
287
|
|
|
288
|
+
An inline run (a tool `script` or `scriptPath`) also saves its script to `.pi/.workflow-runs/<run-id>.script.js`, a separate file the record only references, because the record never holds prompts. The script holds nothing new: pi's session transcript already contains it as the tool call's arguments. A pruned record takes its script with it. Whenever the engine creates `.pi/.workflow-runs/`, it adds a `.gitignore` containing `*` so run files stay out of the project's git history; an existing `.gitignore` there is never replaced.
|
|
289
|
+
|
|
290
|
+
The result handed to the host agent names the run's files after the run ID: `Script:` for an inline run, and `Journal:` with a reminder to read the journal before diagnosing an empty or unexpected result. A failed inline run lists the same lines after its error, a background start names the script too, and a background delivery names the record, script, and journal (see [Background workflows](#background-workflows)). To iterate, edit the script and rerun it with `scriptPath` (see [Author an inline workflow](#author-an-inline-workflow)).
|
|
291
|
+
|
|
260
292
|
### Background workflows
|
|
261
293
|
|
|
262
294
|
The host-facing `workflow` tool accepts `background: true`. It returns as soon as the run ID and origin metadata are durably recorded, while the normal workflow engine continues in-process with the same concurrency, budget, worktree, progress, usage, and resume rules. Synchronous execution remains the default, and the `/workflow` slash command remains synchronous.
|
|
263
295
|
|
|
264
|
-
Background runs are detached from the initiating tool-call signal, so the host agent can finish that turn and continue the conversation. While one runs, it shows a single line above the editor (`◆ background · code-review 56b57550 · Find 6/10 · 12s`) instead of the full live widget. When the run completes or fails, the extension waits for the originating session to become idle and inserts one concise `workflow-result` message containing the run ID, terminal state, and summary. Delivery state is stored with the run record; on reload, the extension checks both that state and the session message history before retrying, which suppresses duplicates even if persistence failed immediately after message insertion.
|
|
296
|
+
Background runs are detached from the initiating tool-call signal, so the host agent can finish that turn and continue the conversation. While one runs, it shows a single line above the editor (`◆ background · code-review 56b57550 · Find 6/10 · 12s`) instead of the full live widget. When the run completes or fails, the extension waits for the originating session to become idle and inserts one concise `workflow-result` message containing the run ID, terminal state, and summary, followed by the run's files: `Record:`, `Script:` for an inline run, and `Journal:` with the reminder to read it before diagnosing an empty or unexpected result, so a later turn knows where the retained result lives. Delivery state is stored with the run record; on reload, the extension checks both that state and the session message history before retrying, which suppresses duplicates even if persistence failed immediately after message insertion.
|
|
297
|
+
|
|
298
|
+
If Dynamax was active when the host agent started the run (sticky mode, or the turn a `dynamax` one-shot covered), the delivery also starts a new agent turn, so the agent wakes to read the result and choose its next step without waiting for you; with sticky mode on, that turn carries the sticky `<dynamax>` section. pi gives the model only a message's text, so a waking delivery carries the full result in place of the summary, rendered and bounded exactly as a synchronous `workflow` call returns it (a review's findings, every other result field, and the usage line), or the full retained message of a failed, stopped, or paused run, before the same file lines. The run record keeps this as `background.origin.wakeOnComplete`, so a delivery after a restart, including the notice for a run paused by shutdown, still wakes the agent, and a resume, restart, or provider-limit auto-resume of that run wakes it too. A run you stopped does not wake the agent, nor does a provider-limit pause whose automatic resume is scheduled: that resume's own delivery wakes it. Other background runs keep the silent delivery above. There is no separate flag: Dynamax being active is the signal that the agent is orchestrating.
|
|
265
299
|
|
|
266
300
|
Provider usage-window continuation is a separate, explicit background option:
|
|
267
301
|
|
|
@@ -275,7 +309,7 @@ Provider usage-window continuation is a separate, explicit background option:
|
|
|
275
309
|
}
|
|
276
310
|
```
|
|
277
311
|
|
|
278
|
-
It is off by default and schedules only registered file workflows whose invocation had no arguments;
|
|
312
|
+
It is off by default and schedules only registered file workflows and inline runs whose script was persisted, and only when the invocation had no arguments; argument-bearing runs still pause safely but require an explicit manual decision because their arguments are not persisted. A recognized provider usage or quota failure aborts the current fan-out before queued agents can open new sessions, preserves the replay journal, and records a sanitized provider message, provider identity, reset hint, total attempt number, and next eligible time. Transport failures and ordinary provider errors keep the existing per-agent `agentRetries` behavior and do not create a provider-limit pause.
|
|
279
313
|
|
|
280
314
|
Reset parsing accepts provider duration hints such as `retry-after: 60` and `x-ratelimit-reset-tokens: 6m0s`, plus future RFC3339 timestamps. Missing, past, or malformed hints use a 60-second fallback; delays are clamped to at least 5 seconds and at most `usageLimitMaxDelayMs`. The default maximum is three total run attempts, including the initial run. When the eligible time arrives, the extension starts a new background run with `resumeFromRunId`; after a process restart, it re-arms only the remaining delay. `/workflow:runs resume` and `/workflow:runs stop` cancel any pending timer, and the attempt cap prevents an infinite resume loop.
|
|
281
315
|
|
|
@@ -285,7 +319,7 @@ A graceful pi session shutdown aborts active background work and records it as `
|
|
|
285
319
|
|
|
286
320
|
### Recent runs and lifecycle actions
|
|
287
321
|
|
|
288
|
-
`/workflow:runs` opens a runs browser in the TUI: one aligned row per recent project run (state glyph and word, workflow, duration, age, cost when known, short run ID) above a details pane for the selected run with its wall-clock times, phases, agent and token counts, resume lineage, pause details, outcome, and why a relaunch is unavailable when it is. Keys act on the selected run: `enter` or `i` inspect, `r` resume, `s` stop (press twice), `R` restart; only the actions currently valid for that run are listed in the footer, and the others explain why they are unavailable. The browser refreshes while a run is active and reopens on the same run after an action. RPC sessions keep pi's native two-step selection (run, then action). The background activity line is present only while this session owns active background work.
|
|
322
|
+
`/workflow:runs` opens a runs browser in the TUI: one aligned row per recent project run (state glyph and word, workflow, tagged `inline` when the run was an inline script, duration, age, cost when known, short run ID) above a details pane for the selected run with its wall-clock times, phases, agent and token counts, resume lineage, pause details, outcome, and why a relaunch is unavailable when it is. Keys act on the selected run: `enter` or `i` inspect, `r` resume, `s` stop (press twice), `R` restart; only the actions currently valid for that run are listed in the footer, and the others explain why they are unavailable. The browser refreshes while a run is active and reopens on the same run after an action. RPC sessions keep pi's native two-step selection (run, then action). The background activity line is present only while this session owns active background work.
|
|
289
323
|
|
|
290
324
|
The same operations are available without the selection UI:
|
|
291
325
|
|
|
@@ -297,7 +331,7 @@ The same operations are available without the selection UI:
|
|
|
297
331
|
/workflow:inspector <run-id>
|
|
298
332
|
```
|
|
299
333
|
|
|
300
|
-
Inspect is always safe. Stop is available for a background run active in the current session and for a retained paused run with a pending resume timer. Resume is limited to paused
|
|
334
|
+
Inspect is always safe. Stop is available for a background run active in the current session and for a retained paused run with a pending resume timer. Resume is limited to paused runs whose source fingerprint is unchanged and whose invocation had no redacted arguments; it starts a new background run ID with `resumeFromRunId` journal replay. A paused run already resumed by a queued, running, paused, or completed run is listed as `resumed as <run-id>` and offers no second resume; once every resume of it has failed or been stopped, manual resume is offered again, while automatic provider-limit resume never fires twice for the same paused run. Restart is limited to completed, failed, or stopped runs that had no arguments and also creates a new background run ID. A registered workflow relaunches by name; an inline run relaunches from its persisted `<run-id>.script.js`, so it is relaunchable only while that script is retained, and fails with a clear message if the file was deleted. Because `.pi/.workflow-runs/` sits in the project, a cloned repository could ship a record and script of its own (the `*` ignore file does not untrack committed files), so the engine seals each inline run: its record holds an HMAC of the run ID and script fingerprint under a key that pi creates once per install at `~/.pi/agent/workflow-run-seal.key` (inside `PI_CODING_AGENT_DIR` when set). Before reading the script, a resume or restart checks the seal against this install's key, then requires the script's fingerprint to match the record; a record from elsewhere, or a script edited in place since the run, is refused with a hint to rerun it with the `workflow` tool's `scriptPath`. A run of a saved `.pi/workflows` or user script is sealed the same way, without a copy: it relaunches by name only while the script still matches the content that ran, since a repository could also ship a record naming a script of its own; otherwise run it again by name. A record without a seal, including every inline record written before sealing existed, offers neither action. Inline runs are listed as `<name> (inline)` and tagged `inline` in the browser, so such a record cannot pass for a registered workflow's run. Argument-bearing invocations are intentionally not relaunched because their arguments were not persisted. In `print` and `json` modes, list and inspect return formatted text without opening TUI components, while resume/restart report that background execution requires TUI or RPC mode.
|
|
301
335
|
|
|
302
336
|
Code-review findings are rendered as a formatted result message by default. pi no longer asks whether to open the findings viewer. Use `--result-viewer` or `--review-viewer` when you want to inspect findings interactively. The viewer lists findings most severe first with the selected finding's full detail beside the list (below it in narrow terminals); `space` or `enter` selects a finding, `a` toggles all, `f` generates patch previews and `c` posts PR comments for the selection, and `pgup`/`pgdn` scroll a long detail. The viewer is sized to its content and shows scroll hints only when something is clipped. `/workflow:results` or `ctrl+shift+r` reopens the most recent validated code-review report in the current pi session without rerunning the workflow; selections reset when it reopens.
|
|
303
337
|
|
|
@@ -320,6 +354,8 @@ With edited-workflow resume enabled, unchanged calls are reused while changed or
|
|
|
320
354
|
|
|
321
355
|
This option can return a result produced before an orchestration edit when that call's explicit identity did not change. Use it only when the unchanged call is still semantically valid in the edited workflow, and prefer stable `cacheKey` values for repeated logical calls. Omit `--resume-edited` or `resumeEditedWorkflow` for the fail-closed default.
|
|
322
356
|
|
|
357
|
+
A tool call with `scriptPath` and `resumeFromRunId` implies `resumeEditedWorkflow: true`, because rerunning an edited script file is what `scriptPath` is for. Only the whole-script fingerprint check is relaxed; each call's own replay identity still decides whether its cached result is reused.
|
|
358
|
+
|
|
323
359
|
Replay is explicit for agents that share the workflow directory and automatic for isolated patch-producing agents:
|
|
324
360
|
|
|
325
361
|
| Agent call | Default during resume | Override |
|
|
@@ -333,9 +369,9 @@ Journals use replay contract v2. A hit requires the same prompt/options and exec
|
|
|
333
369
|
|
|
334
370
|
Cached values are revalidated before use: text must still be text, structured output must satisfy the current typebox schema, and an isolated patch must have consistent metadata and pass `git apply --check --binary` against its fresh baseline worktree. A malformed or stale value runs live.
|
|
335
371
|
|
|
336
|
-
Invalidations appear in progress output with a concise reason. Legacy v1 journals and early v2 entries without effective-session identity are parsed for compatibility but always miss. Git repositories with no commits and non-Git directories remain supported. Git control data and engine-owned workflow journals cannot be declared as inputs; if a Git-visible or explicit surface exceeds safety bounds, that call runs live.
|
|
372
|
+
Invalidations appear in progress output with a concise reason. Legacy v1 journals and early v2 entries without effective-session identity are parsed for compatibility but always miss. Journals written by pi-workflow-engine 1.1.0 or earlier miss once with `effective system prompt changed`, because later versions append the return-value instruction to every subagent's system prompt; the resumed run records fresh entries. Git repositories with no commits and non-Git directories remain supported. Git control data and engine-owned workflow journals cannot be declared as inputs; if a Git-visible or explicit surface exceeds safety bounds, that call runs live.
|
|
337
373
|
|
|
338
|
-
Statically loaded built-ins fingerprint and revalidate their bounded extension source tree so imported helper changes invalidate replay, while inline workflows use the compiler-provided script fingerprint. Dynamically discovered and programmatic modules are intentionally non-replayable because their transitive runtime imports or captured closures cannot be bound to an immutable source snapshot.
|
|
374
|
+
Statically loaded built-ins fingerprint and revalidate their bounded extension source tree so imported helper changes invalidate replay, while inline workflows and saved `.js` scripts use the compiler-provided script fingerprint. Dynamically discovered `.ts` and programmatic modules are intentionally non-replayable because their transitive runtime imports or captured closures cannot be bound to an immutable source snapshot.
|
|
339
375
|
|
|
340
376
|
Resume only caches completed `agent()` calls. It does not snapshot arbitrary workflow local variables, in-flight tool work, environment or clock state, external services, or generated inputs outside the captured repository/workflow/skill/tool surfaces. Use `resume: "off"` for calls that depend on those values, and keep prompts and options deterministic when you want cache hits.
|
|
341
377
|
|
|
@@ -351,6 +387,11 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
|
351
387
|
export const meta: WorkflowMeta = {
|
|
352
388
|
name: "my-workflow",
|
|
353
389
|
description: "Find and summarize something important.",
|
|
390
|
+
whenToUse: "Use when the user wants the important thing found and summarized; args name the target.",
|
|
391
|
+
phases: [
|
|
392
|
+
{ title: "Find", profile: "small" },
|
|
393
|
+
{ title: "Synthesize", profile: "medium" },
|
|
394
|
+
],
|
|
354
395
|
};
|
|
355
396
|
|
|
356
397
|
const Finding = Type.Object({
|
|
@@ -389,6 +430,8 @@ export default async function run({ agent, parallel, phase, args }: WorkflowApi)
|
|
|
389
430
|
}
|
|
390
431
|
```
|
|
391
432
|
|
|
433
|
+
`meta.name` is required. `description` and `whenToUse` describe the workflow in the picker and in the host's workflow catalog, so write `whenToUse` as the situation that should make a model choose it and what `args` it expects. `phases` declares the progress phases up front; each may add a default `profile` for agents that pass that phase title as their `phase` option.
|
|
434
|
+
|
|
392
435
|
Core primitives:
|
|
393
436
|
|
|
394
437
|
| Primitive | What it does |
|
|
@@ -401,6 +444,10 @@ Core primitives:
|
|
|
401
444
|
| `progress(event)` | Emits counters, summaries, and lane items. |
|
|
402
445
|
| `workflow(ref, args?)` | Runs another workflow inline as a sub-step and returns its result. |
|
|
403
446
|
|
|
447
|
+
Every subagent's system prompt states that its return value goes to the workflow program as data, not to a person: a text agent's final message is returned verbatim, so it replies with only the output its task asks for (no greeting, preamble, narration, or offer of further help), and a schema agent is told the same about its `final_answer` arguments. The instruction is appended after any `APPEND_SYSTEM.md` pi discovers, which subagents keep loading.
|
|
448
|
+
|
|
449
|
+
`args` holds the invocation's arguments as text and `input` holds them verbatim. A slash command passes a string, so both are that string. The `workflow` tool's `args` field and `workflow(ref, args)` also accept any JSON value: `input` is the value and `args` its JSON text, so workflows written for string arguments keep working. Omitted arguments are `""` in both. Like string arguments, JSON arguments are never written to the run record, so a run that had any cannot be resumed or restarted from `/workflow:runs`.
|
|
450
|
+
|
|
404
451
|
Schema agents accept structured output only through the validated, terminating `final_answer` tool. If the first response omits it, the engine makes at most **two repair attempts** (three prompt attempts total). Repair prompts expose only `final_answer` when the active pi session supports tool restriction. Clean exhaustion throws `WorkflowStructuredOutputError` with code `WORKFLOW_STRUCTURED_OUTPUT_NONCOMPLIANCE`; provider failures and host cancellation still propagate unchanged.
|
|
405
452
|
|
|
406
453
|
Use settled mode when the workflow must distinguish a successful `null` from a failed branch. It preserves input order and returns `{ ok: true, value }` or `{ ok: false, error: { name?, message, code?, details? } }` for every thunk. Default parallel mode still turns recoverable structured-output exhaustion into a `null` slot, while settled mode retains the typed code and attempt details. Genuine workflow cancellation still rejects the whole `parallel()` call and cancels siblings.
|
|
@@ -427,8 +474,8 @@ overrides its profile.
|
|
|
427
474
|
|
|
428
475
|
Subagents are SDK sessions, so pi's built-in codemode and MCP extensions do not load in them. Two opt-ins cover that:
|
|
429
476
|
|
|
430
|
-
- **Codemode.** Add `"codemode"` to an agent's `tools` to let it batch or filter many tool calls in one script, for example `tools: ["read", "grep", "codemode"]`. Scripts can call only that agent's other allowed tools, and never `final_answer`. Classifier calls a script makes count toward the run's usage and budget.
|
|
431
|
-
- **Host MCP tools.** In runs started by the `workflow` tool, subagents reach the host's MCP tools over its existing connections and through its permission handlers. Name a tool in `tools` (for example `"mcp__linear__create_issue"`), or use `toolHints: ["external-search"]` to pick up MCP web research tools; servers that declare a tool not read-only or closed-world are excluded from that hint. Bridged tools act in the host session, so the local `search` hint never bridges them, and an isolated worktree agent that names a local MCP tool sees the main checkout through it, not its worktree. `/workflow` commands and background runs outlive the host tool call that makes this possible: an agent there that names an MCP tool fails before prompting rather than running without it.
|
|
477
|
+
- **Codemode.** Add `"codemode"` to an agent's `tools` to let it batch or filter many tool calls in one script, for example `tools: ["read", "grep", "codemode"]`. Scripts can call only that agent's other allowed tools, and never `final_answer`. Classifier calls a script makes count toward the run's usage and budget. pi runs at most two codemode scripts per process, shared with the host, and fails a third instead of waiting; subagents' codemode calls therefore queue for one of those two slots, so a wide fan-out of codemode agents runs their scripts two at a time. A codemode agent runs its tool calls one at a time, and so do its scripts' own calls: by default pi checks every call in a message before it starts any, and a codemode call takes its slot in that check, so three codemode calls in one message would hold both slots with calls that cannot start while the third waits for one. To batch calls, put them in one script, which still makes them all in a single model turn. pi frees a slot only once the script's worker has exited, which can lag its result, so an agent can occasionally still get pi's "Codemode worker limit reached" error and retry. The host's own codemode calls do not queue.
|
|
478
|
+
- **Host MCP tools.** In runs started by the `workflow` tool, subagents reach the host's MCP tools over its existing connections and through its permission handlers. Name a tool in `tools` (for example `"mcp__linear__create_issue"`), or use `toolHints: ["external-search"]` to pick up MCP web research tools; servers that declare a tool not read-only or closed-world are excluded from that hint. Bridged tools act in the host session, so the local `search` hint never bridges them, and an isolated worktree agent that names a local MCP tool sees the main checkout through it, not its worktree. `/workflow` commands and background runs outlive the host tool call that makes this possible: an agent there that names an MCP tool fails before prompting rather than running without it. So that a run started early in a session does not race servers that are still connecting, the `workflow` tool tells pi (1.2 or newer) which servers a call may reach, and pi waits for them before the run starts: the servers of the `mcp__…` tools an inline script names, or every server for a named workflow, a script that uses `external-search` hints or `workflow()`, or any `scriptPath`. pi asks before the call runs and on its main thread, so a `scriptPath` file is not read then; the call itself reads it only if it is a regular file of at most 1 MiB. A background call waits for none. Each server becomes ready once, so the wait costs at most one pause at session start.
|
|
432
479
|
|
|
433
480
|
Subagents receive no skills by default. Opt in per agent with `skills: ["skill-name"]`; if `tools` is also restricted, the engine automatically keeps `read` available so the subagent can load the selected `SKILL.md`. When `skills` is omitted, clear prompt text such as `/skill:name`, `include skill name`, or `use the name skill` is also treated as an opt-in. Pass `skills: []` to suppress that inference.
|
|
434
481
|
|
|
@@ -457,16 +504,17 @@ Cleanup is a required finalizer. The engine attempts every registered worktree r
|
|
|
457
504
|
|
|
458
505
|
### Compose workflows
|
|
459
506
|
|
|
460
|
-
`workflow(ref, args?)` runs a registered workflow by name as a sub-step, returning its result. The child shares the parent run's concurrency cap, total-agent limit, per-agent timeout, abort signal, and perf timing, and its phases nest under `<name> ▸ <phase>` in the live UI.
|
|
507
|
+
`workflow(ref, args?)` runs a registered workflow by name, or an inline-format script file via `{ scriptPath }`, as a sub-step, returning its result. `args` is a string or any JSON value, which the child reads as `input` and, as text, `args`. The child shares the parent run's concurrency cap, total-agent limit, per-agent timeout, abort signal, and perf timing, and its phases nest under `<name> ▸ <phase>` in the live UI.
|
|
461
508
|
|
|
462
509
|
```ts
|
|
463
510
|
export default async function run({ workflow }: WorkflowApi) {
|
|
464
511
|
const review = await workflow("code-review", "HEAD~3");
|
|
465
|
-
|
|
512
|
+
const audit = await workflow({ scriptPath: "scripts/audit.js" }, { paths: ["src/"], strict: true });
|
|
513
|
+
return { review, audit };
|
|
466
514
|
}
|
|
467
515
|
```
|
|
468
516
|
|
|
469
|
-
Nesting is one level only: calling `workflow()` from inside a sub-workflow rejects. Resolution throws on an unknown name. Inside `parallel()` or `pipeline()`, recoverable branch errors become `null` results, so filter nulls before synthesis; a genuine run abort still rejects.
|
|
517
|
+
A `scriptPath` is resolved against the workflow cwd and must name a regular file of at most 1 MiB; the file follows the [inline workflow rules](#author-an-inline-workflow) and is fingerprinted by content like an inline script. Nesting is one level only: calling `workflow()` from inside a sub-workflow rejects. Resolution throws on an unknown name, an unreadable `scriptPath`, or a script that does not compile. Inside `parallel()` or `pipeline()`, recoverable branch errors become `null` results, so filter nulls before synthesis; a genuine run abort still rejects.
|
|
470
518
|
|
|
471
519
|
### Classify between stages
|
|
472
520
|
|
|
@@ -495,12 +543,19 @@ Guaranteed built-ins are statically registered:
|
|
|
495
543
|
2. Import it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`.
|
|
496
544
|
3. Add a `defineBuiltinWorkflow(mod, "<name>.ts")` entry to `BUILTIN_WORKFLOW_DEFINITIONS`.
|
|
497
545
|
|
|
546
|
+
Saved scripts are workflows in the [inline script format](#author-an-inline-workflow) kept as `*.js` files:
|
|
547
|
+
|
|
548
|
+
- `<project>/.pi/workflows/*.js`: project workflows, shared through the repository.
|
|
549
|
+
- `~/.pi/agent/workflows/*.js` (pi's configured agent directory): user workflows, next to the `.ts` drop-ins.
|
|
550
|
+
|
|
551
|
+
They compile like an inline `script`, with the injected `Type` and no imports, so they share pi's typebox, unlike `.ts` drop-ins, and they are fingerprinted by content, so `--resume` and `/workflow:runs` resume and restart them like built-ins. Project scripts load only when pi reports the project as trusted (`ctx.isProjectTrusted()`). pi itself does not ask for trust because of `.pi/workflows` alone, so a folder with no other protected project resources counts as trusted; review a repository's `.pi/workflows` before running them. Files starting with `_` or `.` and scripts that do not compile are skipped with a stderr note.
|
|
552
|
+
|
|
498
553
|
Drop-in workflows are also discovered best-effort from:
|
|
499
554
|
|
|
500
555
|
- `~/.pi/agent/workflows/*.ts`: the user-level drop-in directory, which survives `pi update`.
|
|
501
556
|
- The extension's own `workflows/*.ts` directory: `.pi/extensions/pi-workflow-engine/workflows/` in a working copy or a project-vendored copy. Files added to an installed package are replaced by `pi update`.
|
|
502
557
|
|
|
503
|
-
|
|
558
|
+
When two sources define the same name, the first wins: built-ins, project scripts, the extension's `.ts` drop-ins, user scripts, then user `.ts` drop-ins. Saved scripts are read again on every lookup, so a script added or edited mid-session runs by name (tool `name`, `/workflow <name>`, `workflow("<name>")`, `/workflow:runs`) with its current content at once. The `<workflows>` catalog stays as it was built for the session, to keep the provider's prompt cache, so a new script joins it from the next session. `.ts` drop-ins are loaded once per process: use `/workflow <name> --refresh` after adding or editing one.
|
|
504
559
|
|
|
505
560
|
## Author an inline workflow
|
|
506
561
|
|
|
@@ -527,6 +582,7 @@ Inline rules:
|
|
|
527
582
|
|
|
528
583
|
- start with `export const meta = { ... }`;
|
|
529
584
|
- default-export an async workflow function;
|
|
585
|
+
- write plain JavaScript: the script compiles in-process, so TypeScript annotations do not parse;
|
|
530
586
|
- no imports or dynamic `import()`;
|
|
531
587
|
- use the injected `Type` object for schemas;
|
|
532
588
|
- request `profile: "small"`, `"medium"`, or `"big"` instead of hard-coding provider models when the stage is portable;
|
|
@@ -535,6 +591,10 @@ Inline rules:
|
|
|
535
591
|
|
|
536
592
|
Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
|
|
537
593
|
|
|
594
|
+
The host agent's reference for writing these scripts is the bundled `workflow-authoring` skill (`skills/workflow-authoring/SKILL.md`). The `workflow` tool's guidelines stay short (the opt-in rule, the target and `Type` rules, profiles, the worktree return shape, host MCP reach, and the run-mode flags) and tell the model to read the skill before it writes or edits a script. The skill covers the full API, why fan-out uses `api.parallel` rather than `Promise.all` and defaults to `api.pipeline`, verification and completeness patterns, budget loops, worktree edits, resume, and saving a reusable workflow.
|
|
595
|
+
|
|
596
|
+
The tool also takes `scriptPath` in place of `script`: a regular file of at most 1 MiB holding an inline script, relative to the working directory. Each tool call gives exactly one of `name`, `script`, or `scriptPath`. Every inline run saves its script as `.pi/.workflow-runs/<run-id>.script.js` and names it in the result, so iterating does not need the script pasted again: edit that file in place, then call the tool with `scriptPath` set to it and `resumeFromRunId` set to the previous run. Replayable agent calls (see [Resume a run](#resume-a-run)) that the edit left unchanged are reused; edited and new calls run live. Edit the persisted file rather than a copy: `.pi/.workflow-runs/` is excluded from the repository state that shared `resume: "read-only"` calls are bound to, while editing any other copy inside the repository, or a saved `.pi/workflows/*.js` script, changes that state, so every shared read-only call misses and runs live.
|
|
597
|
+
|
|
538
598
|
## Runtime knobs
|
|
539
599
|
|
|
540
600
|
Only tune these when a workflow is too slow, too expensive, or too noisy:
|
|
@@ -543,7 +603,7 @@ Only tune these when a workflow is too slow, too expensive, or too noisy:
|
|
|
543
603
|
| --- | --- |
|
|
544
604
|
| `--concurrency=N` / `PI_WORKFLOW_CONCURRENCY=N` | Cap concurrent subagents. Default is `min(8, max(2, CPU count))`. |
|
|
545
605
|
| `--parallel-limit=N` / `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` | Limit eager `parallel()` submission. |
|
|
546
|
-
| `--max-agents=N` / `PI_WORKFLOW_MAX_AGENTS=N` | Cap live model calls across the complete run, including sub-workflows. Default `64`; values are clamped to `1`–`10000`. Replay hits do not consume the cap. The `workflow` tool field is `maxAgents`. |
|
|
606
|
+
| `--max-agents=N` / `PI_WORKFLOW_MAX_AGENTS=N` | Cap live model calls across the complete run, including sub-workflows. Default `64`; values are clamped to `1`–`10000`. Replay hits do not consume the cap. The `workflow` tool field is `maxAgents`. This is independent of the `/workflow:size` prompt guideline. |
|
|
547
607
|
| `--agent-timeout-ms=N` / `PI_WORKFLOW_AGENT_TIMEOUT_MS=N` | Abort one live agent after this duration. Default `1800000` (30 minutes); values are clamped to `1000`–`86400000`. The session is aborted and its isolated worktree is cleaned up. The `workflow` tool field is `agentTimeoutMs`. |
|
|
548
608
|
| `--agent-retries=N` / `PI_WORKFLOW_AGENT_RETRIES=N` | Restart the whole agent after a transient provider or transport failure that pi's in-session turn retry could not recover. Default `0`; values are clamped to `0`–`10`. Backoff starts at one second, doubles, and caps at 30 seconds. The `workflow` tool field is `agentRetries`. |
|
|
549
609
|
| `--budget=N` / `PI_WORKFLOW_BUDGET=N` | Set an output-token ceiling across recorded subagent attempts, including failed provider attempts. `agent()` throws `WorkflowBudgetExceededError` before starting another model request once the ceiling is reached; agents already running may overshoot because the engine does not reserve per-agent estimates. |
|
|
@@ -559,6 +619,38 @@ and abort scope. Usage from failed attempts is retained, so it can exhaust the
|
|
|
559
619
|
budget before the next retry begins. Host cancellation takes precedence over
|
|
560
620
|
limit and timeout failures.
|
|
561
621
|
|
|
622
|
+
### Turn budget directive
|
|
623
|
+
|
|
624
|
+
Put a standalone `+N` token in a prompt to give that turn's workflow runs one
|
|
625
|
+
shared output-token pool:
|
|
626
|
+
|
|
627
|
+
```text
|
|
628
|
+
dynamax +500k review this branch and verify every finding
|
|
629
|
+
+1.5m map every caller of the old API, then propose the migration
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The token takes `k` or `m` suffixes in either case (`+500k`, `+1.5m`, `+2M`) or
|
|
633
|
+
a plain count (`+250000`). It must follow the start of the prompt or whitespace
|
|
634
|
+
and end at whitespace, punctuation, or the end, so `C++`, `a+500k`, or `+500kb`
|
|
635
|
+
do not count, and values under 1,000 tokens are ignored so a `+1` in ordinary
|
|
636
|
+
prose never caps a run. The first such token in a typed prompt wins; prompts
|
|
637
|
+
that extensions send are not read.
|
|
638
|
+
|
|
639
|
+
While the pool is active, a `workflow` tool call without its own `budget` runs
|
|
640
|
+
with the pool's remainder as its budget (clamped to the tool's budget range),
|
|
641
|
+
and each synchronous run's output tokens are subtracted once it finishes,
|
|
642
|
+
failed or not. When less than one token remains, the tool refuses the call and
|
|
643
|
+
tells the agent the turn's budget is spent. An explicit `budget` on the call
|
|
644
|
+
can only lower that ceiling, is refused the same way once the pool is spent, and
|
|
645
|
+
its spend is subtracted too. The pool clears when the run that
|
|
646
|
+
received the prompt settles, so it still covers the run after pi retries a
|
|
647
|
+
provider error or compacts an overflowing context within that run.
|
|
648
|
+
|
|
649
|
+
Background runs receive the current remainder as their budget but do not draw
|
|
650
|
+
the pool down, because their usage lands after the turn. Parallel `workflow`
|
|
651
|
+
calls in one assistant message each see the same remainder before any of them
|
|
652
|
+
finishes. Per-run budget accounting is unchanged either way.
|
|
653
|
+
|
|
562
654
|
Transient provider failures are retried in two layers. First, each subagent
|
|
563
655
|
inherits pi's session-level automatic retry from your pi `retry` settings
|
|
564
656
|
(enabled by default: 3 retries with 2s, 4s, 8s backoff). It retries the failed
|
|
@@ -601,6 +693,13 @@ fails, the result retains verified records and reports incomplete coverage.
|
|
|
601
693
|
Synthesis selects or merges IDs; the workflow reconstructs evidence-bearing
|
|
602
694
|
fields from those records.
|
|
603
695
|
|
|
696
|
+
Each lens verifies at most the per-lens candidate count its finder prompt
|
|
697
|
+
states (six per `code-review` angle, four or five for the other workflows).
|
|
698
|
+
When a finder returns more, the workflow logs how many it cut from that lens.
|
|
699
|
+
`stats.candidates` counts everything the finders returned, and `stats.dropped`
|
|
700
|
+
counts the candidates never verified: those over a lens cap, duplicates, and
|
|
701
|
+
out-of-scope candidates.
|
|
702
|
+
|
|
604
703
|
### Duplicate candidates
|
|
605
704
|
|
|
606
705
|
Lenses often report the same defect in different words. When a TypeSafe Jev
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-workflow-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zero-dependency Dynamax workflows for pi: live parallel subagents, typed handoffs, synthesized results.",
|
|
6
6
|
"keywords": [
|
|
@@ -29,8 +29,12 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"pi": {
|
|
32
|
-
"extensions": [
|
|
33
|
-
|
|
32
|
+
"extensions": [
|
|
33
|
+
".pi/extensions/pi-workflow-engine/index.ts"
|
|
34
|
+
],
|
|
35
|
+
"skills": [
|
|
36
|
+
"skills"
|
|
37
|
+
],
|
|
34
38
|
"image": "https://raw.githubusercontent.com/timbrinded/pi-workflow-engine/master/assets/preview.png"
|
|
35
39
|
},
|
|
36
40
|
"scripts": {
|
|
@@ -55,7 +59,12 @@
|
|
|
55
59
|
"typebox": "*"
|
|
56
60
|
},
|
|
57
61
|
"devDependencies": {
|
|
58
|
-
"@types/node": "^22.20.
|
|
62
|
+
"@types/node": "^22.20.5",
|
|
59
63
|
"typescript": "^7.0.2"
|
|
64
|
+
},
|
|
65
|
+
"allowScripts": {
|
|
66
|
+
"esbuild": true,
|
|
67
|
+
"protobufjs": true,
|
|
68
|
+
"@google/genai": true
|
|
60
69
|
}
|
|
61
70
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-authoring
|
|
3
|
+
description: "Reference for writing pi-workflow-engine workflow scripts: the inline script contract, the injected api (agent, parallel, pipeline, classify, budget, workflow), quality patterns, resume, and saving a reusable workflow. Read before writing or editing an inline script for the workflow tool. It does not itself authorize running a workflow."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing workflow scripts
|
|
7
|
+
|
|
8
|
+
Read this before you write or edit a script for the `workflow` tool. It does not grant permission to run one: call the tool only when the user opted in (the `dynamax` token, sticky `/workflow:dynamax`, an explicit request to run or author a workflow, or a command or skill instruction).
|
|
9
|
+
|
|
10
|
+
A workflow is plain code that fans work out to subagents, passes validated data between stages, and returns one result. Prefer a registered workflow (`name`) when one fits; write a `script` when none does.
|
|
11
|
+
|
|
12
|
+
## Script contract
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
export const meta = {
|
|
16
|
+
name: "auth-audit",
|
|
17
|
+
description: "Audit the auth middleware for bypasses.",
|
|
18
|
+
phases: [{ title: "Find" }, { title: "Verify" }],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default async function run(api) {
|
|
22
|
+
const Finding = Type.Object({ file: Type.String(), summary: Type.String() });
|
|
23
|
+
const findings = await api.agent(`Find auth bypasses in ${api.args || "src/"}`, {
|
|
24
|
+
label: "find", phase: "Find", profile: "small", schema: Type.Object({ items: Type.Array(Finding, { maxItems: 20 }) }),
|
|
25
|
+
});
|
|
26
|
+
return { summary: `${findings.items.length} candidate bypasses`, findings: findings.items };
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- The script is plain JavaScript compiled in-process: no TypeScript annotations, `as` casts, or interfaces.
|
|
31
|
+
- It must start with `export const meta = { ... };`, a pure literal: strings, numbers, booleans, `null`, arrays, and objects only. No variables, calls, template literals, spreads, computed keys, or raw newlines inside strings. `name` is required; `description`, `whenToUse` (the situation that should make a model pick it), and `phases` are optional. Each phase is `{ title, profile? }`.
|
|
32
|
+
- Exactly one `export default` follows the meta: an `async function` or `async (api) => { ... }` with a block body. Nothing may follow it but `;` and comments. Put helpers and constants inside the function body.
|
|
33
|
+
- No `import` statements, no dynamic `import()`, no other exports.
|
|
34
|
+
- `Type` (TypeBox) is an injected global: write `Type.Object(...)` directly. It is not a property of `api`. Using the injected `Type` keeps schemas on pi's bundled TypeBox, so validation works.
|
|
35
|
+
- The return value becomes the tool result. Return a string or an object with a short `summary` first; the result the host model sees is truncated when it is very large, so return conclusions, not raw transcripts.
|
|
36
|
+
|
|
37
|
+
## The api
|
|
38
|
+
|
|
39
|
+
| Member | What it does |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `agent(prompt, opts?)` | Runs one subagent session. `prompt` must be a plain string (build it with a template string first). Without `schema` it resolves to the final assistant text, a string. With `schema` it resolves to an object already validated against the schema. |
|
|
42
|
+
| `parallel(thunks)` | Runs `() => Promise` thunks concurrently and waits for all. A recoverable failure becomes a `null` slot; the other slots keep their results. |
|
|
43
|
+
| `parallel(thunks, { settled: true })` | Same, but every slot is `{ ok: true, value }` or `{ ok: false, error: { name?, message, code?, details? } }`, in input order. |
|
|
44
|
+
| `pipeline(items, ...stages)` | Runs every item through all stages independently. Each stage is `(prev, item, index) => Promise`; the first stage receives the item as `prev`. A failed stage turns that item's slot into `null` and skips its later stages. |
|
|
45
|
+
| `classify(context, opts?)` | Answers typed `choice`/`score`/`bool` questions about JSON `state` with a classifier model and no chat session. |
|
|
46
|
+
| `workflow(nameOrRef, args?)` | Runs another workflow as a sub-step and returns its result: a registered workflow by name, or an inline-format script file via `{ scriptPath }`, relative to the workflow cwd. `args` is a string or any JSON value, which the child reads as `input` and `args`. One level of nesting only. Rejects on an unknown name, an unreadable `scriptPath`, or a script that does not compile. |
|
|
47
|
+
| `phase(title)` / `log(message)` / `progress(event)` | Drive the live progress tree, logs, counters, and lanes. |
|
|
48
|
+
| `budget` | `{ total, spent(), remaining() }` in output tokens; `total` is `null` when the run has no budget. |
|
|
49
|
+
| `input` | The arguments verbatim: the `/workflow` command's string, or the JSON value the tool call or `workflow()` passed; `""` when none was given. |
|
|
50
|
+
| `args`, `cwd`, `signal` | The arguments as text (a string as is, any other JSON value as JSON text, `""` when none), the host working directory, and the run's abort signal. |
|
|
51
|
+
|
|
52
|
+
Agent options:
|
|
53
|
+
|
|
54
|
+
| Option | Meaning |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `profile` | `"small"`, `"medium"`, or `"big"`: the user's central model routes. Set one on every agent. |
|
|
57
|
+
| `label`, `phase` | Row label and phase group in the progress tree. |
|
|
58
|
+
| `schema` | TypeBox schema; the agent must answer through a validated `final_answer` tool. Two repair attempts, then the call rejects with code `WORKFLOW_STRUCTURED_OUTPUT_NONCOMPLIANCE`. |
|
|
59
|
+
| `tools` | Strict allowlist of tool names, for example `["read", "bash", "grep", "find", "ls"]`. `[]` gives a tool-free agent. |
|
|
60
|
+
| `toolHints` | `["search"]` adds installed grep/find/code-search tools; `["external-search"]` adds web search, browsing, and URL-extraction tools. `requireToolHints: true` fails before prompting when a hint matched nothing. |
|
|
61
|
+
| `skills` | Pi skills to expose to this subagent; none by default. |
|
|
62
|
+
| `isolation: "worktree"` | Run in a disposable git worktree and return the patch (see below). |
|
|
63
|
+
| `model`, `thinkingLevel` | Intentional per-call overrides of the profile. `model` is `"provider/id"`. |
|
|
64
|
+
| `resume`, `resumeInputs`, `cacheKey` | Replay controls (see Resume). |
|
|
65
|
+
|
|
66
|
+
Add `"codemode"` to an agent's `tools` when it should batch or filter many tool calls in one script; those scripts can call only the agent's other allowed tools. Host MCP tools reach subagents only in synchronous `workflow` tool runs: name them in `tools` (for example `"mcp__linear__create_issue"`) or use `toolHints: ["external-search"]`. Background runs and `/workflow` commands cannot bridge them, and an agent that names one fails before prompting.
|
|
67
|
+
|
|
68
|
+
## Profiles, labels, and phases
|
|
69
|
+
|
|
70
|
+
- Set `profile` on every `agent()` call: `small` for scouting, finding, and verifying; `medium` for judgement and synthesis; `big` for the hardest reasoning. Use `model`/`thinkingLevel` only for a deliberate override.
|
|
71
|
+
- A phase declared as `{ title: "Verify", profile: "small" }` routes agents that pass `phase: "Verify"` and set neither `profile` nor `model`. Routing order: the agent's `model`, its `profile`, its phase's `profile`, then the host model.
|
|
72
|
+
- Inside `parallel` and `pipeline`, pass `phase` and `label` in each agent's options. The global `phase()` is one shared value that every concurrent branch reads and writes, so calling it inside fan-out branches races and misfiles rows. Call `phase()` only between sequential stages.
|
|
73
|
+
|
|
74
|
+
## Concurrency rules
|
|
75
|
+
|
|
76
|
+
**Use `api.parallel`, never `Promise.all` or `Promise.allSettled` over agents.** `api.parallel` turns a recoverable failure (schema exhaustion, an agent timeout, the budget or agent limit, a provider error) into a `null` slot and the survivors continue; `Promise.all` rejects the whole stage on the first failure and discards every sibling result. `api.parallel` also applies the run's submission limit and cancels siblings on a genuine abort. Filter the nulls before using the results:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const found = (await api.parallel(lenses.map((lens) => () =>
|
|
80
|
+
api.agent(`Review ${api.args} for ${lens}`, { label: `find:${lens}`, phase: "Find", profile: "small", schema: Findings }),
|
|
81
|
+
))).filter((result) => result !== null);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use settled mode when the error detail matters, for example to report which branches failed and why instead of silently shrinking coverage.
|
|
85
|
+
|
|
86
|
+
**Default to `api.pipeline(items, ...stages)`.** With no barrier between stages, item A can be verified while item B is still being found, and the run's concurrency cap still bounds the number of live agents, so it is safe to start every item at once:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const verified = await api.pipeline(
|
|
90
|
+
files,
|
|
91
|
+
(file) => api.agent(`Find defects in ${file}`, { label: `find:${file}`, phase: "Find", profile: "small", schema: Findings }),
|
|
92
|
+
(found, file) => api.parallel(found.items.map((finding, i) => () =>
|
|
93
|
+
api.agent(`Try to refute this finding in ${file}: ${JSON.stringify(finding)}`, {
|
|
94
|
+
label: `verify:${file}:${i}`, phase: "Verify", profile: "small", schema: Verdict,
|
|
95
|
+
}),
|
|
96
|
+
)),
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Barrier smell test: before writing `await api.parallel(...)` followed by another fan-out over its results, ask whether the next stage needs every result of the previous one. A barrier is justified only for cross-item dedupe or merge, an early exit on a zero count, or a stage that compares against all prior results. Flatten, map, or filter alone is not a reason: do it inside a pipeline stage.
|
|
101
|
+
|
|
102
|
+
Host-written scripts in practice reached for `Promise.all` and never for `pipeline`; both habits lose results or wall-clock time. Check every fan-out against these two rules.
|
|
103
|
+
|
|
104
|
+
## Quality patterns
|
|
105
|
+
|
|
106
|
+
- **Adversarial verify.** Give each finding to a separate verifier told to refute it, and keep only what survives. Thin evidence means not substantiated, not confirmed.
|
|
107
|
+
- **Perspective-diverse verify.** For findings that matter, use two or three verifiers with different methods (read the callers, reproduce it, check the tests) and require agreement.
|
|
108
|
+
- **Judge panel.** Generate several independent answers or designs, then have a judge (or several) pick or merge them against stated criteria.
|
|
109
|
+
- **Loop until dry.** Repeat discovery rounds, deduping each round against everything seen so far, not only the last round. Stop when a round adds nothing new, or at a round cap or budget guard.
|
|
110
|
+
- **Multi-modal sweep.** Cover the space from different angles (per file, per lens, per symptom, search-driven and reading-driven) and take the union.
|
|
111
|
+
- **Completeness critic.** After synthesis, have an agent check the result against the original request or work list and name the gaps; follow up only on material ones.
|
|
112
|
+
- **No silent caps.** Whenever you slice a list, cap rounds, or skip items, `api.log()` what was dropped and report it in the result, for example in a `gaps` or `skipped` field.
|
|
113
|
+
- **Adaptive multi-pass, only when needed.** A single-pass fan-out is the default when it can answer the question. When the first pass may expose gaps, conflicts, weak claims, or missing evidence: run the bounded first-pass agents; give their surviving results to a structured gap-analysis agent so the LLM decides what needs follow-up; put a hard `maxItems` bound on LLM-authored task arrays and defensively slice them before fan-out; use ordinary conditionals or bounded loops to launch follow-up agents only when gaps exist; synthesize the first-pass and follow-up results together. Do not generate a second pass when the first pass is sufficient, and do not invent iteration, quorum, graph, reduction, or retry primitives for this.
|
|
114
|
+
|
|
115
|
+
## Plan the run
|
|
116
|
+
|
|
117
|
+
- **Scout inline first, then orchestrate.** Use your own tools to find the work list (the files, failing tests, or findings) before writing the script, then fan out over that concrete list. Embed the list in the script or pass it as a JSON `args` value and read it from `api.input`.
|
|
118
|
+
- **One single-phase workflow per turn.** For multi-phase work (understand, design, implement, review), run one workflow per phase and read each result before choosing the next, rather than one script that guesses every phase in advance.
|
|
119
|
+
- **Scale to the request and the size guideline.** A focused question needs a few agents, a broad sweep more. The `<workflows>` section's size guideline line (set by the user with `/workflow:size`) bounds each workflow unless the user asks for more. The run's `maxAgents` limit (default 64) is a separate hard backstop, not a target.
|
|
120
|
+
- **Use `classify()` as a cheap gate** between stages: dedupe, triage, or route items without a full agent. It rejects when no classifier model is configured, so call it inside `api.parallel` or a `try`/`catch` and fall back to an agent or to keeping the item. Results are not journaled, so a resumed run classifies again.
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
const answers = await api.classify(
|
|
124
|
+
{ state: { finding }, questions: { real: { type: "bool", instructions: "Is this a real defect?", criteria: { true: "Concrete and reachable", false: "Speculative" } } } },
|
|
125
|
+
{ label: `triage:${finding.id}` },
|
|
126
|
+
);
|
|
127
|
+
const keep = answers.real?.type === "bool" ? answers.real.probability > 0.5 : true;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Budget
|
|
131
|
+
|
|
132
|
+
`api.budget.total` is the output-token ceiling (`null` when unset); `spent()` and `remaining()` are live. When the user's prompt carries a `+500k`-style directive, every run that turn is capped at the remaining pool (its own `budget` can only lower that), and synchronous runs draw the pool down; once it is spent the tool refuses further runs that turn. Once the ceiling is reached `agent()` rejects with `WorkflowBudgetExceededError` (a `null` slot inside `parallel`); agents already running can overshoot. Scale fleets from `total`, and guard every loop with it so an unbudgeted run still terminates:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const width = api.budget.total ? Math.max(2, Math.min(8, Math.floor(api.budget.total / 40_000))) : 4;
|
|
136
|
+
for (let round = 0; round < 5 && (api.budget.total === null || api.budget.remaining() > 30_000); round++) {
|
|
137
|
+
// one discovery round
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Edits in worktrees
|
|
142
|
+
|
|
143
|
+
Give `isolation: "worktree"` only to agents that edit files. The agent runs in a disposable git worktree, and `agent()` resolves to `{ result, patch, changed, baselineOid }`: `result` is the usual text or schema object, `patch` is its `git diff`, `changed` says whether it touched anything. The user's checkout is never modified; return the patches so the host can review and apply them. It requires the workflow `cwd` to be inside a git work tree.
|
|
144
|
+
|
|
145
|
+
## Resume and diagnosis
|
|
146
|
+
|
|
147
|
+
- Every result names its run ID. Pass `resumeFromRunId: "<run id>"` to the tool to replay completed agent calls whose prompt, options, and execution identity still match; the rest run live.
|
|
148
|
+
- Shared-workspace agents replay only with `resume: "read-only"` (use it for advisory agents); isolated agents replay by default. `resume: "off"` forces a live call. Give repeated identical calls a distinct `cacheKey`, such as `` `verify:${finding.id}` ``.
|
|
149
|
+
- To iterate on an inline run, edit the persisted script its result names (`.pi/.workflow-runs/<runId>.script.js`) in place, then call the tool with `scriptPath` set to that file and `resumeFromRunId`. `scriptPath` implies `resumeEditedWorkflow`, so unchanged replayable calls are reused and edited or new ones run live. Edit that file, not a copy: it is excluded from the repository fingerprint, while editing any other copy in the repository, or a saved `.pi/workflows` script, changes repository state, so every shared `read-only` call reruns live.
|
|
150
|
+
- After editing the script, `resumeEditedWorkflow: true` (with `resumeFromRunId`) reuses calls that did not change. Use it only when the user accepts that.
|
|
151
|
+
- The journal at `.pi/.workflow-runs/<runId>.jsonl` holds one line per completed replayable agent call (`resume: "read-only"` or isolated) with its result; `<runId>.run.json` beside it holds the run state, compact progress, and the final result or error. Read them before diagnosing an empty or unexpected result: a replayable call missing from the journal failed or never ran, while calls without a replay policy are never journaled.
|
|
152
|
+
|
|
153
|
+
## Run modes
|
|
154
|
+
|
|
155
|
+
- `background: true` only when the user wants the run to continue after this tool call; the tool returns a run ID and the result arrives later as a message naming the run's record and journal (and script, for an inline run). Under Dynamax that message carries the full result or error and starts a new turn, so you wake to read it and choose the next step; otherwise it arrives silently for the next turn with only the summary, and the record holds the rest. `autoResumeOnUsageLimit: true` only for such a background run when the user wants bounded automatic continuation after a provider usage window.
|
|
156
|
+
- Every call provides exactly one of `name`, `script`, or `scriptPath`.
|
|
157
|
+
|
|
158
|
+
## Save a reusable workflow
|
|
159
|
+
|
|
160
|
+
When a script is worth keeping, write it to `.pi/workflows/<name>.js` with the same contract (pure-literal `meta`, one default export, no imports, injected `Type`). Saved workflows are discovered by name, so later runs can use `name: "<name>"` or `/workflow <name>`; `scriptPath: ".pi/workflows/<name>.js"` runs the file directly. The file is read again on every run by name, so a script saved or edited this session runs with its current content right away, but it appears in the `<workflows>` catalog only from the next session.
|