pi-workflow-engine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +369 -0
  2. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +238 -0
  3. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +28 -0
  4. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +171 -0
  5. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +152 -0
  6. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +83 -0
  7. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -0
  8. package/{src → .pi/extensions/pi-workflow-engine/src}/engine.ts +36 -8
  9. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +396 -0
  10. package/.pi/extensions/pi-workflow-engine/src/options.ts +46 -0
  11. package/.pi/extensions/pi-workflow-engine/src/perf.ts +139 -0
  12. package/{src → .pi/extensions/pi-workflow-engine/src}/progress.ts +82 -35
  13. package/{src → .pi/extensions/pi-workflow-engine/src}/types.ts +12 -4
  14. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-format.ts +59 -6
  15. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-inspector.ts +8 -6
  16. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-result-renderer.ts +11 -8
  17. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +182 -0
  18. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +263 -0
  19. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +57 -0
  20. package/{src → .pi/extensions/pi-workflow-engine/src}/workflows.ts +8 -2
  21. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/code-review.ts +57 -125
  22. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/diagnose.ts +12 -73
  23. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/perf-review.ts +41 -148
  24. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/refactor-scout.ts +43 -151
  25. package/.pi/settings.json +3 -0
  26. package/README.md +101 -27
  27. package/USAGE.md +183 -0
  28. package/assets/preview-candidate-core.png +0 -0
  29. package/assets/preview-candidate-relic-emblem.png +0 -0
  30. package/assets/preview-candidate-robot-squad.png +0 -0
  31. package/assets/preview.png +0 -0
  32. package/package.json +33 -6
  33. package/src/agent-runner.ts +0 -122
  34. package/src/concurrency.ts +0 -55
  35. package/src/discovery.ts +0 -54
  36. package/src/index.ts +0 -141
  37. package/src/ui/workflow-widget.ts +0 -143
  38. package/workflows/ping.ts +0 -28
  39. /package/{src → .pi/extensions/pi-workflow-engine/src}/advisory-schema.ts +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # pi-workflow-engine
4
4
 
5
- Claude Code style workflows for the [pi](https://pi.dev) coding agent.
5
+ Programmable multi-agent workflows for the [pi](https://pi.dev) coding agent.
6
6
 
7
7
  This is not just a way to run a prompt. It is a way to turn an agentic procedure into code: scope the task, fork isolated subagents, fan out across review lenses, validate every handoff with schemas, verify candidate findings, and synthesize one ranked result.
8
8
 
@@ -10,7 +10,7 @@ The built-in workflows are advisory: they inspect, fan out, verify, and report.
10
10
 
11
11
  ## Why workflows matter
12
12
 
13
- Claude Code popularised a useful pattern: the best agentic coding work is not a single chat turn. It is a loop of context gathering, delegated action, verification, and synthesis. Its docs describe the [agentic loop](https://code.claude.com/docs/en/glossary#agentic-loop), [skills and commands](https://code.claude.com/docs/en/skills), and [subagents](https://code.claude.com/docs/en/glossary#subagent) as composable building blocks for repeatable engineering work.
13
+ The best agentic coding work is rarely a single chat turn. It is a repeatable loop of context gathering, delegated investigation, structured handoffs, verification, and synthesis.
14
14
 
15
15
  `pi-workflow-engine` brings that shape to pi with static TypeScript workflows:
16
16
 
@@ -35,7 +35,7 @@ Or from npm:
35
35
  pi install npm:pi-workflow-engine
36
36
  ```
37
37
 
38
- That's all. pi fetches the package and serves its core dependencies from its own bundle, so there is no clone, install, or build step. Restart pi, or run `/reload` in an open session, then confirm it is registered:
38
+ That's all. pi fetches the package and serves its core dependencies from its own bundle, so there is no clone, install, or build step. The package entrypoint is the canonical pi extension module at `.pi/extensions/pi-workflow-engine/index.ts`. Restart pi, or run `/reload` in an open session, then confirm it is registered:
39
39
 
40
40
  ```bash
41
41
  pi list
@@ -55,6 +55,8 @@ pi remove git:github.com/timbrinded/pi-workflow-engine
55
55
 
56
56
  ## Usage
57
57
 
58
+ For the complete installation, invocation, authoring, tuning, and troubleshooting guide, see [USAGE.md](USAGE.md).
59
+
58
60
  In a pi session, from inside a git repo with changes:
59
61
 
60
62
  ```text
@@ -64,15 +66,25 @@ In a pi session, from inside a git repo with changes:
64
66
  /workflow diagnose "typecheck fails after the schema change"
65
67
  /workflow perf-review "workflow startup latency"
66
68
  /workflow code-review --inspect # open the live workflow inspector
67
- /workflow ping # quick engine smoke test
68
69
  ```
69
70
 
70
- The host agent can also invoke the `workflow` tool mid-conversation:
71
+ The host agent can also invoke the `workflow` tool mid-conversation. It accepts either a registered workflow `name` or a one-off inline workflow `script`:
71
72
 
72
73
  ```text
73
74
  Run the code-review workflow on this PR and use the result before deciding what to fix.
74
75
  ```
75
76
 
77
+ Opt into dynamic multi-agent orchestration with the literal `dynamax` token, or keep it sticky for the session:
78
+
79
+ ```text
80
+ dynamax inspect this bug with multiple focused agents
81
+ /dynamax on
82
+ /dynamax status
83
+ /dynamax off
84
+ ```
85
+
86
+ `dynamax` is a permission signal for the host agent: once opted in, it may run an existing named workflow or author an inline workflow script through the `workflow` tool. In interactive mode, `/workflow` with no arguments also offers `✍ Author temporary one-shot workflow…`; choose it, type a brief, and pi will ask the host agent to author/run a temporary inline workflow.
87
+
76
88
  The advisory workflows inspect and report only; they do not edit files. They return the same shape: summary, ranked findings, evidence, impact, recommendations, and next steps.
77
89
 
78
90
  ## Built-in workflows
@@ -81,7 +93,6 @@ The advisory workflows inspect and report only; they do not edit files. They ret
81
93
  - `refactor-scout`: Looks for small, safe refactor opportunities: duplication, complexity, weak types, boundary leaks, dead code, and convention drift.
82
94
  - `diagnose`: Investigates a symptom, failing command, or regression. It generates competing root-cause hypotheses, verifies them, and returns the most likely causes with next validation steps.
83
95
  - `perf-review`: Reviews a slow path or workload for bottleneck hypotheses, measurement gaps, and safe optimization directions. It avoids claiming certainty when measurement evidence is missing.
84
- - `ping`: Minimal smoke workflow for checking that the engine can run one structured agent call.
85
96
 
86
97
  ## The code-review workflow
87
98
 
@@ -93,7 +104,7 @@ The bundled review workflow is deliberately shaped like a serious review process
93
104
  4. **Verify**: send each survivor to an independent verifier that must confirm, mark plausible, or refute with evidence.
94
105
  5. **Synthesize**: produce one ranked report with stats, verdicts, and concrete locations.
95
106
 
96
- The review lenses live in `workflows/code-review.ts`. That is where your repo's real failure modes belong.
107
+ The review lenses live in `.pi/extensions/pi-workflow-engine/workflows/code-review.ts`. That is where your repo's real failure modes belong.
97
108
 
98
109
  ## Authoring workflows
99
110
 
@@ -135,12 +146,33 @@ export default async function run({ agent, parallel, phase }: WorkflowApi) {
135
146
  }
136
147
  ```
137
148
 
149
+ Inline workflow scripts use the same primitives, but are passed as a string to the `workflow` tool instead of being saved as files. A minimal inline workflow starts with `export const meta` and uses the injected Type object for schemas:
150
+
151
+ ```ts
152
+ export const meta = {
153
+ name: "inline-review",
154
+ description: "One-off focused review workflow.",
155
+ };
156
+
157
+ export default async function run({ agent, parallel, phase, args }) {
158
+ phase("Find");
159
+ const Finding = Type.Object({ summary: Type.String() });
160
+ const findings = await parallel([
161
+ () => agent(`Find correctness issues: ${args}`, { schema: Finding, thinkingLevel: "low" }),
162
+ () => agent(`Find edge cases: ${args}`, { schema: Finding, thinkingLevel: "low" }),
163
+ ]);
164
+ return { summary: JSON.stringify(findings) };
165
+ }
166
+ ```
167
+
168
+ Inline v1 rules: no imports, no dynamic `import()`, pure-literal `meta`, schemas must use the injected Type value, and scripts run in-process with the extension's permissions. This v1 path is permissive rather than sandboxed.
169
+
138
170
  Under the hood:
139
171
 
140
172
  - **Each `agent()` is an in-process pi `AgentSession`** using `createAgentSession` and `SessionManager.inMemory()`.
141
173
  - **Structured output is a terminating tool**. The engine registers one tool whose `parameters` is your schema; pi validates the call and the engine captures the args. There is no JSON scraping.
142
174
  - **A single global semaphore caps concurrent agents**, so `parallel` and `pipeline` can nest freely while every `agent()` call still respects the run cap.
143
- - **Two surfaces are registered**: `/workflow <name> [args]` for direct use, and a `workflow` tool for host-agent delegation.
175
+ - **Three surfaces are registered**: `/workflow <name> [args]` for direct use, `/dynamax on|off|status` for sticky orchestration opt-in, and a `workflow` tool for host-agent delegation by `name` or inline `script`.
144
176
 
145
177
  ## Local development
146
178
 
@@ -151,36 +183,78 @@ git clone https://github.com/timbrinded/pi-workflow-engine
151
183
  cd pi-workflow-engine
152
184
  bun install
153
185
  bun run typecheck
154
- bun scripts/smoke.ts
186
+ bun run test
155
187
  ```
156
188
 
157
- Load your working copy into a session without installing it. This is ephemeral and overrides nothing:
189
+ The test suite is no-LLM and uses Bun's built-in `bun test` runner, not a third-party test framework.
190
+
191
+ ### Performance controls and benchmarks
192
+
193
+ The workflow engine is measurement-first: tune only after checking queue wait, local orchestration, discovery, and UI render costs on the machine where you run pi.
194
+
195
+ Runtime controls:
196
+
197
+ - `PI_WORKFLOW_PERF=1` enables the internal per-run performance recorder used by the orchestration instrumentation.
198
+ - `PI_WORKFLOW_CONCURRENCY=N` sets the per-run agent semaphore cap. The default remains `min(8, max(2, CPU count))`.
199
+ - `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` limits how many `parallel()` thunks are submitted at once; this is separate from the running-agent semaphore cap.
200
+ - `PI_WORKFLOW_LANE_ITEM_LIMIT=N` caps retained progress lane items per lane; snapshots report how many older items are hidden.
201
+ - Slash commands can override selected controls per run: `/workflow <name> --concurrency=N --parallel-limit=N ...`.
202
+
203
+ No-LLM benchmark scripts:
158
204
 
159
205
  ```bash
160
- pi -e ./src/index.ts -p "/workflow ping"
206
+ bun run bench:concurrency -- --items 200 --concurrency 8 --json
207
+ bun run bench:discovery -- --iterations 3 --json
208
+ bun run bench:startup -- --json
209
+ bun run bench:ui -- --agents 1000 --lane-items 1000 --json
161
210
  ```
162
211
 
163
- Add a workflow by creating `workflows/<name>.ts`, importing it in `src/workflows.ts`, and adding it to `BUILTIN_WORKFLOWS`. Statically imported workflows share pi's bundled `typebox`, which guarantees schema validation. Files in `workflows/` and `~/.pi/agent/workflows/` are also discovered dynamically at runtime on a best-effort basis.
212
+ Add `--out` to write machine-local JSON under `.artifacts/benchmarks/`. These timings are advisory, not portable thresholds.
213
+
214
+ Guardrails:
215
+
216
+ - Do not pool or reuse subagent sessions until `agent.create_session_ms` is proven material and isolation semantics are reviewed.
217
+ - Do not lazy-load guaranteed built-in workflow modules until pi/jiti bundled `typebox` identity is proven for that path.
218
+ - Do not raise the default concurrency without queue-wait versus run-time evidence.
219
+
220
+ Load your working copy through the package manifest without installing it. This is ephemeral and exercises the same `.pi/extensions/pi-workflow-engine/index.ts` entrypoint that installed packages use:
221
+
222
+ ```bash
223
+ pi -e .
224
+ ```
225
+
226
+ This repo also includes `.pi/settings.json` for project-local auto-discovery. If you also have the global package installed, pi may report duplicate `/workflow` or `workflow` tool diagnostics; remove one source or force pi to ignore discovered extensions and load this working copy instead:
227
+
228
+ ```bash
229
+ pi -ne -e .
230
+ ```
231
+
232
+ Add a built-in workflow by creating `.pi/extensions/pi-workflow-engine/workflows/<name>.ts`, importing it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`, and adding it to `BUILTIN_WORKFLOWS`. Statically imported workflows share pi's bundled `typebox`, which guarantees schema validation. Files in `.pi/extensions/pi-workflow-engine/workflows/` and `~/.pi/agent/workflows/` are also discovered dynamically at runtime on a best-effort basis.
233
+
234
+ Keep guaranteed built-ins statically imported. Optimize cold startup at the extension entrypoint or discovery boundary first; do not lazy-load built-in workflow modules unless a test proves pi/jiti preserves the same bundled `typebox` identity for dynamically imported built-ins.
164
235
 
165
- Tune the built-in review workflow by editing the `ANGLES` array in `workflows/code-review.ts`. Tune `model`, `thinkingLevel`, and `tools` per `agent()` call, and tune `DEFAULT_CONCURRENCY` in `src/engine.ts`.
236
+ Tune the built-in review workflow by editing the `ANGLES` array in `.pi/extensions/pi-workflow-engine/workflows/code-review.ts`. Tune `model`, `thinkingLevel`, and `tools` per `agent()` call, and use `PI_WORKFLOW_CONCURRENCY` or `/workflow <name> --concurrency=N` for measured per-run concurrency experiments.
166
237
 
167
238
  ### Layout
168
239
 
169
240
  ```text
170
- src/
171
- index.ts extension entry; registers the command and tool
172
- types.ts WorkflowApi / WorkflowModule contracts
173
- agent-runner.ts createAgentSession + terminating-tool schema bridge
174
- concurrency.ts Semaphore, parallel(), pipeline()
175
- engine.ts runWorkflow(); binds primitives to one run
176
- progress.ts live phase/agent tree via ctx.ui.setWidget
177
- discovery.ts static registry + best-effort drop-in loading
178
- workflows/
179
- code-review.ts scope -> find -> verify -> synthesize
180
- refactor-scout.ts advisory refactor opportunities
181
- diagnose.ts advisory bug diagnosis
182
- perf-review.ts advisory performance investigation
183
- ping.ts minimal smoke workflow
241
+ .pi/
242
+ settings.json project-local pi resource settings
243
+ extensions/pi-workflow-engine/
244
+ index.ts canonical extension entry; registers the command and tool
245
+ src/
246
+ types.ts WorkflowApi / WorkflowModule contracts
247
+ agent-runner.ts createAgentSession + terminating-tool schema bridge
248
+ concurrency.ts Semaphore, parallel(), pipeline()
249
+ engine.ts runWorkflow(); binds primitives to one run
250
+ progress.ts live phase/agent tree via ctx.ui.setWidget
251
+ discovery.ts static registry + best-effort drop-in loading
252
+ workflows.ts statically registered built-in workflows
253
+ workflows/
254
+ code-review.ts scope -> find -> verify -> synthesize
255
+ refactor-scout.ts advisory refactor opportunities
256
+ diagnose.ts advisory bug diagnosis
257
+ perf-review.ts advisory performance investigation
184
258
  ```
185
259
 
186
260
  ## License
package/USAGE.md ADDED
@@ -0,0 +1,183 @@
1
+ # Usage
2
+
3
+ `pi-workflow-engine` adds multi-agent workflows to pi. Most users only need two surfaces:
4
+
5
+ - `/workflow <name> [args]` to run a saved workflow directly.
6
+ - `dynamax` when you want the host agent to author a one-off inline workflow for investigation/review.
7
+
8
+ ## Run a workflow
9
+
10
+ From inside a repo:
11
+
12
+ ```text
13
+ /workflow code-review
14
+ /workflow code-review HEAD~3
15
+ /workflow refactor-scout src/
16
+ /workflow diagnose "typecheck fails after the schema change"
17
+ /workflow perf-review "workflow startup latency"
18
+ ```
19
+
20
+ The built-in workflows are advisory. They inspect, verify, and report; they do **not** edit files.
21
+
22
+ Useful flags:
23
+
24
+ ```text
25
+ /workflow code-review --inspect # show live inspector
26
+ /workflow code-review --concurrency=4 # cap concurrent subagents
27
+ /workflow code-review --refresh # rediscover newly added workflow files
28
+ ```
29
+
30
+ ## Built-in workflows
31
+
32
+ | Name | Purpose |
33
+ | --- | --- |
34
+ | `code-review` | Reviews a PR, branch diff, ref range, or focus area. |
35
+ | `refactor-scout` | Finds small, safe refactor opportunities. |
36
+ | `diagnose` | Investigates a bug, failing command, or regression. |
37
+ | `perf-review` | Reviews a slow path or performance concern. |
38
+
39
+ `code-review` auto-detects a diff when you do not pass args. If there is an open GitHub PR for the current branch, it uses that PR diff; otherwise it falls back to branch-vs-main/master or `HEAD~1`.
40
+
41
+ ## Use `dynamax` for inline workflows
42
+
43
+ Use `dynamax` when the built-in workflows are not specific enough and you want the main pi agent to author a temporary workflow for the current question.
44
+
45
+ Interactive shortcut: run `/workflow` with no arguments, choose `✍ Author temporary one-shot workflow…`, and type a brief. pi will send that brief back to the host agent as a `dynamax` request so it can author and run an inline workflow with the `workflow` tool's `script` argument.
46
+
47
+ That inline workflow should investigate, fan out, verify, and summarize. It is **not** where code edits happen. If you want changes made, use the workflow result as evidence and then ask pi to edit separately.
48
+
49
+ One-off examples:
50
+
51
+ ```text
52
+ dynamax author an inline workflow to investigate why typecheck is failing
53
+ dynamax create a one-off workflow to compare the parser and tests before we change code
54
+ dynamax use parallel agents to review this design and report the risks
55
+ ```
56
+
57
+ Sticky for the session:
58
+
59
+ ```text
60
+ /dynamax on
61
+ /dynamax off
62
+ /dynamax status
63
+ ```
64
+
65
+ With dynamax enabled, the host agent usually calls the `workflow` tool with `script`: a one-off inline workflow. It can still call a saved workflow by `name` when one already fits.
66
+
67
+ ## Workflow results
68
+
69
+ Workflow results are rendered in pi with:
70
+
71
+ - summary;
72
+ - ranked findings;
73
+ - locations and evidence;
74
+ - recommendations;
75
+ - next steps;
76
+ - run stats when available.
77
+
78
+ During a run, pi shows live phases and subagent status. Use `--inspect` if you want a larger live view.
79
+
80
+ ## Author a saved workflow
81
+
82
+ Saved workflows are TypeScript modules with `meta` plus a default async function:
83
+
84
+ ```ts
85
+ import { Type } from "typebox";
86
+ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
87
+
88
+ export const meta: WorkflowMeta = {
89
+ name: "my-workflow",
90
+ description: "Find and summarize something important.",
91
+ };
92
+
93
+ const Finding = Type.Object({
94
+ summary: Type.String(),
95
+ });
96
+
97
+ export default async function run({ agent, parallel, phase, args }: WorkflowApi) {
98
+ phase("Find");
99
+ const findings = await parallel([
100
+ () => agent(`Find correctness issues: ${args}`, { schema: Finding, tools: ["read", "bash"], thinkingLevel: "low" }),
101
+ () => agent(`Find edge cases: ${args}`, { schema: Finding, tools: ["read", "bash"], thinkingLevel: "low" }),
102
+ ]);
103
+
104
+ phase("Synthesize");
105
+ return agent(`Summarize: ${JSON.stringify(findings)}`, { thinkingLevel: "medium" });
106
+ }
107
+ ```
108
+
109
+ Core primitives:
110
+
111
+ | Primitive | What it does |
112
+ | --- | --- |
113
+ | `agent(prompt, opts)` | Runs one isolated subagent. With `schema`, returns validated structured data. |
114
+ | `parallel(thunks)` | Runs thunks concurrently and waits for all. |
115
+ | `pipeline(items, ...stages)` | Runs each item through stages independently. |
116
+ | `phase(title)` / `log(message)` | Updates workflow progress UI. |
117
+ | `progress(event)` | Emits counters, summaries, and lane items. |
118
+
119
+ Set `thinkingLevel` on fan-out agents. Otherwise many subagents can inherit an expensive global reasoning level.
120
+
121
+ ### Where workflows live
122
+
123
+ Guaranteed built-ins are statically registered:
124
+
125
+ 1. Add `.pi/extensions/pi-workflow-engine/workflows/<name>.ts`.
126
+ 2. Import it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`.
127
+ 3. Add it to `BUILTIN_WORKFLOWS`.
128
+
129
+ Drop-in workflows are also discovered best-effort from:
130
+
131
+ - `.pi/extensions/pi-workflow-engine/workflows/*.ts`
132
+ - `~/.pi/agent/workflows/*.ts`
133
+
134
+ Use `/workflow <name> --refresh` after adding a drop-in file.
135
+
136
+ ## Author an inline workflow
137
+
138
+ Inline workflows are passed to the `workflow` tool as a script string. They are useful for one-off dynamax orchestration.
139
+
140
+ ```ts
141
+ export const meta = {
142
+ name: "inline-review",
143
+ description: "One-off focused review.",
144
+ };
145
+
146
+ export default async function run({ agent, parallel, phase, args }) {
147
+ phase("Find");
148
+ const Finding = Type.Object({ summary: Type.String() });
149
+ const findings = await parallel([
150
+ () => agent(`Find correctness issues: ${args}`, { schema: Finding, thinkingLevel: "low" }),
151
+ () => agent(`Find edge cases: ${args}`, { schema: Finding, thinkingLevel: "low" }),
152
+ ]);
153
+ return { summary: JSON.stringify(findings) };
154
+ }
155
+ ```
156
+
157
+ Inline rules:
158
+
159
+ - start with `export const meta = { ... }`;
160
+ - default-export an async workflow function;
161
+ - no imports or dynamic `import()`;
162
+ - use the injected `Type` object for schemas;
163
+ - no extra exports or code after the default export.
164
+
165
+ Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
166
+
167
+ ## Runtime knobs
168
+
169
+ Only tune these when a workflow is too slow, too expensive, or too noisy:
170
+
171
+ | Knob | Meaning |
172
+ | --- | --- |
173
+ | `--concurrency=N` / `PI_WORKFLOW_CONCURRENCY=N` | Cap concurrent subagents. Default is `min(8, max(2, CPU count))`. |
174
+ | `--parallel-limit=N` / `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` | Limit eager `parallel()` submission. |
175
+ | `--perf` / `PI_WORKFLOW_PERF=1` | Include internal timing aggregates. |
176
+ | `PI_WORKFLOW_LANE_ITEM_LIMIT=N` | Cap retained progress lane items. |
177
+
178
+ ## Common fixes
179
+
180
+ - **Unknown workflow**: confirm the extension is loaded with `pi list`; use `--refresh` for new drop-ins.
181
+ - **Inline compile error**: check the inline rules above.
182
+ - **Slow run**: lower fan-out, set `thinkingLevel`, or reduce `--concurrency`.
183
+ - **Duplicate command/tool warnings while developing**: avoid loading both the global package and the working copy.
Binary file
package/package.json CHANGED
@@ -1,23 +1,50 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "A customisable multi-agent workflow-orchestration engine for the pi coding agent.",
6
- "keywords": ["pi-package"],
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "pi-workflow",
10
+ "workflow-engine",
11
+ "multi-agent",
12
+ "coding-agent"
13
+ ],
7
14
  "license": "MIT",
15
+ "homepage": "https://github.com/timbrinded/pi-workflow-engine#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/timbrinded/pi-workflow-engine/issues"
18
+ },
8
19
  "repository": {
9
20
  "type": "git",
10
21
  "url": "git+https://github.com/timbrinded/pi-workflow-engine.git"
11
22
  },
12
- "files": ["src", "workflows"],
23
+ "files": [
24
+ ".pi/extensions/pi-workflow-engine",
25
+ ".pi/settings.json",
26
+ "assets",
27
+ "README.md",
28
+ "USAGE.md",
29
+ "LICENSE"
30
+ ],
13
31
  "pi": {
14
- "extensions": ["./src/index.ts"],
32
+ "extensions": [".pi/extensions/pi-workflow-engine/index.ts"],
15
33
  "image": "https://raw.githubusercontent.com/timbrinded/pi-workflow-engine/master/assets/preview.png"
16
34
  },
17
35
  "scripts": {
18
36
  "typecheck": "tsc --noEmit",
19
- "test:ui": "bun scripts/test-workflow-ui.ts",
20
- "prepublishOnly": "tsc --noEmit"
37
+ "test": "bun test",
38
+ "test:changedlines": "bun test tests/changedlines.test.ts",
39
+ "test:concurrency": "bun test tests/concurrency.test.ts",
40
+ "test:smoke": "bun test tests/discovery.test.ts",
41
+ "test:ui": "bun test tests/workflow-ui.test.ts",
42
+ "bench": "bun run bench:concurrency && bun run bench:discovery && bun run bench:ui",
43
+ "bench:concurrency": "bun run scripts/bench/concurrency.ts",
44
+ "bench:discovery": "bun run scripts/bench/discovery.ts",
45
+ "bench:startup": "bun run scripts/bench/startup.ts",
46
+ "bench:ui": "bun run scripts/bench/ui.ts",
47
+ "prepublishOnly": "tsc --noEmit && bun run test"
21
48
  },
22
49
  "peerDependencies": {
23
50
  "@earendil-works/pi-agent-core": "*",
@@ -1,122 +0,0 @@
1
- import { createAgentSession, defineTool, SessionManager } from "@earendil-works/pi-coding-agent";
2
- import type { ModelRegistry, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
- import type { Model } from "@earendil-works/pi-ai";
4
- import type { AgentState } from "@earendil-works/pi-agent-core";
5
- import type { AgentOptions } from "./types.ts";
6
- import type { Semaphore } from "./concurrency.ts";
7
- import type { ProgressTracker } from "./progress.ts";
8
-
9
- /** Name of the synthetic terminating tool that carries structured output. */
10
- const FINAL_TOOL = "final_answer";
11
-
12
- /** Shared per-run context threaded into every agent() call. */
13
- export interface RunContext {
14
- cwd: string;
15
- hostModel: Model<any> | undefined;
16
- modelRegistry: ModelRegistry;
17
- semaphore: Semaphore;
18
- progress: ProgressTracker;
19
- signal: AbortSignal | undefined;
20
- }
21
-
22
- /** Pull the last assistant message's plain text out of a finished session. */
23
- function lastAssistantText(state: AgentState): string {
24
- const messages = state.messages;
25
- for (let i = messages.length - 1; i >= 0; i--) {
26
- const message = messages[i];
27
- if ("role" in message && message.role === "assistant") {
28
- const content = (message as { content?: Array<{ type: string; text?: string }> }).content ?? [];
29
- return content
30
- .filter((part) => part.type === "text")
31
- .map((part) => part.text ?? "")
32
- .join("")
33
- .trim();
34
- }
35
- }
36
- return "";
37
- }
38
-
39
- /**
40
- * Run one subagent to completion in an isolated in-memory session.
41
- *
42
- * Structured output trick: when `opts.schema` is set we register a single
43
- * terminating tool whose `parameters` IS the schema. The agent's last action is to
44
- * call it; pi validates the args against the schema, hands them to `execute`, and
45
- * `terminate: true` ends the turn with no extra LLM round-trip. We capture those
46
- * validated args in a closure — that captured object is the structured result, so
47
- * no event-stream parsing is needed.
48
- */
49
- export async function runAgent(rc: RunContext, prompt: string, opts: AgentOptions = {}): Promise<unknown> {
50
- const label = opts.label ?? "agent";
51
-
52
- // Track queued agents before acquiring a global concurrency slot.
53
- const rowId = rc.progress.agentQueued(opts.phase, label);
54
- return rc.semaphore.run(async () => {
55
- rc.progress.agentStart(opts.phase, label, rowId);
56
-
57
- let captured: unknown = null;
58
- let failed = false;
59
- const customTools: ToolDefinition[] = opts.schema
60
- ? [
61
- defineTool({
62
- name: FINAL_TOOL,
63
- label: "Final Answer",
64
- description:
65
- "Return your final structured answer. This MUST be your last action — do not write a normal reply after calling it.",
66
- parameters: opts.schema,
67
- async execute(_toolCallId, params) {
68
- captured = params;
69
- return { content: [{ type: "text", text: "Recorded." }], details: params, terminate: true };
70
- },
71
- }),
72
- ]
73
- : [];
74
-
75
- // When the workflow restricts built-in tools, keep the terminating tool visible.
76
- const allow = opts.tools
77
- ? opts.schema
78
- ? [...opts.tools, FINAL_TOOL]
79
- : opts.tools
80
- : undefined;
81
-
82
- // Resolve the model by runtime id lookup (no compile-time model-id union); fall back to the host's model.
83
- const model = opts.model ? rc.modelRegistry.find("anthropic", opts.model) ?? rc.hostModel : rc.hostModel;
84
-
85
- const { session } = await createAgentSession({
86
- cwd: rc.cwd,
87
- model,
88
- modelRegistry: rc.modelRegistry,
89
- thinkingLevel: opts.thinkingLevel,
90
- tools: allow,
91
- customTools,
92
- sessionManager: SessionManager.inMemory(rc.cwd),
93
- });
94
-
95
- const unsubscribe = session.subscribe((event) => {
96
- if (event.type === "tool_execution_start" && event.toolName !== FINAL_TOOL) {
97
- rc.progress.agentTool(label, event.toolName, rowId);
98
- }
99
- });
100
-
101
- try {
102
- const finalPrompt = opts.schema
103
- ? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
104
- : prompt;
105
- await session.prompt(finalPrompt);
106
- if (opts.schema) {
107
- if (captured === null) rc.progress.log(`${label}: no structured answer returned`);
108
- return captured;
109
- }
110
- return lastAssistantText(session.state);
111
- } catch (error) {
112
- failed = true;
113
- rc.progress.agentFailed(label, error, rowId);
114
- rc.progress.log(`${label} failed: ${error instanceof Error ? error.message : String(error)}`);
115
- throw error;
116
- } finally {
117
- unsubscribe();
118
- session.dispose();
119
- if (!failed) rc.progress.agentDone(label, rowId);
120
- }
121
- });
122
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * A counting semaphore: bounds how many async tasks run at once.
3
- *
4
- * This is the single global concurrency cap for a workflow run. Every `agent()`
5
- * call acquires a slot before spawning a session and releases it when done, so the
6
- * cap holds no matter how `parallel`/`pipeline` are nested — exactly like the
7
- * built-in Workflow tool, where "concurrent agent() calls are capped and excess queue".
8
- */
9
- export class Semaphore {
10
- private active = 0;
11
- private readonly waiters: Array<() => void> = [];
12
-
13
- constructor(private readonly max: number) {}
14
-
15
- async run<T>(fn: () => Promise<T>): Promise<T> {
16
- if (this.active >= this.max) {
17
- await new Promise<void>((resolve) => this.waiters.push(resolve));
18
- }
19
- this.active++;
20
- try {
21
- return await fn();
22
- } finally {
23
- this.active--;
24
- this.waiters.shift()?.();
25
- }
26
- }
27
- }
28
-
29
- /** Run every thunk concurrently and wait for all results (a barrier). */
30
- export async function parallel<T>(thunks: Array<() => Promise<T>>): Promise<T[]> {
31
- return Promise.all(thunks.map((thunk) => thunk()));
32
- }
33
-
34
- /**
35
- * Run each item through all stages independently. There is NO barrier between
36
- * stages: item A can be in stage 3 while item B is still in stage 1. Actual agent
37
- * concurrency is bounded by the run's Semaphore (inside each `agent()`), not here —
38
- * so launching every item at once is safe.
39
- *
40
- * Each stage receives (previousResult, originalItem, index).
41
- */
42
- export async function pipeline(
43
- items: unknown[],
44
- ...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>
45
- ): Promise<unknown[]> {
46
- return Promise.all(
47
- items.map(async (item, index) => {
48
- let acc: unknown = item;
49
- for (const stage of stages) {
50
- acc = await stage(acc, item, index);
51
- }
52
- return acc;
53
- }),
54
- );
55
- }