pi-workflow-engine 1.0.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 +236 -94
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +9 -6
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +10 -3
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +18 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +71 -23
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +12 -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/classify.ts +107 -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 +123 -40
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +78 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +5 -0
- package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -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/model-ref.ts +19 -0
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
- 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 +8 -6
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +15 -3
- package/.pi/extensions/pi-workflow-engine/src/types.ts +68 -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/usage.ts +20 -2
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +123 -9
- 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 +12 -4
- package/USAGE.md +167 -23
- package/package.json +13 -4
- package/skills/workflow-authoring/SKILL.md +160 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface WorkflowToolRequestParams {
|
|
2
|
+
readonly name?: string;
|
|
3
|
+
readonly script?: string;
|
|
4
|
+
readonly scriptPath?: string;
|
|
5
|
+
readonly resumeFromRunId?: string;
|
|
6
|
+
readonly background?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type WorkflowToolRequest =
|
|
10
|
+
| { readonly kind: "named"; readonly name: string }
|
|
11
|
+
| { readonly kind: "inline"; readonly script: string }
|
|
12
|
+
| { readonly kind: "script-path"; readonly scriptPath: string }
|
|
13
|
+
| { readonly kind: "error"; readonly error: "invalid_workflow_invocation"; readonly message: string };
|
|
14
|
+
|
|
15
|
+
export const INVALID_WORKFLOW_INVOCATION_MESSAGE = "Provide exactly one of a workflow name, an inline workflow script, or a scriptPath.";
|
|
16
|
+
|
|
17
|
+
export function normalizeWorkflowToolRequest(params: WorkflowToolRequestParams): WorkflowToolRequest {
|
|
18
|
+
const name = params.name?.trim() ?? "";
|
|
19
|
+
const script = params.script?.trim() ?? "";
|
|
20
|
+
const scriptPath = params.scriptPath?.trim() ?? "";
|
|
21
|
+
const provided = [name, script, scriptPath].filter((value) => value.length > 0).length;
|
|
22
|
+
if (provided !== 1) return { kind: "error", error: "invalid_workflow_invocation", message: INVALID_WORKFLOW_INVOCATION_MESSAGE };
|
|
23
|
+
if (name) return { kind: "named", name };
|
|
24
|
+
return script ? { kind: "inline", script } : { kind: "script-path", scriptPath };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** pi names MCP tools `mcp__<server>__<tool>`; such a word contains its server's `mcp__<server>` namespace. */
|
|
28
|
+
const MCP_TOOL_REFERENCE = /mcp__[A-Za-z0-9_-]+/g;
|
|
29
|
+
/** `api.workflow(...)` or a destructured `workflow(...)`; prose that happens to match only costs one wait. */
|
|
30
|
+
const SUB_WORKFLOW_CALL = /\bworkflow\s*\(/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Which tools a `workflow` tool call may reach, for pi's `reachesTools`, read from the same request the
|
|
34
|
+
* call executes. Before the call runs, pi's MCP extension waits for every server whose namespace a
|
|
35
|
+
* returned word contains, or for all of them on `"all"`. Without that wait a synchronous run early in a
|
|
36
|
+
* session can start before MCP tools register, and agents that name them then fail at the host-tools bridge.
|
|
37
|
+
*
|
|
38
|
+
* - A background run never gets the bridge, so it reaches nothing.
|
|
39
|
+
* - A named workflow is opaque, so it reaches `"all"`. Each server's readiness settles once, so this
|
|
40
|
+
* costs at most one wait at session start.
|
|
41
|
+
* - An inline script reaches the MCP tools it names, or `"all"` when it asks for `external-search`
|
|
42
|
+
* tools or composes a sub-workflow, which is as opaque as a named one.
|
|
43
|
+
* - A script file reaches `"all"` unread. pi asks synchronously on its main thread, where reading a FIFO,
|
|
44
|
+
* a terminal or a device named as `scriptPath` would block or never end.
|
|
45
|
+
* - An invalid call fails before running anything, so it reaches nothing.
|
|
46
|
+
*/
|
|
47
|
+
export function workflowToolReach(input: Record<string, unknown>): "all" | readonly string[] {
|
|
48
|
+
if (input.background === true) return [];
|
|
49
|
+
const request = normalizeWorkflowToolRequest({ name: text(input.name), script: text(input.script), scriptPath: text(input.scriptPath) });
|
|
50
|
+
if (request.kind === "inline") return scriptReach(request.script);
|
|
51
|
+
return request.kind === "error" ? [] : "all";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scriptReach(script: string): "all" | readonly string[] {
|
|
55
|
+
if (script.includes("external-search") || SUB_WORKFLOW_CALL.test(script)) return "all";
|
|
56
|
+
return [...new Set(script.match(MCP_TOOL_REFERENCE) ?? [])];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function text(value: unknown): string | undefined {
|
|
60
|
+
return typeof value === "string" ? value : undefined;
|
|
61
|
+
}
|
|
@@ -23,6 +23,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
|
23
23
|
export const meta: WorkflowMeta = {
|
|
24
24
|
name: "code-review",
|
|
25
25
|
description: "Fan-out review of the branch's open PR (or branch vs main): scope → per-angle find → independent verify → synthesize.",
|
|
26
|
+
whenToUse: "Use to review a PR, branch diff, ref range, or focused target for correctness bugs and cleanups, with every finding independently verified; args are the target or review instructions, blank auto-detects the diff.",
|
|
26
27
|
phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
|
|
27
28
|
};
|
|
28
29
|
|
|
@@ -16,6 +16,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
|
16
16
|
export const meta: WorkflowMeta = {
|
|
17
17
|
name: "diagnose",
|
|
18
18
|
description: "Advisory-only bug diagnosis: scope symptoms → competing hypotheses → independent verify → synthesize likely root causes.",
|
|
19
|
+
whenToUse: "Use when a bug, failing command, flaky test, or regression has an unclear cause and competing hypotheses should be checked independently before anyone edits code; args describe the symptom.",
|
|
19
20
|
phases: [{ title: "Scope" }, { title: "Hypothesize" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
|
|
20
21
|
};
|
|
21
22
|
|
|
@@ -17,6 +17,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
|
17
17
|
export const meta: WorkflowMeta = {
|
|
18
18
|
name: "perf-review",
|
|
19
19
|
description: "Advisory-only performance review: scope slow path → per-lens bottleneck hypotheses → verify evidence → synthesize measurements and safe optimizations.",
|
|
20
|
+
whenToUse: "Use for a slow path, startup cost, or performance concern where measured bottlenecks and safe optimizations matter more than guesses; args name the path, workload, or command.",
|
|
20
21
|
phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
|
|
21
22
|
};
|
|
22
23
|
|
|
@@ -18,6 +18,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
|
18
18
|
export const meta: WorkflowMeta = {
|
|
19
19
|
name: "refactor-scout",
|
|
20
20
|
description: "Advisory-only refactor scout: scope → per-lens find → independent verify → synthesize safe refactor opportunities.",
|
|
21
|
+
whenToUse: "Use to find small, behaviour-preserving refactors (duplication, complexity, type safety, boundaries, dead code) in a path or module without changing it; args name the target.",
|
|
21
22
|
phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MAX_RESEARCH_LANES,
|
|
3
|
+
MAX_VERIFICATION_CLAIMS,
|
|
3
4
|
ResearchLaneResultSchema,
|
|
4
5
|
ResearchPlanSchema,
|
|
5
6
|
ResearchReportSchema,
|
|
@@ -23,6 +24,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
|
23
24
|
export const meta: WorkflowMeta = {
|
|
24
25
|
name: "research",
|
|
25
26
|
description: "Source-grounded external research: decompose → gather direct-page evidence → independently verify claims → cited synthesis.",
|
|
27
|
+
whenToUse: "Use for questions that need current external sources and cited, independently verified claims; needs an installed web search or browsing tool; args are the question and any scope constraints.",
|
|
26
28
|
phases: [{ title: "Plan" }, { title: "Gather" }, { title: "Verify" }, { title: "Synthesize" }],
|
|
27
29
|
};
|
|
28
30
|
|
|
@@ -105,10 +107,11 @@ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
|
|
|
105
107
|
const gathered = gatherResults.flatMap((result) => (result.ok ? [result.value] : []));
|
|
106
108
|
const failedLanes = lanes.filter((_, index) => !gatherResults[index]!.ok);
|
|
107
109
|
|
|
108
|
-
const candidates = buildClaimCandidates(gathered);
|
|
110
|
+
const { candidates, overCap } = buildClaimCandidates(gathered);
|
|
109
111
|
if (candidates.length === 0) return unavailableResearchReport(failedLanes.length > 0 ? "lanes-failed" : "no-evidence");
|
|
110
112
|
progress({ type: "counter", key: "research.claims", label: "claims to verify", value: candidates.length });
|
|
111
113
|
log(`${candidates.length} bounded claim(s) selected for independent verification`);
|
|
114
|
+
if (overCap > 0) log(`Dropped ${overCap} lower-priority claim(s) over the verification cap of ${MAX_VERIFICATION_CLAIMS}`);
|
|
112
115
|
|
|
113
116
|
phase("Verify");
|
|
114
117
|
const verificationResults = await parallel(
|
|
@@ -162,9 +165,14 @@ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
|
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const report = sanitizeResearchReport(synthesis, synthesisInputs);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
const gaps: string[] = [];
|
|
169
|
+
if (failedLanes.length > 0) {
|
|
170
|
+
gaps.push(`${failedLanes.length} of ${lanes.length} research lane(s) failed (${failedLanes.map((lane) => lane.title).join(", ")}); coverage is incomplete.`);
|
|
171
|
+
}
|
|
172
|
+
if (overCap > 0) {
|
|
173
|
+
gaps.push(`${overCap} lower-priority claim(s) were not independently verified; verification is capped at ${MAX_VERIFICATION_CLAIMS} claims.`);
|
|
174
|
+
}
|
|
175
|
+
return gaps.length === 0 ? report : { ...report, limitations: [...report.limitations, ...gaps] };
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
function verificationPrompt(question: string, candidate: ResearchClaimCandidate): string {
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ one-off workflow for the task.
|
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
|
-
Requires **pi 0.
|
|
18
|
+
Requires **pi 1.0.0 or newer**.
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
pi install npm:pi-workflow-engine
|
|
@@ -72,15 +72,21 @@ dynamax investigate this flaky test using separate timing, fixture, diff, and ex
|
|
|
72
72
|
dynamax argue both implementation options, then have another agent challenge the recommendation
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
The token enables Dynamax for one
|
|
76
|
-
session
|
|
75
|
+
The token enables Dynamax for that one request. Sticky mode instead makes
|
|
76
|
+
workflow orchestration the default for every substantive task in the pi session;
|
|
77
|
+
`/workflow:dynamax` with no argument toggles it:
|
|
77
78
|
|
|
78
79
|
```text
|
|
80
|
+
/workflow:dynamax
|
|
79
81
|
/workflow:dynamax on
|
|
80
82
|
/workflow:dynamax off
|
|
81
83
|
/workflow:dynamax status
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
`/workflow:size small|medium|large|off` sets how many agents pi aims to keep
|
|
87
|
+
each authored workflow under (5, 10 by default, or 25); see the
|
|
88
|
+
[size guideline](USAGE.md#workflow-size-guideline).
|
|
89
|
+
|
|
84
90
|
In the TUI, a standalone `dynamax` token receives a short moving-shine sweep and
|
|
85
91
|
then stays highlighted. Use `PI_DYNAMAX_EFFECT=static` or
|
|
86
92
|
`PI_DYNAMAX_EFFECT=off` to reduce or disable the cue; the
|
|
@@ -108,7 +114,9 @@ covers review actions and recent run history.
|
|
|
108
114
|
## Create your own workflows
|
|
109
115
|
|
|
110
116
|
Saved workflows are TypeScript modules for repeatable orchestration; inline
|
|
111
|
-
workflows are useful for one-off Dynamax tasks.
|
|
117
|
+
workflows are useful for one-off Dynamax tasks. An inline script that proves
|
|
118
|
+
useful can be saved as a `.js` file in a project's `.pi/workflows/` or in
|
|
119
|
+
`~/.pi/agent/workflows/` and run by name. The
|
|
112
120
|
[workflow authoring guide](USAGE.md#author-a-saved-workflow) covers the API,
|
|
113
121
|
typed handoffs, model profiles, tool access, composition, and registration.
|
|
114
122
|
|
package/USAGE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-workflow-engine` adds zero-dependency Dynamax workflows to pi: opt into live, parallel subagents for the tasks that need a custom investigation instead of one long prompt. Most users only need these surfaces:
|
|
4
4
|
|
|
5
|
-
This release requires **pi 0.
|
|
5
|
+
This release requires **pi 1.0.0 or newer**. pi remains host-provided rather
|
|
6
6
|
than bundled by this package, and the extension rejects older runtimes at load
|
|
7
7
|
time with an update message.
|
|
8
8
|
|
|
@@ -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
|
|
@@ -109,16 +114,20 @@ command instead of selecting another model.
|
|
|
109
114
|
as external web search, browsing, or HTTP(S) page extraction. Local grep/find
|
|
110
115
|
tools do not satisfy this check. The workflow does not bundle a scraper or a
|
|
111
116
|
paid API; install or enable the web capability you prefer, then restart or
|
|
112
|
-
reload pi so subagent sessions can see it.
|
|
117
|
+
reload pi so subagent sessions can see it. An MCP web search server also
|
|
118
|
+
qualifies when the host agent runs `research` through the `workflow` tool,
|
|
119
|
+
because only those runs can reach the host's MCP tools.
|
|
113
120
|
|
|
114
121
|
The workflow bounds decomposition to four lanes and verification to twelve
|
|
115
122
|
claims, while the engine's concurrency, total-agent, timeout, cancellation, and
|
|
116
|
-
token-budget limits still apply.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
122
131
|
|
|
123
132
|
## Use Dynamax for custom workflows
|
|
124
133
|
|
|
@@ -139,6 +148,7 @@ dynamax use parallel agents to review this design and report the risks
|
|
|
139
148
|
Sticky mode and inspector shortcut:
|
|
140
149
|
|
|
141
150
|
```text
|
|
151
|
+
/workflow:dynamax # toggle sticky mode
|
|
142
152
|
/workflow:dynamax on
|
|
143
153
|
/workflow:dynamax off
|
|
144
154
|
/workflow:dynamax status
|
|
@@ -146,7 +156,7 @@ Sticky mode and inspector shortcut:
|
|
|
146
156
|
|
|
147
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.
|
|
148
158
|
|
|
149
|
-
When only the literal `dynamax` token is used, the opt-in is one-shot: the next agent run receives the workflow
|
|
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.
|
|
150
160
|
|
|
151
161
|
### Prompt editor cue
|
|
152
162
|
|
|
@@ -160,7 +170,7 @@ The default effect is `shine`. Set `PI_DYNAMAX_EFFECT=static` for a motionless
|
|
|
160
170
|
cue or `PI_DYNAMAX_EFFECT=off` to disable it. A non-empty `NO_COLOR` also
|
|
161
171
|
disables the default cue; an explicit `PI_DYNAMAX_EFFECT` value takes precedence.
|
|
162
172
|
|
|
163
|
-
The implementation follows pi
|
|
173
|
+
The implementation follows pi's [official `CustomEditor`
|
|
164
174
|
pattern](https://pi.dev/docs/latest/extensions#custom-editor): it decorates only
|
|
165
175
|
the editor's rendered lines, retaining the wrapped editor's input, cursor,
|
|
166
176
|
deletion, paste, multiline, undo, completion, IME, and app-keybinding behavior.
|
|
@@ -189,6 +199,24 @@ Set either shortcut to `null` to disable it while keeping `/workflow:inspector`
|
|
|
189
199
|
|
|
190
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.
|
|
191
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
|
+
|
|
192
220
|
### Adaptive multi-pass authoring
|
|
193
221
|
|
|
194
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:
|
|
@@ -247,6 +275,8 @@ During a run, a widget above the editor shows the workflow's progress bar, finis
|
|
|
247
275
|
|
|
248
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.
|
|
249
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
|
+
|
|
250
280
|
### Durable run records
|
|
251
281
|
|
|
252
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.
|
|
@@ -255,11 +285,17 @@ Record replacement is atomic, corrupt or unsupported-version files are ignored i
|
|
|
255
285
|
|
|
256
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.
|
|
257
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
|
+
|
|
258
292
|
### Background workflows
|
|
259
293
|
|
|
260
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.
|
|
261
295
|
|
|
262
|
-
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.
|
|
263
299
|
|
|
264
300
|
Provider usage-window continuation is a separate, explicit background option:
|
|
265
301
|
|
|
@@ -273,7 +309,7 @@ Provider usage-window continuation is a separate, explicit background option:
|
|
|
273
309
|
}
|
|
274
310
|
```
|
|
275
311
|
|
|
276
|
-
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.
|
|
277
313
|
|
|
278
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.
|
|
279
315
|
|
|
@@ -283,7 +319,7 @@ A graceful pi session shutdown aborts active background work and records it as `
|
|
|
283
319
|
|
|
284
320
|
### Recent runs and lifecycle actions
|
|
285
321
|
|
|
286
|
-
`/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.
|
|
287
323
|
|
|
288
324
|
The same operations are available without the selection UI:
|
|
289
325
|
|
|
@@ -295,7 +331,7 @@ The same operations are available without the selection UI:
|
|
|
295
331
|
/workflow:inspector <run-id>
|
|
296
332
|
```
|
|
297
333
|
|
|
298
|
-
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.
|
|
299
335
|
|
|
300
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.
|
|
301
337
|
|
|
@@ -318,6 +354,8 @@ With edited-workflow resume enabled, unchanged calls are reused while changed or
|
|
|
318
354
|
|
|
319
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.
|
|
320
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
|
+
|
|
321
359
|
Replay is explicit for agents that share the workflow directory and automatic for isolated patch-producing agents:
|
|
322
360
|
|
|
323
361
|
| Agent call | Default during resume | Override |
|
|
@@ -331,9 +369,9 @@ Journals use replay contract v2. A hit requires the same prompt/options and exec
|
|
|
331
369
|
|
|
332
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.
|
|
333
371
|
|
|
334
|
-
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.
|
|
335
373
|
|
|
336
|
-
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.
|
|
337
375
|
|
|
338
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.
|
|
339
377
|
|
|
@@ -349,6 +387,11 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
|
349
387
|
export const meta: WorkflowMeta = {
|
|
350
388
|
name: "my-workflow",
|
|
351
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
|
+
],
|
|
352
395
|
};
|
|
353
396
|
|
|
354
397
|
const Finding = Type.Object({
|
|
@@ -387,6 +430,8 @@ export default async function run({ agent, parallel, phase, args }: WorkflowApi)
|
|
|
387
430
|
}
|
|
388
431
|
```
|
|
389
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
|
+
|
|
390
435
|
Core primitives:
|
|
391
436
|
|
|
392
437
|
| Primitive | What it does |
|
|
@@ -399,6 +444,10 @@ Core primitives:
|
|
|
399
444
|
| `progress(event)` | Emits counters, summaries, and lane items. |
|
|
400
445
|
| `workflow(ref, args?)` | Runs another workflow inline as a sub-step and returns its result. |
|
|
401
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
|
+
|
|
402
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.
|
|
403
452
|
|
|
404
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.
|
|
@@ -423,6 +472,11 @@ overrides its profile.
|
|
|
423
472
|
|
|
424
473
|
`tools` is a strict allowlist. If you set `tools: ["read", "bash"]`, extension tools such as `ast-grep`, `mgrep`, `fffind`, or `ffgrep` are hidden from that subagent. Add `toolHints: ["search"]` to dynamically expose installed grep/find/search-like tools while keeping the concrete base allowlist portable. Use `toolHints: ["external-search"]` for installed web search, browsing, and URL-extraction tools; `requireToolHints: true` fails before prompting unless each requested capability matched. The built-in advisory workflows use `tools: ["read", "bash", "grep", "find", "ls"]` plus `toolHints: ["search"]`.
|
|
425
474
|
|
|
475
|
+
Subagents are SDK sessions, so pi's built-in codemode and MCP extensions do not load in them. Two opt-ins cover that:
|
|
476
|
+
|
|
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.
|
|
479
|
+
|
|
426
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.
|
|
427
481
|
|
|
428
482
|
Set `model` only when a subagent should use a specific model. Bare ids keep the Anthropic shorthand; `provider/id` targets built-in, custom, or local providers. Explicit models override a requested profile; calls with neither inherit the host/session default. Malformed or unknown explicit refs fail fast.
|
|
@@ -450,16 +504,36 @@ Cleanup is a required finalizer. The engine attempts every registered worktree r
|
|
|
450
504
|
|
|
451
505
|
### Compose workflows
|
|
452
506
|
|
|
453
|
-
`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.
|
|
454
508
|
|
|
455
509
|
```ts
|
|
456
510
|
export default async function run({ workflow }: WorkflowApi) {
|
|
457
511
|
const review = await workflow("code-review", "HEAD~3");
|
|
458
|
-
|
|
512
|
+
const audit = await workflow({ scriptPath: "scripts/audit.js" }, { paths: ["src/"], strict: true });
|
|
513
|
+
return { review, audit };
|
|
459
514
|
}
|
|
460
515
|
```
|
|
461
516
|
|
|
462
|
-
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.
|
|
518
|
+
|
|
519
|
+
### Classify between stages
|
|
520
|
+
|
|
521
|
+
`classify(context, opts?)` answers typed `choice`, `score`, or `bool` questions about JSON `state` with one of pi's classifier models (for example TypeSafe Jev or Cloudflare Clef), using the host's credentials and no chat session. It suits cheap gates between stages, such as deduplicating findings or deciding which items need a follow-up agent.
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
const answers = await api.classify(
|
|
525
|
+
{
|
|
526
|
+
state: { finding },
|
|
527
|
+
questions: {
|
|
528
|
+
followUp: { type: "bool", instructions: "Does this finding need verification?", criteria: { true: "Plausible but unproven", false: "Clearly settled" } },
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{ label: `triage:${finding.id}` },
|
|
532
|
+
);
|
|
533
|
+
if (answers.followUp?.type === "bool" && answers.followUp.probability > 0.5) { /* verify it */ }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Without `opts.model` ("provider/id"), the first classifier with working credentials is used. Calls share the run's concurrency cap, count toward usage and the budget, and are not journaled, so a resumed run classifies again. When no classifier is configured the call rejects with a recoverable error, so guard it or let `parallel()` turn it into `null`.
|
|
463
537
|
|
|
464
538
|
### Where workflows live
|
|
465
539
|
|
|
@@ -469,12 +543,19 @@ Guaranteed built-ins are statically registered:
|
|
|
469
543
|
2. Import it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`.
|
|
470
544
|
3. Add a `defineBuiltinWorkflow(mod, "<name>.ts")` entry to `BUILTIN_WORKFLOW_DEFINITIONS`.
|
|
471
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
|
+
|
|
472
553
|
Drop-in workflows are also discovered best-effort from:
|
|
473
554
|
|
|
474
555
|
- `~/.pi/agent/workflows/*.ts`: the user-level drop-in directory, which survives `pi update`.
|
|
475
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`.
|
|
476
557
|
|
|
477
|
-
|
|
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.
|
|
478
559
|
|
|
479
560
|
## Author an inline workflow
|
|
480
561
|
|
|
@@ -501,6 +582,7 @@ Inline rules:
|
|
|
501
582
|
|
|
502
583
|
- start with `export const meta = { ... }`;
|
|
503
584
|
- default-export an async workflow function;
|
|
585
|
+
- write plain JavaScript: the script compiles in-process, so TypeScript annotations do not parse;
|
|
504
586
|
- no imports or dynamic `import()`;
|
|
505
587
|
- use the injected `Type` object for schemas;
|
|
506
588
|
- request `profile: "small"`, `"medium"`, or `"big"` instead of hard-coding provider models when the stage is portable;
|
|
@@ -509,6 +591,10 @@ Inline rules:
|
|
|
509
591
|
|
|
510
592
|
Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
|
|
511
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
|
+
|
|
512
598
|
## Runtime knobs
|
|
513
599
|
|
|
514
600
|
Only tune these when a workflow is too slow, too expensive, or too noisy:
|
|
@@ -517,7 +603,7 @@ Only tune these when a workflow is too slow, too expensive, or too noisy:
|
|
|
517
603
|
| --- | --- |
|
|
518
604
|
| `--concurrency=N` / `PI_WORKFLOW_CONCURRENCY=N` | Cap concurrent subagents. Default is `min(8, max(2, CPU count))`. |
|
|
519
605
|
| `--parallel-limit=N` / `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` | Limit eager `parallel()` submission. |
|
|
520
|
-
| `--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. |
|
|
521
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`. |
|
|
522
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`. |
|
|
523
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. |
|
|
@@ -533,6 +619,38 @@ and abort scope. Usage from failed attempts is retained, so it can exhaust the
|
|
|
533
619
|
budget before the next retry begins. Host cancellation takes precedence over
|
|
534
620
|
limit and timeout failures.
|
|
535
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
|
+
|
|
536
654
|
Transient provider failures are retried in two layers. First, each subagent
|
|
537
655
|
inherits pi's session-level automatic retry from your pi `retry` settings
|
|
538
656
|
(enabled by default: 3 retries with 2s, 4s, 8s backoff). It retries the failed
|
|
@@ -575,6 +693,32 @@ fails, the result retains verified records and reports incomplete coverage.
|
|
|
575
693
|
Synthesis selects or merges IDs; the workflow reconstructs evidence-bearing
|
|
576
694
|
fields from those records.
|
|
577
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
|
+
|
|
703
|
+
### Duplicate candidates
|
|
704
|
+
|
|
705
|
+
Lenses often report the same defect in different words. When a TypeSafe Jev
|
|
706
|
+
classifier is reachable (TypeSafe directly, OpenRouter, Cloudflare Workers AI,
|
|
707
|
+
Vercel AI Gateway, or OpenCode Zen), the four advisory workflows ask it whether
|
|
708
|
+
each pair of candidates in the same file describes one defect. Before
|
|
709
|
+
verification, a candidate joins a group only when the classifier is at least 80%
|
|
710
|
+
sure it matches every member, so each defect gets one verifier. Merged
|
|
711
|
+
candidates keep all of their source IDs, locations, impacts, and evidence, and
|
|
712
|
+
the verifier sees the other wordings as "Also reported as" evidence. If the
|
|
713
|
+
first comparison fails (no Jev credentials, a bad key, a rate limit), the merge
|
|
714
|
+
is skipped.
|
|
715
|
+
|
|
716
|
+
Measured on GPT-6.1 Sol code reviews with planted bugs, the merge cut verifier
|
|
717
|
+
agents and cost by about 40% (e.g. 29 → 18 agents, $0.60 → $0.37 list price on
|
|
718
|
+
a small diff; 22 → 14 agents, $1.74 → $0.89 on a 160 KB diff). In every
|
|
719
|
+
instrumented run, each planted bug that a finder proposed reached the final
|
|
720
|
+
report, with or without merging.
|
|
721
|
+
|
|
578
722
|
### Selective adversarial challenges
|
|
579
723
|
|
|
580
724
|
Add `--challenge` to an advisory workflow to challenge up to three uncertain or
|