pi-workflow-engine 0.1.1 → 0.3.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/README.md +109 -56
- package/package.json +2 -1
- package/src/advisory-schema.ts +64 -0
- package/src/agent-runner.ts +8 -4
- package/src/engine.ts +18 -2
- package/src/index.ts +79 -14
- package/src/progress.ts +258 -23
- package/src/types.ts +29 -0
- package/src/ui/workflow-format.ts +83 -0
- package/src/ui/workflow-inspector.ts +248 -0
- package/src/ui/workflow-result-renderer.ts +195 -0
- package/src/ui/workflow-widget.ts +143 -0
- package/src/workflows.ts +4 -1
- package/workflows/code-review.ts +163 -58
- package/workflows/diagnose.ts +305 -0
- package/workflows/perf-review.ts +295 -0
- package/workflows/refactor-scout.ts +289 -0
package/README.md
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
<img src="assets/preview.png" width="200" alt="pi-workflow-engine — a central agent orchestrating a ring of subagents" />
|
|
3
|
-
</p>
|
|
1
|
+

|
|
4
2
|
|
|
5
3
|
# pi-workflow-engine
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
Claude Code style 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
|
+
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.
|
|
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,154 @@ Or from npm:
|
|
|
18
35
|
pi install npm:pi-workflow-engine
|
|
19
36
|
```
|
|
20
37
|
|
|
21
|
-
That's all
|
|
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:
|
|
22
39
|
|
|
23
40
|
```bash
|
|
24
41
|
pi list
|
|
25
42
|
```
|
|
26
43
|
|
|
27
|
-
Scope it to a single repo instead of globally
|
|
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
|
|
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
|
|
42
|
-
/workflow
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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:
|
|
89
|
+
|
|
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.
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
The review lenses live in `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
|
|
71
|
-
| `parallel(thunks)` | Runs every thunk concurrently and waits for all
|
|
72
|
-
| `pipeline(items, ...stages)` | Runs each item through all stages independently
|
|
73
|
-
| `phase(title)` / `log(msg)` |
|
|
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 = {
|
|
114
|
+
export const meta: WorkflowMeta = {
|
|
115
|
+
name: "my-workflow",
|
|
116
|
+
description: "Find, verify, and summarize something important.",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const FindingSchema = Type.Object({
|
|
120
|
+
summary: Type.String(),
|
|
121
|
+
file: Type.String(),
|
|
122
|
+
line: Type.Optional(Type.Number()),
|
|
123
|
+
});
|
|
80
124
|
|
|
81
|
-
export default async function run({ agent, parallel,
|
|
125
|
+
export default async function run({ agent, parallel, phase }: WorkflowApi) {
|
|
82
126
|
phase("Find");
|
|
83
|
-
|
|
84
|
-
|
|
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`**
|
|
91
|
-
- **Structured output is a terminating tool
|
|
92
|
-
- **A single global semaphore caps concurrent agents**, so `parallel
|
|
93
|
-
- **
|
|
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
|
|
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
|
|
103
|
-
bun run typecheck
|
|
104
|
-
bun scripts/smoke.ts
|
|
152
|
+
bun install
|
|
153
|
+
bun run typecheck
|
|
154
|
+
bun scripts/smoke.ts
|
|
105
155
|
```
|
|
106
156
|
|
|
107
|
-
Load your working copy into a session without installing it
|
|
157
|
+
Load your working copy into a session without installing it. This is ephemeral and overrides nothing:
|
|
108
158
|
|
|
109
159
|
```bash
|
|
110
160
|
pi -e ./src/index.ts -p "/workflow ping"
|
|
111
161
|
```
|
|
112
162
|
|
|
113
|
-
|
|
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.
|
|
114
164
|
|
|
115
|
-
|
|
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`.
|
|
116
166
|
|
|
117
167
|
### Layout
|
|
118
168
|
|
|
119
|
-
```
|
|
169
|
+
```text
|
|
120
170
|
src/
|
|
121
|
-
index.ts extension entry
|
|
171
|
+
index.ts extension entry; registers the command and tool
|
|
122
172
|
types.ts WorkflowApi / WorkflowModule contracts
|
|
123
|
-
agent-runner.ts
|
|
173
|
+
agent-runner.ts createAgentSession + terminating-tool schema bridge
|
|
124
174
|
concurrency.ts Semaphore, parallel(), pipeline()
|
|
125
|
-
engine.ts runWorkflow()
|
|
175
|
+
engine.ts runWorkflow(); binds primitives to one run
|
|
126
176
|
progress.ts live phase/agent tree via ctx.ui.setWidget
|
|
127
177
|
discovery.ts static registry + best-effort drop-in loading
|
|
128
178
|
workflows/
|
|
129
|
-
code-review.ts
|
|
130
|
-
|
|
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
|
|
131
184
|
```
|
|
132
185
|
|
|
133
186
|
## License
|
|
134
187
|
|
|
135
|
-
MIT
|
|
188
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-workflow-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A customisable multi-agent workflow-orchestration engine for the pi coding agent.",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test:ui": "bun scripts/test-workflow-ui.ts",
|
|
19
20
|
"prepublishOnly": "tsc --noEmit"
|
|
20
21
|
},
|
|
21
22
|
"peerDependencies": {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const AdvisorySeveritySchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
|
|
4
|
+
description: "Impact level of an advisory finding.",
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const AdvisoryConfidenceSchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
|
|
8
|
+
description: "Confidence that the finding is actionable.",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const AdvisoryVerifierVerdictSchema = Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE"), Type.Literal("REFUTED")], {
|
|
12
|
+
description: "Verifier judgment for a candidate finding.",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const AdvisoryLocationSchema = Type.Object({
|
|
16
|
+
file: Type.String({ description: "Repository-relative file path." }),
|
|
17
|
+
line: Type.Optional(Type.Number({ description: "Relevant 1-based line number when known." })),
|
|
18
|
+
symbol: Type.Optional(Type.String({ description: "Relevant function, class, type, or other symbol when known." })),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const AdvisoryCandidateSchema = Type.Object({
|
|
22
|
+
summary: Type.String({ description: "One-line candidate finding or hypothesis." }),
|
|
23
|
+
category: Type.String({ description: "Workflow-specific category such as bug, duplication, root-cause, or io." }),
|
|
24
|
+
locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
|
|
25
|
+
impact: Type.String({ description: "Concrete consequence if this candidate is real." }),
|
|
26
|
+
recommendation: Type.Optional(Type.String({ description: "Optional first-step recommendation before synthesis." })),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const AdvisoryCandidatesSchema = Type.Object({
|
|
30
|
+
candidates: Type.Array(AdvisoryCandidateSchema, { description: "Candidate advisory findings or hypotheses." }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const AdvisoryVerdictSchema = Type.Object({
|
|
34
|
+
verdict: AdvisoryVerifierVerdictSchema,
|
|
35
|
+
evidence: Type.Array(Type.String({ description: "Quoted or cited evidence supporting the verdict." })),
|
|
36
|
+
confidence: Type.Optional(AdvisoryConfidenceSchema),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const AdvisoryFindingSchema = Type.Object({
|
|
40
|
+
summary: Type.String({ description: "One-line final finding." }),
|
|
41
|
+
category: Type.String({ description: "Workflow-specific category for grouping and display." }),
|
|
42
|
+
severity: AdvisorySeveritySchema,
|
|
43
|
+
confidence: AdvisoryConfidenceSchema,
|
|
44
|
+
locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
|
|
45
|
+
evidence: Type.Array(Type.String({ description: "Quoted or cited evidence." })),
|
|
46
|
+
impact: Type.String({ description: "Concrete user, runtime, maintenance, or performance impact." }),
|
|
47
|
+
recommendation: Type.String({ description: "Advisory next action; not an automatic edit." }),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const AdvisoryReportSchema = Type.Object({
|
|
51
|
+
summary: Type.String({ description: "Short overall advisory summary." }),
|
|
52
|
+
findings: Type.Array(AdvisoryFindingSchema, { description: "Verified and ranked advisory findings." }),
|
|
53
|
+
nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
|
|
57
|
+
export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
|
|
58
|
+
export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
|
|
59
|
+
export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
|
|
60
|
+
export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
|
|
61
|
+
export type AdvisoryCandidates = Static<typeof AdvisoryCandidatesSchema>;
|
|
62
|
+
export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
|
|
63
|
+
export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
|
|
64
|
+
export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
|
package/src/agent-runner.ts
CHANGED
|
@@ -49,11 +49,13 @@ function lastAssistantText(state: AgentState): string {
|
|
|
49
49
|
export async function runAgent(rc: RunContext, prompt: string, opts: AgentOptions = {}): Promise<unknown> {
|
|
50
50
|
const label = opts.label ?? "agent";
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// Track queued agents before acquiring a global concurrency slot.
|
|
53
|
+
const rowId = rc.progress.agentQueued(opts.phase, label);
|
|
53
54
|
return rc.semaphore.run(async () => {
|
|
54
|
-
rc.progress.agentStart(opts.phase, label);
|
|
55
|
+
rc.progress.agentStart(opts.phase, label, rowId);
|
|
55
56
|
|
|
56
57
|
let captured: unknown = null;
|
|
58
|
+
let failed = false;
|
|
57
59
|
const customTools: ToolDefinition[] = opts.schema
|
|
58
60
|
? [
|
|
59
61
|
defineTool({
|
|
@@ -92,7 +94,7 @@ export async function runAgent(rc: RunContext, prompt: string, opts: AgentOption
|
|
|
92
94
|
|
|
93
95
|
const unsubscribe = session.subscribe((event) => {
|
|
94
96
|
if (event.type === "tool_execution_start" && event.toolName !== FINAL_TOOL) {
|
|
95
|
-
rc.progress.agentTool(label, event.toolName);
|
|
97
|
+
rc.progress.agentTool(label, event.toolName, rowId);
|
|
96
98
|
}
|
|
97
99
|
});
|
|
98
100
|
|
|
@@ -107,12 +109,14 @@ export async function runAgent(rc: RunContext, prompt: string, opts: AgentOption
|
|
|
107
109
|
}
|
|
108
110
|
return lastAssistantText(session.state);
|
|
109
111
|
} catch (error) {
|
|
112
|
+
failed = true;
|
|
113
|
+
rc.progress.agentFailed(label, error, rowId);
|
|
110
114
|
rc.progress.log(`${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
115
|
throw error;
|
|
112
116
|
} finally {
|
|
113
117
|
unsubscribe();
|
|
114
118
|
session.dispose();
|
|
115
|
-
rc.progress.agentDone(label);
|
|
119
|
+
if (!failed) rc.progress.agentDone(label, rowId);
|
|
116
120
|
}
|
|
117
121
|
});
|
|
118
122
|
}
|
package/src/engine.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { parallel, pipeline, Semaphore } from "./concurrency.ts";
|
|
4
4
|
import { runAgent, type RunContext } from "./agent-runner.ts";
|
|
5
5
|
import { ProgressTracker } from "./progress.ts";
|
|
6
|
-
import type { AgentOptions, WorkflowApi, WorkflowModule } from "./types.ts";
|
|
6
|
+
import type { AgentOptions, WorkflowApi, WorkflowModule, WorkflowRunOptions } from "./types.ts";
|
|
7
|
+
import { WorkflowInspector } from "./ui/workflow-inspector.ts";
|
|
7
8
|
|
|
8
9
|
/** Default global cap on concurrent agents per run. */
|
|
9
10
|
const DEFAULT_CONCURRENCY = Math.min(8, Math.max(2, cpus().length));
|
|
@@ -12,8 +13,22 @@ const DEFAULT_CONCURRENCY = Math.min(8, Math.max(2, cpus().length));
|
|
|
12
13
|
* Run a workflow module: build the per-run primitives (binding agent/parallel/pipeline
|
|
13
14
|
* to a shared semaphore + progress tracker), invoke the workflow, return its result.
|
|
14
15
|
*/
|
|
15
|
-
export async function runWorkflow(
|
|
16
|
+
export async function runWorkflow(
|
|
17
|
+
ctx: ExtensionContext,
|
|
18
|
+
mod: WorkflowModule,
|
|
19
|
+
args: string,
|
|
20
|
+
options: WorkflowRunOptions = {},
|
|
21
|
+
): Promise<unknown> {
|
|
16
22
|
const progress = new ProgressTracker(ctx, mod.meta.name);
|
|
23
|
+
if (options.inspect && ctx.hasUI) {
|
|
24
|
+
void ctx.ui
|
|
25
|
+
.custom<void>(
|
|
26
|
+
(tui, theme, _keybindings, done) => new WorkflowInspector(() => progress.snapshot(), tui, theme, () => done(undefined)),
|
|
27
|
+
{ overlay: true, overlayOptions: { anchor: "right-center", width: "60%", maxHeight: "80%", margin: 1 } },
|
|
28
|
+
)
|
|
29
|
+
.catch((error: unknown) => progress.log(`inspector failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
const rc: RunContext = {
|
|
18
33
|
cwd: ctx.cwd,
|
|
19
34
|
hostModel: ctx.model,
|
|
@@ -31,6 +46,7 @@ export async function runWorkflow(ctx: ExtensionContext, mod: WorkflowModule, ar
|
|
|
31
46
|
pipeline,
|
|
32
47
|
phase: (title) => progress.phase(title),
|
|
33
48
|
log: (message) => progress.log(message),
|
|
49
|
+
progress: (event) => progress.event(event),
|
|
34
50
|
args,
|
|
35
51
|
cwd: ctx.cwd,
|
|
36
52
|
signal: ctx.signal,
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
5
|
import { discoverWorkflows } from "./discovery.ts";
|
|
5
6
|
import { runWorkflow } from "./engine.ts";
|
|
7
|
+
import type { WorkflowModule, WorkflowRunOptions } from "./types.ts";
|
|
8
|
+
import { isWorkflowResult, renderWorkflowResult, type WorkflowResultEnvelope } from "./ui/workflow-result-renderer.ts";
|
|
6
9
|
|
|
7
10
|
/** Repo root (this file lives in <repo>/src/index.ts). */
|
|
8
11
|
const REPO_DIR = fileURLToPath(new URL("..", import.meta.url));
|
|
@@ -18,34 +21,84 @@ function formatReport(name: string, result: unknown): string {
|
|
|
18
21
|
return [`## Workflow: ${name}`, "", "```json", JSON.stringify(result, null, 2), "```"].join("\n");
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
function workflowEnvelope(name: string, result: unknown): WorkflowResultEnvelope {
|
|
25
|
+
return { name, result, completedAt: Date.now() };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface WorkflowInvocation {
|
|
29
|
+
name: string;
|
|
30
|
+
args: string;
|
|
31
|
+
options: WorkflowRunOptions;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseWorkflowInvocation(input: string): WorkflowInvocation {
|
|
35
|
+
const trimmed = input.trim();
|
|
36
|
+
const space = trimmed.indexOf(" ");
|
|
37
|
+
const name = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
38
|
+
const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
39
|
+
const inspect = /(?:^|\s)--inspect(?:\s|$)/.test(rest);
|
|
40
|
+
const cleanedArgs = rest.replace(/(?:^|\s)--inspect(?=\s|$)/g, " ").trim();
|
|
41
|
+
return { name, args: cleanedArgs, options: inspect ? { inspect: true } : {} };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function pickWorkflow(
|
|
45
|
+
workflows: ReadonlyMap<string, WorkflowModule>,
|
|
46
|
+
ctx: ExtensionCommandContext,
|
|
47
|
+
): Promise<WorkflowInvocation | undefined> {
|
|
48
|
+
const choices = [...workflows.values()].map((mod) => `${mod.meta.name} — ${mod.meta.description}`);
|
|
49
|
+
const choice = await ctx.ui.select("Run workflow", choices);
|
|
50
|
+
if (!choice) return undefined;
|
|
51
|
+
|
|
52
|
+
const separator = choice.indexOf(" — ");
|
|
53
|
+
const name = separator === -1 ? choice : choice.slice(0, separator);
|
|
54
|
+
const args = name === "code-review" ? (await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff"))?.trim() ?? "" : "";
|
|
55
|
+
const inspect = await ctx.ui.confirm("Open inspector?", "Open a live workflow inspector while this workflow runs?");
|
|
56
|
+
return { name, args, options: { inspect } };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function sendWorkflowResult(
|
|
60
|
+
pi: ExtensionAPI,
|
|
61
|
+
ctx: ExtensionCommandContext,
|
|
62
|
+
name: string,
|
|
63
|
+
mod: WorkflowModule,
|
|
64
|
+
args: string,
|
|
65
|
+
options: WorkflowRunOptions,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const result = await runWorkflow(ctx, mod, args, options);
|
|
68
|
+
pi.sendMessage(
|
|
69
|
+
{ customType: "workflow-result", content: formatReport(name, result), display: true, details: workflowEnvelope(name, result) },
|
|
70
|
+
{ triggerTurn: false },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
export default function workflowEngine(pi: ExtensionAPI): void {
|
|
75
|
+
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
|
|
76
|
+
const details = message.details;
|
|
77
|
+
if (isWorkflowResult(details)) return renderWorkflowResult(details.name, details.result, expanded, theme);
|
|
78
|
+
return renderWorkflowResult("workflow", details ?? message.content, expanded, theme);
|
|
79
|
+
});
|
|
80
|
+
|
|
22
81
|
// /workflow <name> [args] — user-invoked.
|
|
23
82
|
pi.registerCommand("workflow", {
|
|
24
83
|
description: "Run a multi-agent workflow: /workflow <name> [args]",
|
|
25
84
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
26
|
-
const trimmed = args.trim();
|
|
27
|
-
const space = trimmed.indexOf(" ");
|
|
28
|
-
const name = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
29
|
-
const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
30
|
-
|
|
31
85
|
const workflows = await discoverWorkflows(REPO_DIR);
|
|
32
86
|
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
87
|
+
const direct = parseWorkflowInvocation(args);
|
|
88
|
+
const invocation = direct.name ? direct : ctx.hasUI ? await pickWorkflow(workflows, ctx) : undefined;
|
|
33
89
|
|
|
34
|
-
if (!
|
|
90
|
+
if (!invocation) {
|
|
35
91
|
ctx.ui.notify(`Usage: /workflow <name> [args]. Available: ${available}`, "warning");
|
|
36
92
|
return;
|
|
37
93
|
}
|
|
38
|
-
|
|
94
|
+
|
|
95
|
+
const mod = workflows.get(invocation.name);
|
|
39
96
|
if (!mod) {
|
|
40
|
-
ctx.ui.notify(`Unknown workflow "${name}". Available: ${available}`, "error");
|
|
97
|
+
ctx.ui.notify(`Unknown workflow "${invocation.name}". Available: ${available}`, "error");
|
|
41
98
|
return;
|
|
42
99
|
}
|
|
43
100
|
|
|
44
|
-
|
|
45
|
-
pi.sendMessage(
|
|
46
|
-
{ customType: "workflow-result", content: formatReport(name, result), display: true, details: result },
|
|
47
|
-
{ triggerTurn: false },
|
|
48
|
-
);
|
|
101
|
+
await sendWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, invocation.options);
|
|
49
102
|
},
|
|
50
103
|
});
|
|
51
104
|
|
|
@@ -59,6 +112,18 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
59
112
|
name: Type.String({ description: "Workflow name, e.g. code-review" }),
|
|
60
113
|
args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
|
|
61
114
|
}),
|
|
115
|
+
renderCall(args, theme) {
|
|
116
|
+
const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
|
|
117
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name)}${suffix}`, 0, 0);
|
|
118
|
+
},
|
|
119
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
120
|
+
if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
|
|
121
|
+
const details = result.details;
|
|
122
|
+
if (isWorkflowResult(details)) return renderWorkflowResult(details.name, details.result, expanded, theme);
|
|
123
|
+
const first = result.content[0];
|
|
124
|
+
const text = first?.type === "text" ? first.text : "Workflow finished.";
|
|
125
|
+
return new Text(theme.fg("muted", text), 0, 0);
|
|
126
|
+
},
|
|
62
127
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
63
128
|
const workflows = await discoverWorkflows(REPO_DIR);
|
|
64
129
|
const mod = workflows.get(params.name);
|
|
@@ -70,7 +135,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
70
135
|
};
|
|
71
136
|
}
|
|
72
137
|
const result = await runWorkflow(ctx, mod, params.args ?? "");
|
|
73
|
-
return { content: [{ type: "text", text: summarize(result) }], details: result };
|
|
138
|
+
return { content: [{ type: "text", text: summarize(result) }], details: workflowEnvelope(params.name, result) };
|
|
74
139
|
},
|
|
75
140
|
});
|
|
76
141
|
}
|