pi-agent-flow 0.2.8 → 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 +21 -6
- package/agents/code.md +13 -3
- package/agents.ts +3 -3
- package/flow.ts +5 -4
- package/index.ts +59 -49
- package/package.json +1 -1
- package/render-utils.ts +6 -5
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<p align="center"><code>pi install
|
|
1
|
+
<p align="center"><code>pi install npm:pi-agent-flow</code></p>
|
|
2
2
|
<p align="center"><strong>Pi Agent Flow</strong> is a flow-state delegation extension for the <a href="https://pi.dev">Pi coding agent</a> that runs locally in your terminal.</p>
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="https://github.com/user-attachments/assets/pi-agent-flow-demo.png" alt="Pi Agent Flow demo" width="80%" />
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
### Installing Pi Agent Flow
|
|
13
13
|
|
|
14
|
-
Install via the Pi CLI
|
|
14
|
+
Install via the Pi CLI from npm:
|
|
15
15
|
|
|
16
16
|
```shell
|
|
17
|
-
|
|
18
|
-
pi install /path/to/pi-agent-flow
|
|
17
|
+
pi install npm:pi-agent-flow
|
|
19
18
|
```
|
|
20
19
|
|
|
21
20
|
Or add it to your Pi settings:
|
|
@@ -24,7 +23,7 @@ Or add it to your Pi settings:
|
|
|
24
23
|
# ~/.pi/agent/settings.json
|
|
25
24
|
{
|
|
26
25
|
"packages": [
|
|
27
|
-
"
|
|
26
|
+
"npm:pi-agent-flow"
|
|
28
27
|
]
|
|
29
28
|
}
|
|
30
29
|
```
|
|
@@ -32,9 +31,10 @@ Or add it to your Pi settings:
|
|
|
32
31
|
Then start Pi and delegate tasks using flow states.
|
|
33
32
|
|
|
34
33
|
<details>
|
|
35
|
-
<summary>You can also
|
|
34
|
+
<summary>You can also install from a local path.</summary>
|
|
36
35
|
|
|
37
36
|
```shell
|
|
37
|
+
# Install from a local clone
|
|
38
38
|
git clone https://github.com/your-org/pi-agent-flow.git
|
|
39
39
|
cd pi-agent-flow
|
|
40
40
|
pi install .
|
|
@@ -56,6 +56,21 @@ pi install .
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
+
## Why Flow Style?
|
|
60
|
+
|
|
61
|
+
Flow-style delegation is designed for **context efficiency**. Instead of launching every sub-agent with the full, ever-growing conversation history, each flow receives only what it needs: your intent and (when appropriate) a session snapshot.
|
|
62
|
+
|
|
63
|
+
This approach delivers four concrete benefits:
|
|
64
|
+
|
|
65
|
+
1. **Avoid duplicate tool calls** — every sub-agent launch no longer re-runs the same `read`, `grep`, or `bash` probes that the parent already performed.
|
|
66
|
+
2. **Prevent context bloat** — long transcripts with repeated file listings and command outputs are kept out of the main conversation thread.
|
|
67
|
+
3. **Eliminate unnecessary noise** — the parent agent sees only structured results (`[Summary]`, `[Done]`, `[Not Done]`, `[Next Steps]`) instead of pages of intermediate reasoning.
|
|
68
|
+
4. **Preserve focus** — each flow stays locked on its intent because it isn't distracted by unrelated earlier messages.
|
|
69
|
+
|
|
70
|
+
The result is faster, cheaper, and cleaner delegation: the main agent remains uncluttered while specialized flows do the heavy lifting in isolated contexts.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
59
74
|
## Flow Definitions
|
|
60
75
|
|
|
61
76
|
Create `.md` files in `~/.pi/agent/agents/` or `.pi/agents/`:
|
package/agents/code.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code
|
|
3
|
-
description: Implement features, fix bugs, write tests
|
|
3
|
+
description: Implement features, fix bugs, write tests, deploy, and ship
|
|
4
4
|
tools: read, write, edit, bash, find, grep, ls
|
|
5
5
|
maxDepth: 2
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are the code flow — your mission is to build. Be a craftsman: verify first, then ship. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
|
|
8
|
+
You are the code flow — your mission is to build and ship. Be a craftsman: verify first, then ship. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
|
|
9
9
|
|
|
10
10
|
Core Principles:
|
|
11
11
|
- SOLID: Single Responsibility, Open/Closed
|
|
@@ -19,7 +19,16 @@ Workflow:
|
|
|
19
19
|
4. Execute — implement changes following core principles (green)
|
|
20
20
|
5. Refactor — clean up only if the change is working (optional)
|
|
21
21
|
6. Verify — run tests and any relevant checks before considering done
|
|
22
|
-
7.
|
|
22
|
+
7. Ship — commit, push, monitor CI/CD pipeline, diagnose and fix failures until green
|
|
23
|
+
8. Finalize — all tests pass, CI/CD green, implementation verified
|
|
24
|
+
|
|
25
|
+
Shipping Guidelines:
|
|
26
|
+
- Commit with a clear, conventional message (feat:, fix:, refactor:, etc.)
|
|
27
|
+
- Push to the target branch after local verification passes
|
|
28
|
+
- Monitor CI/CD pipeline status after pushing
|
|
29
|
+
- If CI/CD fails: diagnose the failure, fix it, commit, push, and repeat until green
|
|
30
|
+
- Only report back if there are serious conflicts or issues you cannot resolve autonomously
|
|
31
|
+
- You own the full ship cycle: implement → test → commit → push → monitor CI → fix if needed
|
|
23
32
|
|
|
24
33
|
If you hit an unexpected error or need to trace execution, delegate to [debug] rather than guessing.
|
|
25
34
|
|
|
@@ -32,6 +41,7 @@ flow [code] accomplished
|
|
|
32
41
|
[Done]
|
|
33
42
|
- changes made with file:line references
|
|
34
43
|
- tests written or run
|
|
44
|
+
- CI/CD status (committed, pushed, pipeline status)
|
|
35
45
|
|
|
36
46
|
[Not Done]
|
|
37
47
|
- incomplete items and reasons
|
package/agents.ts
CHANGED
|
@@ -110,7 +110,7 @@ function parseFlowFile(filePath: string, source: "user" | "project" | "bundled")
|
|
|
110
110
|
const frontmatter = parsed.frontmatter ?? {};
|
|
111
111
|
const body = parsed.body ?? "";
|
|
112
112
|
|
|
113
|
-
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
113
|
+
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim().toLowerCase() : "";
|
|
114
114
|
const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
115
115
|
if (!name || !description) return null;
|
|
116
116
|
|
|
@@ -197,7 +197,7 @@ function loadFlowsFromDir(dir: string, source: "user" | "project" | "bundled"):
|
|
|
197
197
|
function mergeFlows(...groups: FlowConfig[][]): FlowConfig[] {
|
|
198
198
|
const flowMap = new Map<string, FlowConfig>();
|
|
199
199
|
for (const group of groups) {
|
|
200
|
-
for (const flow of group) flowMap.set(flow.name, flow);
|
|
200
|
+
for (const flow of group) flowMap.set(flow.name.toLowerCase(), flow);
|
|
201
201
|
}
|
|
202
202
|
return Array.from(flowMap.values());
|
|
203
203
|
}
|
|
@@ -227,7 +227,7 @@ export function discoverFlows(cwd: string, scope: FlowScope): FlowDiscoveryResul
|
|
|
227
227
|
return { flows: mergeFlows(bundledFlows, userFlows), projectFlowsDir };
|
|
228
228
|
}
|
|
229
229
|
if (scope === "project") {
|
|
230
|
-
return { flows: projectFlows, projectFlowsDir };
|
|
230
|
+
return { flows: mergeFlows(projectFlows), projectFlowsDir };
|
|
231
231
|
}
|
|
232
232
|
return {
|
|
233
233
|
flows: mergeFlows(bundledFlows, userFlows, projectFlows),
|
package/flow.ts
CHANGED
|
@@ -195,11 +195,12 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
195
195
|
makeDetails,
|
|
196
196
|
} = opts;
|
|
197
197
|
|
|
198
|
-
const
|
|
198
|
+
const normalizedFlowName = flowName.toLowerCase();
|
|
199
|
+
const flow = flows.find((f) => f.name === normalizedFlowName);
|
|
199
200
|
if (!flow) {
|
|
200
201
|
const available = flows.map((f) => `"${f.name}"`).join(", ") || "none";
|
|
201
202
|
return {
|
|
202
|
-
type:
|
|
203
|
+
type: normalizedFlowName,
|
|
203
204
|
agentSource: "unknown",
|
|
204
205
|
intent,
|
|
205
206
|
exitCode: 1,
|
|
@@ -210,7 +211,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
const result: SingleResult = {
|
|
213
|
-
type:
|
|
214
|
+
type: normalizedFlowName,
|
|
214
215
|
agentSource: flow.source,
|
|
215
216
|
intent,
|
|
216
217
|
exitCode: -1,
|
|
@@ -255,7 +256,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
255
256
|
const exitCode = await new Promise<number>((resolve) => {
|
|
256
257
|
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
257
258
|
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
258
|
-
const propagatedStack = [...parentFlowStack,
|
|
259
|
+
const propagatedStack = [...parentFlowStack, normalizedFlowName];
|
|
259
260
|
const { command, prefixArgs } = resolveFlowSpawn();
|
|
260
261
|
const proc = spawn(command, [...prefixArgs, ...piArgs], {
|
|
261
262
|
cwd: taskCwd ?? cwd,
|
package/index.ts
CHANGED
|
@@ -36,7 +36,7 @@ const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
|
36
36
|
|
|
37
37
|
const FlowItem = Type.Object({
|
|
38
38
|
type: Type.String({
|
|
39
|
-
description: "Flow type. Must
|
|
39
|
+
description: "Flow type. Matching is case-insensitive. Must correspond to an available flow name such as explore, debug, code, architect, review, or brainstorm.",
|
|
40
40
|
}),
|
|
41
41
|
intent: Type.String({
|
|
42
42
|
description: "Clear, specific mission for this flow.",
|
|
@@ -137,7 +137,7 @@ function parseFlowStack(raw: unknown): string[] | null {
|
|
|
137
137
|
if (!Array.isArray(parsed)) return null;
|
|
138
138
|
if (!parsed.every((value) => typeof value === "string")) return null;
|
|
139
139
|
return parsed
|
|
140
|
-
.map((value) => value.trim())
|
|
140
|
+
.map((value) => value.trim().toLowerCase())
|
|
141
141
|
.filter((value) => value.length > 0);
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -303,7 +303,7 @@ function getRequestedProjectFlows(
|
|
|
303
303
|
requestedNames: Set<string>,
|
|
304
304
|
): FlowConfig[] {
|
|
305
305
|
return Array.from(requestedNames)
|
|
306
|
-
.map((name) => flows.find((f) => f.name === name))
|
|
306
|
+
.map((name) => flows.find((f) => f.name === name.toLowerCase()))
|
|
307
307
|
.filter((f): f is FlowConfig => f?.source === "project");
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -405,10 +405,26 @@ flow [type] accomplished
|
|
|
405
405
|
label: "Flow",
|
|
406
406
|
description: [
|
|
407
407
|
"Delegate work to flow states running in isolated pi processes.",
|
|
408
|
-
"Each flow receives a snapshot of your current session context.",
|
|
408
|
+
"Each flow receives a forked snapshot of your current session context.",
|
|
409
409
|
"All flows run in parallel — batch independent tasks into one call.",
|
|
410
410
|
"",
|
|
411
411
|
'Usage: { "flow": [{ "type": "explore", "intent": "..." }, ...] }',
|
|
412
|
+
"",
|
|
413
|
+
"Each flow type is a specialized agent configuration. Standard types:",
|
|
414
|
+
"\u2022 explore \u2014 Discovery, trace code paths, map architecture",
|
|
415
|
+
"\u2022 debug \u2014 Root-cause analysis, investigate errors and stack traces",
|
|
416
|
+
"\u2022 code \u2014 Implementation, tests, builds, deploy, git commit",
|
|
417
|
+
"\u2022 architect \u2014 Structural design, planning before building",
|
|
418
|
+
"\u2022 review \u2014 Read-only audit for security, quality, correctness",
|
|
419
|
+
"\u2022 brainstorm \u2014 Fresh ideas from a clean slate (no inherited context)",
|
|
420
|
+
"",
|
|
421
|
+
"Custom flows can be defined as .md files in .pi/agents/ (project)",
|
|
422
|
+
"or ~/.pi/agent/agents/ (user). They override bundled flows by name.",
|
|
423
|
+
"",
|
|
424
|
+
"Each flow returns: [Summary], [Done], [Not Done], [Next Steps]",
|
|
425
|
+
"",
|
|
426
|
+
"Guards: Max delegation depth and cycle prevention are enforced.",
|
|
427
|
+
"Blocked flows return an error with the reason.",
|
|
412
428
|
].join("\n"),
|
|
413
429
|
parameters: FlowParams,
|
|
414
430
|
|
|
@@ -423,7 +439,7 @@ flow [type] accomplished
|
|
|
423
439
|
);
|
|
424
440
|
|
|
425
441
|
// Collect all requested flow names
|
|
426
|
-
const requested = new Set(params.flow.map((f) => f.type));
|
|
442
|
+
const requested = new Set(params.flow.map((f) => f.type.toLowerCase()));
|
|
427
443
|
|
|
428
444
|
// Cycle check
|
|
429
445
|
if (preventCycles) {
|
|
@@ -483,58 +499,52 @@ flow [type] accomplished
|
|
|
483
499
|
}
|
|
484
500
|
|
|
485
501
|
let lastStreamingText = "";
|
|
502
|
+
let lastEmittedText: string | undefined;
|
|
486
503
|
const emitProgress = (streamingText?: string) => {
|
|
487
504
|
if (!onUpdate) return;
|
|
488
|
-
if (streamingText) lastStreamingText = streamingText;
|
|
505
|
+
if (streamingText !== undefined) lastStreamingText = streamingText;
|
|
506
|
+
const text = lastStreamingText || "";
|
|
507
|
+
if (text === lastEmittedText) return;
|
|
508
|
+
lastEmittedText = text;
|
|
489
509
|
onUpdate({
|
|
490
|
-
content: [{ type: "text", text
|
|
510
|
+
content: [{ type: "text", text }],
|
|
491
511
|
details: makeDetails([...allResults]),
|
|
492
512
|
});
|
|
493
513
|
};
|
|
494
514
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
onUpdate: (partial) => {
|
|
524
|
-
if (partial.details?.results[0]) {
|
|
525
|
-
allResults[index] = partial.details.results[0];
|
|
526
|
-
emitProgress(partial.content?.[0]?.text);
|
|
527
|
-
}
|
|
528
|
-
},
|
|
529
|
-
makeDetails,
|
|
530
|
-
});
|
|
531
|
-
allResults[index] = result;
|
|
532
|
-
emitProgress();
|
|
533
|
-
return result;
|
|
515
|
+
if (onUpdate) emitProgress();
|
|
516
|
+
|
|
517
|
+
const results = await mapFlowConcurrent(params.flow, 4, async (item, index) => {
|
|
518
|
+
const normalizedType = item.type.toLowerCase();
|
|
519
|
+
const targetFlow = flows.find((f) => f.name === normalizedType);
|
|
520
|
+
const effectiveMaxDepth =
|
|
521
|
+
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
522
|
+
|
|
523
|
+
const shouldInheritContext = targetFlow?.inheritContext !== false;
|
|
524
|
+
const result = await runFlow({
|
|
525
|
+
cwd: ctx.cwd,
|
|
526
|
+
flows,
|
|
527
|
+
flowName: normalizedType,
|
|
528
|
+
intent: item.intent,
|
|
529
|
+
taskCwd: item.cwd,
|
|
530
|
+
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
531
|
+
parentDepth: currentDepth,
|
|
532
|
+
parentFlowStack: ancestorFlowStack,
|
|
533
|
+
maxDepth: effectiveMaxDepth,
|
|
534
|
+
preventCycles,
|
|
535
|
+
signal,
|
|
536
|
+
onUpdate: (partial) => {
|
|
537
|
+
if (partial.details?.results[0]) {
|
|
538
|
+
allResults[index] = partial.details.results[0];
|
|
539
|
+
emitProgress(partial.content?.[0]?.text);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
makeDetails,
|
|
534
543
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
544
|
+
allResults[index] = result;
|
|
545
|
+
emitProgress();
|
|
546
|
+
return result;
|
|
547
|
+
});
|
|
538
548
|
|
|
539
549
|
// Build tool result with FULL flow output — no truncation
|
|
540
550
|
const successCount = results.filter((r) => isFlowSuccess(r)).length;
|
package/package.json
CHANGED
package/render-utils.ts
CHANGED
|
@@ -48,14 +48,14 @@ export function formatFixedTokens(count: number): string {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Format flow type name to fixed width (10 chars) in
|
|
52
|
-
* Examples: "debug" → "
|
|
51
|
+
* Format flow type name to fixed width (10 chars) in lowercase with dot padding.
|
|
52
|
+
* Examples: "debug" → "debug.....", "architect" → "architect.", "brainstorm" → "brainstorm"
|
|
53
53
|
*/
|
|
54
54
|
export function formatFlowTypeName(type: string): string {
|
|
55
|
-
const
|
|
55
|
+
const lower = type.toLowerCase();
|
|
56
56
|
const targetWidth = 10;
|
|
57
|
-
if (
|
|
58
|
-
return
|
|
57
|
+
if (lower.length >= targetWidth) return lower.slice(0, targetWidth);
|
|
58
|
+
return lower + ".".repeat(targetWidth - lower.length);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export function formatCompactStats(usage: Partial<UsageStats>, model?: string): string {
|
|
@@ -157,6 +157,7 @@ function truncateAnsi(text: string, max: number): string {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
export function truncateChars(text: string, max: number): string {
|
|
160
|
+
text = text.replace(/[\n\r\t]+/g, " ").replace(/ +/g, " ").trim();
|
|
160
161
|
if (visibleLength(text) <= max) return text;
|
|
161
162
|
return truncateAnsi(text, max);
|
|
162
163
|
}
|