pi-workflow-engine 0.2.0 → 0.4.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 (37) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +256 -0
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +64 -0
  3. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +238 -0
  4. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +28 -0
  5. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +171 -0
  6. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +152 -0
  7. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +114 -0
  8. package/{src → .pi/extensions/pi-workflow-engine/src}/engine.ts +36 -8
  9. package/.pi/extensions/pi-workflow-engine/src/options.ts +46 -0
  10. package/.pi/extensions/pi-workflow-engine/src/perf.ts +139 -0
  11. package/{src → .pi/extensions/pi-workflow-engine/src}/progress.ts +82 -35
  12. package/{src → .pi/extensions/pi-workflow-engine/src}/types.ts +12 -4
  13. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-format.ts +59 -6
  14. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-inspector.ts +8 -6
  15. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +198 -0
  16. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +182 -0
  17. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +263 -0
  18. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +24 -0
  19. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/code-review.ts +89 -145
  20. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +244 -0
  21. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +188 -0
  22. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/ping.ts +1 -0
  23. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +181 -0
  24. package/.pi/settings.json +3 -0
  25. package/README.md +163 -63
  26. package/assets/preview-candidate-core.png +0 -0
  27. package/assets/preview-candidate-relic-emblem.png +0 -0
  28. package/assets/preview-candidate-robot-squad.png +0 -0
  29. package/assets/preview.png +0 -0
  30. package/package.json +32 -6
  31. package/src/agent-runner.ts +0 -122
  32. package/src/concurrency.ts +0 -55
  33. package/src/discovery.ts +0 -54
  34. package/src/index.ts +0 -141
  35. package/src/ui/workflow-result-renderer.ts +0 -149
  36. package/src/ui/workflow-widget.ts +0 -143
  37. package/src/workflows.ts +0 -14
package/README.md CHANGED
@@ -1,10 +1,27 @@
1
- <p align="center">
2
- <img src="assets/preview.png" width="200" alt="pi-workflow-engine — a central agent orchestrating a ring of subagents" />
3
- </p>
1
+ ![agents](assets/preview.png)
4
2
 
5
3
  # pi-workflow-engine
6
4
 
7
- A customisable multi-agent workflow-orchestration engine for the [pi](https://pi.dev) coding agent — your own version of Claude Code's built-in `/code-review`, built on pi's SDK. Workflows fan out to many subagents, pass validated structured data between stages, and synthesise a result.
5
+ Programmable multi-agent workflows for the [pi](https://pi.dev) coding agent.
6
+
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
+
9
+ The built-in workflows are advisory: they inspect, fan out, verify, and report. They do not edit files. Start with `/workflow code-review`, or use the focused scouts for refactors, diagnosis, and performance.
10
+
11
+ ## Why workflows matter
12
+
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
+
15
+ `pi-workflow-engine` brings that shape to pi with static TypeScript workflows:
16
+
17
+ - **Procedures, not prompts**: write the workflow once, then invoke it with `/workflow` or let the host agent call the `workflow` tool.
18
+ - **Isolated subagents**: each `agent()` runs in its own in-memory pi session, so exploratory work does not pollute the main conversation.
19
+ - **Parallel cognition**: run many focused agents at once, with a shared concurrency cap so large workflows stay bounded.
20
+ - **Typed handoffs**: pass structured data between stages using typebox schemas instead of asking the model to emit parseable prose.
21
+ - **Verifier stages**: make verification part of the control flow, not an optional final instruction.
22
+ - **Live progress**: surface phases, agent status, counters, and lane items in the TUI while the run is still moving.
23
+
24
+ The result is closer to an executable review playbook than a chatbot shortcut.
8
25
 
9
26
  ## Install
10
27
 
@@ -18,118 +35,201 @@ Or from npm:
18
35
  pi install npm:pi-workflow-engine
19
36
  ```
20
37
 
21
- That's all — pi fetches the package and serves its dependencies from its own bundle, so there's no clone, install, or build step. Restart pi (or run `/reload` in an open session), then confirm it's 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:
22
39
 
23
40
  ```bash
24
41
  pi list
25
42
  ```
26
43
 
27
- Scope it to a single repo instead of globally (writes to that repo's `.pi/settings.json`):
44
+ Scope it to a single repo instead of globally. This writes to that repo's `.pi/settings.json`:
28
45
 
29
46
  ```bash
30
47
  pi install git:github.com/timbrinded/pi-workflow-engine -l
31
48
  ```
32
49
 
33
- Uninstall with `pi remove git:github.com/timbrinded/pi-workflow-engine`.
50
+ Uninstall with:
51
+
52
+ ```bash
53
+ pi remove git:github.com/timbrinded/pi-workflow-engine
54
+ ```
34
55
 
35
56
  ## Usage
36
57
 
37
58
  In a pi session, from inside a git repo with changes:
38
59
 
39
- ```
40
- /workflow code-review # review the current branch
41
- /workflow code-review HEAD~3 # review a ref range / target / focus area
42
- /workflow ping # quick engine smoke test (one agent)
60
+ ```text
61
+ /workflow code-review # review the current branch or open PR
62
+ /workflow code-review HEAD~3 # review a ref range, target, or focus area
63
+ /workflow refactor-scout src/ # find safe refactor opportunities
64
+ /workflow diagnose "typecheck fails after the schema change"
65
+ /workflow perf-review "workflow startup latency"
66
+ /workflow code-review --inspect # open the live workflow inspector
67
+ /workflow ping # quick engine smoke test
43
68
  ```
44
69
 
45
- The host agent can also invoke the `workflow` tool itself mid-conversation (e.g. "run the code-review workflow") and fold the structured result back into its reasoning.
46
-
47
- `code-review` returns a verified, ranked report:
48
-
49
- ```json
50
- {
51
- "summary": "1 confirmed bug: off-by-one loop boundary in sum.js ...",
52
- "findings": [
53
- {
54
- "file": "sum.js",
55
- "line": 3,
56
- "severity": "bug",
57
- "verdict": "CONFIRMED",
58
- "summary": "Off-by-one: `i <= arr.length` should be `i < arr.length`; accesses arr[arr.length] (undefined) → NaN."
59
- }
60
- ]
61
- }
70
+ The host agent can also invoke the `workflow` tool mid-conversation:
71
+
72
+ ```text
73
+ Run the code-review workflow on this PR and use the result before deciding what to fix.
62
74
  ```
63
75
 
64
- ## How it works
76
+ 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
+
78
+ ## Built-in workflows
79
+
80
+ - `code-review`: Reviews the current branch, open PR, ref range, or target. It looks for correctness bugs and cleanup issues, then independently verifies candidates before ranking them.
81
+ - `refactor-scout`: Looks for small, safe refactor opportunities: duplication, complexity, weak types, boundary leaks, dead code, and convention drift.
82
+ - `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
+ - `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
+
86
+ ## The code-review workflow
87
+
88
+ The bundled review workflow is deliberately shaped like a serious review process:
65
89
 
66
- A **workflow** is a TypeScript module that exports `meta` plus a default `async (api) => result`. The injected `api` gives you five primitives:
90
+ 1. **Scope**: detect the open PR or branch diff, list changed files, and read relevant project conventions.
91
+ 2. **Find**: fan out across focused review lenses such as logic bugs, error paths, edge cases, simplification, and conventions.
92
+ 3. **Gate and dedupe**: bound candidates to changed lines and collapse duplicate findings before spending verifier tokens.
93
+ 4. **Verify**: send each survivor to an independent verifier that must confirm, mark plausible, or refute with evidence.
94
+ 5. **Synthesize**: produce one ranked report with stats, verdicts, and concrete locations.
95
+
96
+ 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
+
98
+ ## Authoring workflows
99
+
100
+ A workflow is a TypeScript module that exports `meta` plus a default `async (api) => result`. The injected `api` gives you the primitives:
67
101
 
68
102
  | Primitive | Behaviour |
69
103
  |-----------|-----------|
70
- | `agent(prompt, { schema })` | Runs a subagent in an isolated session; with a typebox `schema` it returns validated structured data, otherwise the final text. |
71
- | `parallel(thunks)` | Runs every thunk concurrently and waits for all (a barrier). |
72
- | `pipeline(items, ...stages)` | Runs each item through all stages independently — no barrier between stages. |
73
- | `phase(title)` / `log(msg)` | Drive the live progress tree shown in the TUI. |
104
+ | `agent(prompt, { schema })` | Runs a subagent in an isolated session. With a typebox `schema`, returns validated structured data; otherwise returns final text. |
105
+ | `parallel(thunks)` | Runs every thunk concurrently and waits for all results. |
106
+ | `pipeline(items, ...stages)` | Runs each item through all stages independently, with no barrier between stages. |
107
+ | `phase(title)` / `log(msg)` | Drives the live progress tree shown in the TUI and stderr breadcrumbs when headless. |
108
+ | `progress(event)` | Emits structured progress for richer workflow UI surfaces. |
74
109
 
75
110
  ```ts
76
111
  import { Type } from "typebox";
77
112
  import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
78
113
 
79
- export const meta: WorkflowMeta = { name: "my-workflow", description: "..." };
114
+ export const meta: WorkflowMeta = {
115
+ name: "my-workflow",
116
+ description: "Find, verify, and summarize something important.",
117
+ };
80
118
 
81
- export default async function run({ agent, parallel, pipeline, phase, log, args }: WorkflowApi) {
119
+ const FindingSchema = Type.Object({
120
+ summary: Type.String(),
121
+ file: Type.String(),
122
+ line: Type.Optional(Type.Number()),
123
+ });
124
+
125
+ export default async function run({ agent, parallel, phase }: WorkflowApi) {
82
126
  phase("Find");
83
- const result = await agent("find issues ...", { schema: Type.Object({ /* ... */ }) });
84
- return result; // a typed object, validated against the schema
127
+
128
+ const findings = await parallel([
129
+ () => agent("Find correctness bugs in the diff.", { schema: FindingSchema, thinkingLevel: "low" }),
130
+ () => agent("Find error-handling bugs in the diff.", { schema: FindingSchema, thinkingLevel: "low" }),
131
+ ]);
132
+
133
+ phase("Synthesize");
134
+ return agent(`Summarize these findings: ${JSON.stringify(findings)}`, { thinkingLevel: "medium" });
85
135
  }
86
136
  ```
87
137
 
88
138
  Under the hood:
89
139
 
90
- - **Each `agent()` is an in-process pi `AgentSession`** (`createAgentSession` + `SessionManager.inMemory()`), inheriting the host's model unless you set one.
91
- - **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 — no parsing.
92
- - **A single global semaphore caps concurrent agents**, so `parallel`/`pipeline` nest freely while the cap holds inside every `agent()` call.
93
- - **Surfaces**: a `/workflow <name> [args]` command, plus a `workflow` tool the host agent can call.
140
+ - **Each `agent()` is an in-process pi `AgentSession`** using `createAgentSession` and `SessionManager.inMemory()`.
141
+ - **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
+ - **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.
94
144
 
95
145
  ## Local development
96
146
 
97
- Only needed if you want to **add or customise workflows, or contribute** — using the bundled workflows requires none of this.
147
+ Only needed if you want to add workflows, customise workflows, or contribute. Using the bundled workflows requires none of this.
98
148
 
99
149
  ```bash
100
150
  git clone https://github.com/timbrinded/pi-workflow-engine
101
151
  cd pi-workflow-engine
102
- bun install # dev deps for typechecking only; pi serves bundled copies at runtime
103
- bun run typecheck # tsc --noEmit
104
- bun scripts/smoke.ts # no-LLM check: module graph loads + workflow discovery resolves
152
+ bun install
153
+ bun run typecheck
154
+ bun run test
105
155
  ```
106
156
 
107
- Load your working copy into a session without installing it (ephemeral — overrides nothing):
157
+ The test suite is no-LLM and uses Bun's built-in `bun test` runner, not a third-party test framework.
158
+
159
+ ### Performance controls and benchmarks
160
+
161
+ 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.
162
+
163
+ Runtime controls:
164
+
165
+ - `PI_WORKFLOW_PERF=1` enables the internal per-run performance recorder used by the orchestration instrumentation.
166
+ - `PI_WORKFLOW_CONCURRENCY=N` sets the per-run agent semaphore cap. The default remains `min(8, max(2, CPU count))`.
167
+ - `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` limits how many `parallel()` thunks are submitted at once; this is separate from the running-agent semaphore cap.
168
+ - `PI_WORKFLOW_LANE_ITEM_LIMIT=N` caps retained progress lane items per lane; snapshots report how many older items are hidden.
169
+ - Slash commands can override selected controls per run: `/workflow <name> --concurrency=N --parallel-limit=N ...`.
170
+
171
+ No-LLM benchmark scripts:
108
172
 
109
173
  ```bash
110
- pi -e ./src/index.ts -p "/workflow ping"
174
+ bun run bench:concurrency -- --items 200 --concurrency 8 --json
175
+ bun run bench:discovery -- --iterations 3 --json
176
+ bun run bench:startup -- --json
177
+ bun run bench:ui -- --agents 1000 --lane-items 1000 --json
111
178
  ```
112
179
 
113
- **Add a workflow:** drop a `.ts` in `workflows/`, import it in `src/workflows.ts`, and add 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, best-effort.)
180
+ Add `--out` to write machine-local JSON under `.artifacts/benchmarks/`. These timings are advisory, not portable thresholds. Optional LLM smoke timing is separate from the required gates:
114
181
 
115
- **Customise the review:** edit the `ANGLES` array in `workflows/code-review.ts` — the lenses are where your codebase's real failure modes and conventions belong. Tune `model` / `thinkingLevel` / `tools` per `agent()` call, and `DEFAULT_CONCURRENCY` in `src/engine.ts`.
182
+ ```bash
183
+ pi -e . -p "/workflow ping"
184
+ ```
116
185
 
117
- ### Layout
186
+ Guardrails:
187
+
188
+ - Do not pool or reuse subagent sessions until `agent.create_session_ms` is proven material and isolation semantics are reviewed.
189
+ - Do not lazy-load guaranteed built-in workflow modules until pi/jiti bundled `typebox` identity is proven for that path.
190
+ - Do not raise the default concurrency without queue-wait versus run-time evidence.
191
+
192
+ 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:
118
193
 
194
+ ```bash
195
+ pi -e . -p "/workflow ping"
196
+ ```
197
+
198
+ 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:
199
+
200
+ ```bash
201
+ pi -ne -e .
119
202
  ```
120
- src/
121
- index.ts extension entry (registers the command + tool)
122
- types.ts WorkflowApi / WorkflowModule contracts
123
- agent-runner.ts the createAgentSession + terminating-tool schema bridge
124
- concurrency.ts Semaphore, parallel(), pipeline()
125
- engine.ts runWorkflow() — binds the primitives to a run
126
- progress.ts live phase/agent tree via ctx.ui.setWidget
127
- discovery.ts static registry + best-effort drop-in loading
128
- workflows/
129
- code-review.ts the example: scope → find → verify → synthesize
130
- ping.ts minimal smoke workflow
203
+
204
+ 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.
205
+
206
+ 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.
207
+
208
+ 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.
209
+
210
+ ### Layout
211
+
212
+ ```text
213
+ .pi/
214
+ settings.json project-local pi resource settings
215
+ extensions/pi-workflow-engine/
216
+ index.ts canonical extension entry; registers the command and tool
217
+ src/
218
+ types.ts WorkflowApi / WorkflowModule contracts
219
+ agent-runner.ts createAgentSession + terminating-tool schema bridge
220
+ concurrency.ts Semaphore, parallel(), pipeline()
221
+ engine.ts runWorkflow(); binds primitives to one run
222
+ progress.ts live phase/agent tree via ctx.ui.setWidget
223
+ discovery.ts static registry + best-effort drop-in loading
224
+ workflows.ts statically registered built-in workflows
225
+ workflows/
226
+ code-review.ts scope -> find -> verify -> synthesize
227
+ refactor-scout.ts advisory refactor opportunities
228
+ diagnose.ts advisory bug diagnosis
229
+ perf-review.ts advisory performance investigation
230
+ ping.ts minimal smoke workflow
131
231
  ```
132
232
 
133
233
  ## License
134
234
 
135
- MIT — see [LICENSE](LICENSE).
235
+ MIT. See [LICENSE](LICENSE).
Binary file
package/package.json CHANGED
@@ -1,23 +1,49 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "0.2.0",
3
+ "version": "0.4.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
+ "LICENSE"
29
+ ],
13
30
  "pi": {
14
- "extensions": ["./src/index.ts"],
31
+ "extensions": [".pi/extensions/pi-workflow-engine/index.ts"],
15
32
  "image": "https://raw.githubusercontent.com/timbrinded/pi-workflow-engine/master/assets/preview.png"
16
33
  },
17
34
  "scripts": {
18
35
  "typecheck": "tsc --noEmit",
19
- "test:ui": "bun scripts/test-workflow-ui.ts",
20
- "prepublishOnly": "tsc --noEmit"
36
+ "test": "bun test",
37
+ "test:changedlines": "bun test tests/changedlines.test.ts",
38
+ "test:concurrency": "bun test tests/concurrency.test.ts",
39
+ "test:smoke": "bun test tests/discovery.test.ts",
40
+ "test:ui": "bun test tests/workflow-ui.test.ts",
41
+ "bench": "bun run bench:concurrency && bun run bench:discovery && bun run bench:ui",
42
+ "bench:concurrency": "bun run scripts/bench/concurrency.ts",
43
+ "bench:discovery": "bun run scripts/bench/discovery.ts",
44
+ "bench:startup": "bun run scripts/bench/startup.ts",
45
+ "bench:ui": "bun run scripts/bench/ui.ts",
46
+ "prepublishOnly": "tsc --noEmit && bun run test"
21
47
  },
22
48
  "peerDependencies": {
23
49
  "@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
- }
package/src/discovery.ts DELETED
@@ -1,54 +0,0 @@
1
- import { readdir } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- import type { WorkflowModule } from "./types.ts";
6
- import { BUILTIN_WORKFLOWS } from "./workflows.ts";
7
-
8
- function isWorkflowModule(value: unknown): value is WorkflowModule {
9
- if (!value || typeof value !== "object") return false;
10
- const candidate = value as { meta?: { name?: unknown }; default?: unknown };
11
- return typeof candidate.meta?.name === "string" && typeof candidate.default === "function";
12
- }
13
-
14
- /** Best-effort dynamic load of every `*.ts` workflow in a directory. */
15
- async function loadDir(dir: string): Promise<WorkflowModule[]> {
16
- let entries: string[];
17
- try {
18
- entries = await readdir(dir);
19
- } catch {
20
- return [];
21
- }
22
-
23
- const modules: WorkflowModule[] = [];
24
- for (const name of entries) {
25
- if (!name.endsWith(".ts") || name.startsWith("_") || name.startsWith(".")) continue;
26
- try {
27
- const loaded: unknown = await import(pathToFileURL(join(dir, name)).href);
28
- if (isWorkflowModule(loaded)) modules.push(loaded);
29
- } catch (error) {
30
- // Drop-in loading depends on the runtime resolving TS + the bundled typebox.
31
- // Failures here are non-fatal: the static registry still works.
32
- console.error(`[workflow-engine] skipped ${name}: ${error instanceof Error ? error.message : String(error)}`);
33
- }
34
- }
35
- return modules;
36
- }
37
-
38
- /**
39
- * All available workflows by name. Static registry wins on name collisions, so the
40
- * bundled example is always the verified one even if a same-named file is dropped in.
41
- */
42
- export async function discoverWorkflows(repoDir: string): Promise<Map<string, WorkflowModule>> {
43
- const byName = new Map<string, WorkflowModule>();
44
- for (const mod of BUILTIN_WORKFLOWS) byName.set(mod.meta.name, mod);
45
-
46
- const dynamic = [
47
- ...(await loadDir(join(repoDir, "workflows"))),
48
- ...(await loadDir(join(homedir(), ".pi", "agent", "workflows"))),
49
- ];
50
- for (const mod of dynamic) {
51
- if (!byName.has(mod.meta.name)) byName.set(mod.meta.name, mod);
52
- }
53
- return byName;
54
- }