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 +21 -0
- package/README.md +346 -0
- package/package.json +55 -0
- package/src/agents.ts +97 -0
- package/src/guide.ts +115 -0
- package/src/index.ts +122 -0
- package/src/journal.ts +182 -0
- package/src/registry.ts +101 -0
- package/src/render.ts +183 -0
- package/src/sandbox.ts +128 -0
- package/src/scheduler.ts +80 -0
- package/src/structured.ts +73 -0
- package/src/subagent.ts +304 -0
- package/src/tool.ts +558 -0
- package/src/types.ts +120 -0
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.`;
|