pi-dynamic-workflow 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 milanglacier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # pi-dynamic-workflow
2
+
3
+ A [pi](https://github.com/badlogic/pi-mono) extension that lets the LLM author a JavaScript orchestration script and fan it out into isolated `pi` subagent subprocesses.
4
+
5
+ > `/workflow <task>` — author a multi-agent workflow on the fly and run it.
6
+
7
+ The session LLM receives an authoring brief, writes a small async JS body that calls `agent(...)` to spawn fully-isolated subagents (each with its own context window, in its own subprocess), and composes them with `parallel()` / `pipeline()`. Subagent failures become `null` rather than rejecting the batch, so scripts can `.filter(Boolean)` and keep going. The script's return value becomes the tool's result.
8
+
9
+ ## Install
10
+
11
+ Install from npm with pi's package manager:
12
+
13
+ ```bash
14
+ pi install npm:pi-dynamic-workflow
15
+ ```
16
+
17
+ Or install directly from the GitHub repository:
18
+
19
+ ```bash
20
+ pi install git:github.com/milanglacier/pi-dynamic-workflow
21
+ ```
22
+
23
+ For local development, drop the path into your `~/.pi/agent/settings.json`:
24
+
25
+ ```json
26
+ {
27
+ "packages": ["/path/to/pi-dynamic-workflow"]
28
+ }
29
+ ```
30
+
31
+ ## User Guide
32
+
33
+ ### When to reach for `/workflow`
34
+
35
+ `/workflow` shines when a task benefits from **independent parallel work** or **multi-stage pipelines with quality gates**. Good fits:
36
+
37
+ - **Auditing or reviewing** across multiple files, lenses, or criteria (security, correctness, style).
38
+ - **Batch transformations** where each file/item flows through the same pipeline (scan → fix → verify).
39
+ - **Research or comparison** that needs multiple perspectives synthesized by a judge.
40
+ - **Any task where you'd otherwise say** "do the same thing to each of these" or "check my work from a different angle."
41
+
42
+ Not every task needs an orchestra. A single-file edit or a straightforward question is better handled directly — the overhead of spawning subagents isn't worth it for a one-step job.
43
+
44
+ ### Prompting the agent
45
+
46
+ The LLM decides what script to write and how to structure the workflow. You steer it with natural language in your `/workflow` prompt. Here are the levers you can pull:
47
+
48
+ #### Pick a quality pattern
49
+
50
+ Tell the agent which pattern to use by name:
51
+
52
+ ```
53
+ /workflow review src/auth.ts using the adversarial verify pattern
54
+ /workflow audit the API layer — use a judge panel with 3 reviewers
55
+ /workflow scan the codebase for bugs — multi-angle sweep: correctness, security, and performance
56
+ ```
57
+
58
+ The agent knows these patterns from its authoring brief. If you don't name one, it picks what fits the task. See the [Quality patterns](#quality-patterns) reference for what each one does.
59
+
60
+ #### Set a spending cap
61
+
62
+ ```
63
+ /workflow audit every module — cap spending at $2
64
+ /workflow review the entire codebase with a budget of 50k tokens
65
+ ```
66
+
67
+ This sets `maxCost` or `maxTokens` on the tool call. The script gets a `budget` object it can check — once exceeded, new `agent()` calls throw a `BudgetExceededError`. In-flight agents still finish, so you get partial results rather than nothing.
68
+
69
+ #### Control concurrency
70
+
71
+ ```
72
+ /workflow review all 40 services — limit to 4 at a time
73
+ ```
74
+
75
+ Sets `maxConcurrency`. Useful when you're hitting rate limits or don't want to saturate your machine. The default is `max(2, min(8, cpus - 2))`.
76
+
77
+ #### Run in the background
78
+
79
+ ```
80
+ /workflow audit the entire monorepo — run this in the background
81
+ ```
82
+
83
+ Sets `background: true`. The tool returns immediately and the workflow keeps running detached. A notification message arrives when it finishes (triggering a new turn). Stop it early with `/workflow-stop <runId>` (no argument lists active runs).
84
+
85
+ #### Use a custom agent type
86
+
87
+ If you've defined agent types (see [Configuring agent types](#configuring-agent-types)), ask the LLM to use them:
88
+
89
+ ```
90
+ /workflow review the PR — use my security-reviewer and style-checker agent types
91
+ ```
92
+
93
+ The LLM will set `agentType` on the relevant `agent()` calls. Each agent type brings its own system prompt, default model, and tool restrictions.
94
+
95
+ #### Restrict what subagents can do
96
+
97
+ ```
98
+ /workflow audit the codebase — all subagents should be read-only
99
+ ```
100
+
101
+ The LLM will set `tools: ["read", "grep", "find", "ls"]` on `agent()` calls so subagents can't modify files.
102
+
103
+ #### Resume a previous run
104
+
105
+ If a run crashed or you want to re-run with small changes, ask the agent to resume:
106
+
107
+ ```
108
+ /workflow re-run my last audit but add a performance lens — resume from the previous run
109
+ ```
110
+
111
+ Changed prompts re-run; unchanged ones replay from cache instantly (free, marked with ↺). See [Resume](#resume) for how this works under the hood.
112
+
113
+ #### Compose with saved workflows
114
+
115
+ ```
116
+ /workflow pipeline: first run my review-sweep on src/, then aggregate the findings
117
+ ```
118
+
119
+ If you have saved workflows (see [Creating saved workflows](#creating-saved-workflows)), the agent can compose them inline.
120
+
121
+ ### Quick reference: all the knobs
122
+
123
+ | What you want | How to say it in a `/workflow` prompt |
124
+ | ----------------------------------- | --------------------------------------------------------------------------- |
125
+ | Use a quality pattern | `... using the judge panel pattern` |
126
+ | Cap spending | `... with a $3 budget` or `... cap at 100k tokens` |
127
+ | Limit parallelism | `... limit to 4 concurrent agents` |
128
+ | Run detached | `... in the background` |
129
+ | Use a custom agent type | `... use my security-reviewer` |
130
+ | Read-only subagents | `... all subagents should be read-only` |
131
+ | Resume a prior run | `... resume from the previous run` |
132
+ | Compose saved workflows | `... first run my review-sweep, then aggregate` |
133
+ | Set per-agent timeout | `... give each subagent at most 2 minutes` |
134
+ | Structured output from subagents | `... each subagent should return JSON with {severity, summary}` |
135
+
136
+ ### Configuring agent types
137
+
138
+ Agent types are reusable subagent profiles — like preset characters with their own system prompt, default model, and tool set. Define them as markdown files:
139
+
140
+ **Location:** `~/.pi/agent/agents/*.md` (global) or `<project>/.pi/agents/*.md` (per-project; overrides global on name collision).
141
+
142
+ **Format:** YAML frontmatter for metadata; the body is the system prompt.
143
+
144
+ ```markdown
145
+ ---
146
+ name: security-reviewer
147
+ description: Reviews code for security issues with an adversarial mindset
148
+ tools: read, grep, find, ls
149
+ model: sonnet
150
+ ---
151
+ You are a security reviewer. For every file you examine:
152
+ 1. Identify all trust boundaries (user input, network, filesystem, DB).
153
+ 2. Trace data flow across each boundary.
154
+ 3. Flag any missing validation, sanitization, or authorization.
155
+ 4. Suggest concrete fixes with code examples.
156
+
157
+ Be concise. Your final message should be a bulleted list of findings
158
+ with severity (critical/high/medium/low) and a one-line recommendation.
159
+ ```
160
+
161
+ **Frontmatter fields:**
162
+
163
+ | Field | Required | Description |
164
+ | ------------- | -------- | ---------------------------------------------------------------------------- |
165
+ | `name` | Yes | Identifier used in `agentType` (kebab-case recommended) |
166
+ | `description` | Yes | Shown in the `/workflow` brief so the LLM knows when to reach for it |
167
+ | `tools` | No | Comma-separated tool list (e.g. `read, grep, find`). Omit to grant all tools |
168
+ | `model` | No | Model override for this agent type (e.g. `haiku`, `sonnet`) |
169
+
170
+ Once defined, the LLM can use them via `agent("...", { agentType: "security-reviewer" })`. Changing an agent type's `.md` file invalidates cached results for runs that used it — so resume will re-run those calls live.
171
+
172
+ ### Creating saved workflows
173
+
174
+ If you find yourself repeating the same orchestration pattern, save it as a reusable script:
175
+
176
+ **Location:** `~/.pi/agent/workflows/*.js` (global) or `<project>/.pi/workflows/*.js` (per-project; overrides global on name collision).
177
+
178
+ **Format:** A `.js` file whose name is the workflow name. The first line, if a `//` comment, is its description (shown in the `/workflow` brief). The body is the same async JS you'd write inline — `agent()`, `parallel()`, `pipeline()`, `args`, etc. are all in scope.
179
+
180
+ ```js
181
+ // Scan every file in a directory for a specific issue and report findings
182
+ const files = await agent(
183
+ `List every .ts file under ${args.dir || "src/"}. Output one path per line.`,
184
+ { label: "list-files", tools: ["find"], schema: { type: "array", items: { type: "string" } } }
185
+ );
186
+ if (!files || files.length === 0) return { findings: [], message: "No files found" };
187
+
188
+ const results = await parallel(
189
+ files.map(f => () =>
190
+ agent(`Scan ${f} for ${args.issue || "hardcoded secrets"}. Report findings concisely.`,
191
+ { label: `scan-${f}`, phase: "scan", tools: ["read", "grep"] }
192
+ )
193
+ )
194
+ );
195
+
196
+ const findings = results.filter(Boolean);
197
+ return { count: findings.length, findings };
198
+ ```
199
+
200
+ **Usage:** Reference it from a prompt (`/workflow run my scan-sweep on src/auth`) or compose it inline from another script (`workflow("scan-sweep", { dir: "src/auth", issue: "SQL injection" })`).
201
+
202
+ ## Reference
203
+
204
+ ### `/workflow` command
205
+
206
+ ```
207
+ /workflow review the auth module for security issues across 3 lenses
208
+ ```
209
+
210
+ This injects the authoring brief into the next turn. The LLM plans phases, calls the `workflow` tool with a `script` parameter, and the orchestration runs.
211
+
212
+ ### The `workflow` tool
213
+
214
+ | Parameter | Type | Description |
215
+ | ----------------- | ----------- | ------------------------------------------------------------------------------- |
216
+ | `name` | `string` | Short kebab-case workflow name |
217
+ | `description` | `string` | One-sentence description |
218
+ | `phases` | `string[]?` | Optional ordered phase titles (documentation of the plan) |
219
+ | `script` | `string?` | Plain async JS body (top-level `await` / `return` allowed) |
220
+ | `workflowName` | `string?` | Run a saved workflow by name instead of an inline script |
221
+ | `scriptPath` | `string?` | Run a workflow script from a `.js` file (relative to the session cwd) |
222
+ | `args` | `any?` | JSON value exposed to the script as `args` |
223
+ | `maxConcurrency` | `number?` | Cap on concurrent subagents (default `max(2, min(8, cpus - 2))`) |
224
+ | `maxAgents` | `number?` | Cap on total `agent()` calls per run (default and hard max 200) |
225
+ | `maxCost` | `number?` | Budget in USD; once spent, further `agent()` calls throw a `BudgetExceededError` (soft cap) |
226
+ | `maxTokens` | `number?` | Budget in tokens (input+output); same enforcement as `maxCost` |
227
+ | `resumeFromRunId` | `string?` | Replay a prior run's journal; unchanged `agent()` calls return cached results |
228
+ | `background` | `boolean?` | Return immediately; a `workflow-complete` message arrives when the run finishes |
229
+
230
+ Provide exactly one of `script`, `workflowName`, or `scriptPath`.
231
+
232
+ ### Script API
233
+
234
+ ```js
235
+ // Spawn an isolated subagent. Resolves to its final text, or null on failure.
236
+ const text = await agent("Summarize src/foo.ts", { label: "summarize-foo" });
237
+
238
+ // Structured output: pass a JSON Schema; resolves to a matching object (or null).
239
+ const info = await agent("Count the exported functions in src/foo.ts", {
240
+ label: "count-foo",
241
+ schema: {
242
+ type: "object",
243
+ properties: { count: { type: "number" } },
244
+ required: ["count"],
245
+ },
246
+ });
247
+
248
+ // Per-agent options: label, phase, model, tools (e.g. ["read","grep"]),
249
+ // cwd (working dir relative to the session), schema (JSON Schema),
250
+ // timeout (ms; kills the subagent and resolves null), systemPrompt,
251
+ // appendSystemPrompt, and agentType — a saved agent definition from
252
+ // ~/.pi/agent/agents/*.md or <project>/.pi/agents/*.md (frontmatter
253
+ // name/description/tools/model; body = system prompt).
254
+ const verdict = await agent("Review src/auth.ts", { agentType: "security-reviewer", timeout: 120000 });
255
+
256
+ // Run thunks concurrently. Failures become null; the batch never rejects.
257
+ const results = await parallel(files.map(f => () => agent(`Review ${f}`, { label: f })));
258
+
259
+ // Per-item pipeline: each item flows through all stages independently
260
+ // (no cross-item barrier). A throwing stage drops that item to null.
261
+ const fixed = await pipeline(files,
262
+ (_, f) => agent(`Find bugs in ${f}`, { label: `scan ${f}`, phase: "scan" }),
263
+ (bugs, f) => bugs?.includes("BUG")
264
+ ? agent(`Fix these bugs in ${f}: ${bugs}`, { phase: "fix" })
265
+ : bugs,
266
+ );
267
+
268
+ phase("aggregate"); // set the default phase for subsequent agent() calls
269
+ log("merging results"); // progress note shown in the TUI
270
+ const inputs = fixed.filter(Boolean); // always drop nulls before aggregating
271
+
272
+ // budget reflects the tool's maxCost/maxTokens caps.
273
+ if (budget.exceeded()) return { partial: inputs.length };
274
+
275
+ // Compose a saved workflow inline (one nesting level; shares this run's
276
+ // concurrency, abort, budget, and agent accounting).
277
+ const sub = await workflow("review-sweep", { target: "src/auth" });
278
+
279
+ return { summary: inputs.length }; // return value becomes the tool result
280
+ ```
281
+
282
+ Scripts must be deterministic so runs can resume: `Date.now()`, `Math.random()`, and
283
+ zero-arg `new Date()` throw in the sandbox — pass timestamps or seeds in via `args`.
284
+
285
+ ### Saved workflows (technical)
286
+
287
+ Saved workflows are discovered from `~/.pi/agent/workflows/*.js` (user) and `<project>/.pi/workflows/*.js` (project; overrides user on name collision). The file name (minus `.js`) is the workflow name; a leading `//` comment is its description. Run them via the `workflowName` tool parameter or compose them from a script with `workflow(name, args)`. See [Creating saved workflows](#creating-saved-workflows) for a step-by-step example.
288
+
289
+ ### Resume
290
+
291
+ Every run writes an append-only journal to `~/.pi/agent/pi-dynamic-workflow/runs/<runId>.jsonl`
292
+ (successful agent calls only, keyed by a hash of prompt + behavioral options — including
293
+ the resolved `agentType` definition, so editing an agent `.md` invalidates its cached
294
+ calls; the 50 newest runs are kept). Re-invoking the tool with `resumeFromRunId` replays
295
+ matching calls from cache — shown with a `↺` icon, free of budget — and only runs what
296
+ changed.
297
+
298
+ ### Background runs
299
+
300
+ `background: true` makes the tool return immediately with the run id; the run continues
301
+ detached and injects a `workflow-complete` message (triggering a turn) when it finishes.
302
+ Stop one early with `/workflow-stop <runId>` (no argument lists active runs). Background
303
+ runs die with the pi process, but their journal makes them resumable.
304
+
305
+ ### Design rules
306
+
307
+ These are the rules the LLM follows when authoring workflow scripts. They're also a good mental model for understanding how workflows behave.
308
+
309
+ 1. **Pipeline by default.** Only add a barrier (sequential `parallel` batches) when a stage genuinely needs to see all items at once (e.g. cross-file dedup, global ranking). Independent per-item work should flow through `pipeline` so fast items don't wait for slow ones.
310
+ 2. **Null-filter religiously.** `agent()`, `parallel()`, and `pipeline()` all yield `null` for failures. `.filter(Boolean)` before joining or aggregating.
311
+ 3. **Keep prompts self-contained.** Subagents share nothing with you or each other. Include file paths, acceptance criteria, and output format in every prompt. Tell agents to be concise — their final message is the return value.
312
+ 4. **Use `schema` when you need machine-readable output** (counts, verdicts, lists). Plain text is fine for prose to be aggregated by another agent.
313
+ 5. **Scale to what the user asked for.** A two-step task needs two agents, not a judge panel. Reserve heavy patterns for tasks that demand rigor.
314
+ 6. **Concurrency is capped** (default `max(2, min(8, cpus - 2))`); you may launch many agents and let the scheduler queue them. Set `maxConcurrency` lower for heavy tasks.
315
+ 7. **Restrict tools** for read-only analysis agents (`tools: ["read","grep","find","ls"]`) so they cannot mutate the repo.
316
+ 8. **Set `timeout` on agents that could wander** and `maxCost`/`maxTokens` on expensive fan-outs; a timed-out agent resolves to null like any other failure.
317
+ 9. **Use `background: true` for long runs** the user shouldn't wait on; report the run id so it can be stopped (`/workflow-stop`) or resumed later.
318
+
319
+ ### Quality patterns
320
+
321
+ Patterns the LLM can use when a task demands rigor. Name them in your prompt to steer the agent (see [Prompting the agent](#prompting-the-agent)).
322
+
323
+ - **Adversarial verify:** producer agent creates, verifier agent (different prompt, fresh context) checks against explicit criteria; loop or fix on failure.
324
+ - **Loop until clean:** `while` a checker agent reports issues (bounded iterations, e.g. 3), run a fixer agent on the report.
325
+ - **Judge panel:** N agents answer independently, one judge agent compares and synthesizes — good for ambiguous questions.
326
+ - **Multi-angle sweep:** fan out agents with different lenses (correctness, security, performance) over the same target, then merge.
327
+
328
+ ## Safety
329
+
330
+ The script is the session LLM's, not untrusted user input. The `node:vm` sandbox provides isolation hygiene, not a security boundary — treat it as an execution context for code the same model wrote, not a jail.
331
+
332
+ To prevent the obvious failure mode (a script that spawns subagents that spawn workflows that spawn…), nested subagent depth is capped at 3, total `agent()` calls per run at 200, and in-script `workflow()` composition at one nesting level.
333
+
334
+ ## Development
335
+
336
+ ```bash
337
+ npm install
338
+ npm run typecheck # tsc --noEmit
339
+ npm test # node --test "test/*.test.ts"
340
+ ```
341
+
342
+ Tests use zero deps — `node --test` with native TypeScript type stripping, and route all subprocess spawning through a fake `pi` stub so they never invoke the real binary.
343
+
344
+ ## License
345
+
346
+ [MIT](LICENSE).
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "pi-dynamic-workflow",
3
+ "version": "0.1.0",
4
+ "description": "Dynamic workflow orchestration for pi: the LLM authors a JS orchestration script that fans out isolated pi subagent sessions",
5
+ "type": "module",
6
+ "author": {
7
+ "name": "milanglacier",
8
+ "url": "https://github.com/milanglacier"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/milanglacier/pi-dynamic-workflow#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/milanglacier/pi-dynamic-workflow.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/milanglacier/pi-dynamic-workflow/issues"
18
+ },
19
+ "keywords": [
20
+ "pi-package",
21
+ "pi",
22
+ "pi-coding-agent",
23
+ "pi-plugins",
24
+ "multi-agent",
25
+ "workflow",
26
+ "orchestration",
27
+ "ai-agents"
28
+ ],
29
+ "files": [
30
+ "src"
31
+ ],
32
+ "pi": {
33
+ "extensions": ["./src/index.ts"]
34
+ },
35
+ "scripts": {
36
+ "prepublishOnly": "npm run typecheck",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "node --test \"test/*.test.ts\""
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-agent-core": "*",
46
+ "@earendil-works/pi-ai": "*",
47
+ "@earendil-works/pi-coding-agent": "*",
48
+ "@earendil-works/pi-tui": "*",
49
+ "typebox": "*"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^24.0.0",
53
+ "typescript": "^5.8.0"
54
+ }
55
+ }
package/src/agents.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Discovery of named agent definitions following pi's agents convention:
3
+ * `~/.pi/agent/agents/*.md` (user) and the nearest `<project>/.pi/agents/*.md`
4
+ * (project, overrides user on name collision). Frontmatter: name, description,
5
+ * tools (comma-separated), model; the markdown body is the system prompt.
6
+ *
7
+ * Adapted from the official subagent example's discovery code ONLY — its pi
8
+ * invocation logic (`getPiInvocation`) is intentionally not copied; see
9
+ * CLAUDE.md and src/subagent.ts for why.
10
+ */
11
+
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
15
+
16
+ export interface AgentTypeConfig {
17
+ name: string;
18
+ description: string;
19
+ tools?: string[];
20
+ model?: string;
21
+ systemPrompt: string;
22
+ source: "user" | "project";
23
+ filePath: string;
24
+ }
25
+
26
+ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentTypeConfig[] {
27
+ const agents: AgentTypeConfig[] = [];
28
+ let entries: fs.Dirent[];
29
+ try {
30
+ entries = fs.readdirSync(dir, { withFileTypes: true });
31
+ } catch {
32
+ return agents;
33
+ }
34
+
35
+ for (const entry of entries) {
36
+ if (!entry.name.endsWith(".md")) continue;
37
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
38
+
39
+ const filePath = path.join(dir, entry.name);
40
+ let content: string;
41
+ try {
42
+ content = fs.readFileSync(filePath, "utf-8");
43
+ } catch {
44
+ continue;
45
+ }
46
+
47
+ const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
48
+ if (!frontmatter.name || !frontmatter.description) continue;
49
+
50
+ const tools = frontmatter.tools
51
+ ?.split(",")
52
+ .map((t: string) => t.trim())
53
+ .filter(Boolean);
54
+
55
+ agents.push({
56
+ name: frontmatter.name,
57
+ description: frontmatter.description,
58
+ ...(tools && tools.length > 0 ? { tools } : {}),
59
+ ...(frontmatter.model ? { model: frontmatter.model } : {}),
60
+ systemPrompt: body,
61
+ source,
62
+ filePath,
63
+ });
64
+ }
65
+
66
+ return agents;
67
+ }
68
+
69
+ function findNearestProjectAgentsDir(cwd: string): string | null {
70
+ let currentDir = cwd;
71
+ while (true) {
72
+ const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
73
+ try {
74
+ if (fs.statSync(candidate).isDirectory()) return candidate;
75
+ } catch {
76
+ // keep walking up
77
+ }
78
+ const parentDir = path.dirname(currentDir);
79
+ if (parentDir === currentDir) return null;
80
+ currentDir = parentDir;
81
+ }
82
+ }
83
+
84
+ /** Discover agent definitions; project entries override user entries by name. */
85
+ export function discoverAgentTypes(cwd: string): Map<string, AgentTypeConfig> {
86
+ const map = new Map<string, AgentTypeConfig>();
87
+ for (const agent of loadAgentsFromDir(path.join(getAgentDir(), "agents"), "user")) {
88
+ map.set(agent.name, agent);
89
+ }
90
+ const projectDir = findNearestProjectAgentsDir(cwd);
91
+ if (projectDir) {
92
+ for (const agent of loadAgentsFromDir(projectDir, "project")) {
93
+ map.set(agent.name, agent);
94
+ }
95
+ }
96
+ return map;
97
+ }
package/src/guide.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Workflow authoring guide injected by the /workflow command, plus the
3
+ * condensed description used on the `workflow` tool itself.
4
+ *
5
+ * NOTE: README.md mirrors the Script API and Design rules sections below.
6
+ * When editing either, keep the other in sync.
7
+ */
8
+
9
+ export const WORKFLOW_TOOL_DESCRIPTION = [
10
+ "Execute a deterministic multi-agent workflow you author as a JavaScript script body.",
11
+ "The script runs in a sandbox with these hooks in scope:",
12
+ "agent(prompt, {label?, phase?, model?, tools?, cwd?, schema?, timeout?, systemPrompt?, appendSystemPrompt?, agentType?}) spawns an isolated pi subagent and resolves to its final text (or a structured object matching `schema`, a JSON Schema); resolves to null on failure or timeout. agentType names a saved agent definition (.pi/agents/*.md).",
13
+ "parallel([...thunks]) runs thunks concurrently; failed thunks become null (never rejects).",
14
+ "pipeline(items, ...stages) flows each item through the stages independently with no cross-item barrier; a throwing stage drops that item to null.",
15
+ "phase(title) groups subsequent agent() calls under a phase; log(msg) records progress; args is the tool's `args` parameter.",
16
+ 'budget ({maxCost, maxTokens, spentCost(), spentTokens(), remainingCost(), remainingTokens(), exceeded()}) reflects the maxCost/maxTokens caps; once exceeded, agent() throws a BudgetExceededError (catch via e.name === "BudgetExceededError").',
17
+ "workflow(nameOrPath, args) runs a saved workflow inline (one nesting level).",
18
+ "Top-level await and return are allowed; the script's return value becomes the tool result.",
19
+ "Scripts must be deterministic: Date.now(), Math.random(), and zero-arg new Date() throw — pass timestamps/seeds via args. Every run is journaled under a run id; pass resumeFromRunId to replay unchanged agent() calls from cache. Set background: true to return immediately and get a workflow-complete message when done.",
20
+ "Instead of `script`, you may pass workflowName (a saved workflow) or scriptPath (a .js file).",
21
+ "Prefer pipeline over staged parallel batches unless a stage truly needs cross-item context. Filter nulls before aggregation.",
22
+ ].join(" ");
23
+
24
+ export const WORKFLOW_GUIDE = `# Dynamic Workflow Brief
25
+
26
+ You have access to a \`workflow\` tool that executes a JavaScript orchestration script you write. Each \`agent()\` call spawns a fully isolated pi subagent (own context window, subprocess). Use it to decompose the user's task into deterministic multi-agent control flow.
27
+
28
+ ## Tool parameters
29
+
30
+ - \`name\`: short workflow name (kebab-case)
31
+ - \`description\`: one sentence describing what it does
32
+ - \`phases\`: optional list of phase titles (documentation of the plan)
33
+ - \`script\`: plain async JavaScript body (NOT a function declaration; top-level \`await\` and \`return\` work)
34
+ - \`workflowName\` / \`scriptPath\`: run a saved workflow (from \`~/.pi/agent/workflows/\` or \`<project>/.pi/workflows/\`) or a \`.js\` file instead of an inline script (provide exactly one of the three)
35
+ - \`args\`: optional JSON value available to the script as \`args\`
36
+ - \`maxConcurrency\`: optional cap on concurrent subagents
37
+ - \`maxAgents\`: optional cap on total \`agent()\` calls (default 200)
38
+ - \`maxCost\` / \`maxTokens\`: optional budget; once total subagent spend reaches it, further \`agent()\` calls throw a \`BudgetExceededError\` — catch via \`e.name === "BudgetExceededError"\` to return partial results (in-flight agents finish — a soft cap)
39
+ - \`resumeFromRunId\`: replay a prior run's journal — \`agent()\` calls whose prompt+options match return the recorded result instantly; new or changed calls run live. Every result reports its run id.
40
+ - \`background\`: return immediately and run detached; a \`workflow-complete\` message arrives when the run finishes (stop early with \`/workflow-stop <runId>\`)
41
+
42
+ ## Script API
43
+
44
+ \`\`\`js
45
+ // Spawn an isolated subagent. Resolves to its final text, or null on failure.
46
+ const text = await agent("Summarize src/foo.ts", { label: "summarize-foo" });
47
+
48
+ // Structured output: pass a JSON Schema; resolves to a matching object (or null).
49
+ const info = await agent("Count the exported functions in src/foo.ts", {
50
+ label: "count-foo",
51
+ schema: { type: "object", properties: { count: { type: "number" } }, required: ["count"] },
52
+ });
53
+
54
+ // Options: label (display name), phase (group), model, tools (e.g. ["read","grep"]),
55
+ // cwd (working dir), schema (JSON Schema for structured output), timeout (ms; kills
56
+ // the subagent and resolves null), systemPrompt / appendSystemPrompt, and agentType
57
+ // (a saved agent definition from ~/.pi/agent/agents/*.md or <project>/.pi/agents/*.md
58
+ // supplying its system prompt plus default tools/model).
59
+ const verdict = await agent("Review src/auth.ts for injection bugs", {
60
+ agentType: "security-reviewer",
61
+ timeout: 120000,
62
+ });
63
+
64
+ // Run thunks concurrently. Failures become null; the batch never rejects.
65
+ const results = await parallel(files.map(f => () => agent(\`Review \${f}\`, { label: f })));
66
+
67
+ // Per-item pipeline: each item flows through all stages independently (no barrier).
68
+ // Stage signature: (prevResult, originalItem, index). A throwing stage drops the item to null.
69
+ const fixed = await pipeline(files,
70
+ (_, f) => agent(\`Find bugs in \${f}\`, { label: \`scan \${f}\`, phase: "scan" }),
71
+ (bugs, f) => bugs && bugs.includes("BUG") ? agent(\`Fix these bugs in \${f}: \${bugs}\`, { phase: "fix" }) : bugs,
72
+ );
73
+
74
+ phase("aggregate"); // set the default phase for subsequent agent() calls
75
+ log("merging results"); // progress note shown in the TUI
76
+ const inputs = fixed.filter(Boolean); // always drop nulls before aggregating
77
+
78
+ // Budget-aware loop: budget reflects the tool's maxCost/maxTokens caps.
79
+ while (budget.maxCost !== null && budget.remainingCost() > 0.05 && inputs.length < 10) {
80
+ const more = await agent("Find one more edge case in src/", { tools: ["read","grep"] });
81
+ if (more) inputs.push(more);
82
+ }
83
+
84
+ // Compose a saved workflow inline (one nesting level; shares this run's
85
+ // concurrency, abort, budget, and agent accounting).
86
+ const subResult = await workflow("review-sweep", { target: "src/auth" });
87
+
88
+ return { summary: inputs.length }; // return value becomes the tool result
89
+ \`\`\`
90
+
91
+ Scripts must be deterministic so runs can resume: \`Date.now()\`, \`Math.random()\`, and
92
+ zero-arg \`new Date()\` throw in the sandbox — pass timestamps or seeds in via \`args\`.
93
+ Every run is journaled; re-invoking the tool with \`resumeFromRunId\` set to a prior
94
+ run id replays unchanged \`agent()\` calls from cache and only re-runs what changed.
95
+
96
+ ## Design rules
97
+
98
+ 1. **Pipeline by default.** Only add a barrier (sequential \`parallel\` batches) when a stage genuinely needs to see all items at once (e.g. cross-file dedup, global ranking). Independent per-item work should flow through \`pipeline\` so fast items don't wait for slow ones.
99
+ 2. **Null-filter religiously.** \`agent()\`, \`parallel()\`, and \`pipeline()\` all yield \`null\` for failures. \`.filter(Boolean)\` before joining or aggregating.
100
+ 3. **Keep prompts self-contained.** Subagents share nothing with you or each other. Include file paths, acceptance criteria, and output format in every prompt. Tell agents to be concise; their final message is the return value.
101
+ 4. **Use \`schema\` when you need machine-readable output** (counts, verdicts, lists). Plain text is fine for prose to be aggregated by another agent.
102
+ 5. **Scale to what the user asked for.** A two-step task needs two agents, not a judge panel. Reserve heavy patterns for tasks that demand rigor.
103
+ 6. **Concurrency is capped** (default max(2, min(8, cpus-2))); you may launch many agents and let the scheduler queue them. Set \`maxConcurrency\` lower for heavy tasks.
104
+ 7. **Restrict tools** for read-only analysis agents (\`tools: ["read","grep","find","ls"]\`) so they cannot mutate the repo.
105
+ 8. **Set \`timeout\` on agents that could wander** and \`maxCost\`/\`maxTokens\` on expensive fan-outs; a timed-out agent resolves to null like any other failure.
106
+ 9. **Use \`background: true\` for long runs** the user shouldn't wait on; report the run id so it can be stopped (\`/workflow-stop\`) or resumed later.
107
+
108
+ ## Quality patterns (use when rigor matters)
109
+
110
+ - **Adversarial verify:** producer agent creates, verifier agent (different prompt, fresh context) checks against explicit criteria; loop or fix on failure.
111
+ - **Loop until clean:** \`while\` a checker agent reports issues (bounded iterations, e.g. 3), run a fixer agent on the report.
112
+ - **Judge panel:** N agents answer independently, one judge agent compares and synthesizes; good for ambiguous questions.
113
+ - **Multi-angle sweep:** fan out agents with different lenses (correctness, security, performance) over the same target, then merge.
114
+
115
+ Now author the workflow for the task below and call the \`workflow\` tool with it. Briefly state your phase plan first, then invoke the tool.`;