pi-agent-flow 0.2.8 → 0.2.11
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.ts +3 -3
- package/flow.ts +5 -4
- package/index.ts +42 -48
- 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.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
|
|
|
@@ -423,7 +423,7 @@ flow [type] accomplished
|
|
|
423
423
|
);
|
|
424
424
|
|
|
425
425
|
// Collect all requested flow names
|
|
426
|
-
const requested = new Set(params.flow.map((f) => f.type));
|
|
426
|
+
const requested = new Set(params.flow.map((f) => f.type.toLowerCase()));
|
|
427
427
|
|
|
428
428
|
// Cycle check
|
|
429
429
|
if (preventCycles) {
|
|
@@ -483,58 +483,52 @@ flow [type] accomplished
|
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
let lastStreamingText = "";
|
|
486
|
+
let lastEmittedText: string | undefined;
|
|
486
487
|
const emitProgress = (streamingText?: string) => {
|
|
487
488
|
if (!onUpdate) return;
|
|
488
|
-
if (streamingText) lastStreamingText = streamingText;
|
|
489
|
+
if (streamingText !== undefined) lastStreamingText = streamingText;
|
|
490
|
+
const text = lastStreamingText || "";
|
|
491
|
+
if (text === lastEmittedText) return;
|
|
492
|
+
lastEmittedText = text;
|
|
489
493
|
onUpdate({
|
|
490
|
-
content: [{ type: "text", text
|
|
494
|
+
content: [{ type: "text", text }],
|
|
491
495
|
details: makeDetails([...allResults]),
|
|
492
496
|
});
|
|
493
497
|
};
|
|
494
498
|
|
|
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;
|
|
499
|
+
if (onUpdate) emitProgress();
|
|
500
|
+
|
|
501
|
+
const results = await mapFlowConcurrent(params.flow, 4, async (item, index) => {
|
|
502
|
+
const normalizedType = item.type.toLowerCase();
|
|
503
|
+
const targetFlow = flows.find((f) => f.name === normalizedType);
|
|
504
|
+
const effectiveMaxDepth =
|
|
505
|
+
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
506
|
+
|
|
507
|
+
const shouldInheritContext = targetFlow?.inheritContext !== false;
|
|
508
|
+
const result = await runFlow({
|
|
509
|
+
cwd: ctx.cwd,
|
|
510
|
+
flows,
|
|
511
|
+
flowName: normalizedType,
|
|
512
|
+
intent: item.intent,
|
|
513
|
+
taskCwd: item.cwd,
|
|
514
|
+
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
515
|
+
parentDepth: currentDepth,
|
|
516
|
+
parentFlowStack: ancestorFlowStack,
|
|
517
|
+
maxDepth: effectiveMaxDepth,
|
|
518
|
+
preventCycles,
|
|
519
|
+
signal,
|
|
520
|
+
onUpdate: (partial) => {
|
|
521
|
+
if (partial.details?.results[0]) {
|
|
522
|
+
allResults[index] = partial.details.results[0];
|
|
523
|
+
emitProgress(partial.content?.[0]?.text);
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
makeDetails,
|
|
534
527
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
528
|
+
allResults[index] = result;
|
|
529
|
+
emitProgress();
|
|
530
|
+
return result;
|
|
531
|
+
});
|
|
538
532
|
|
|
539
533
|
// Build tool result with FULL flow output — no truncation
|
|
540
534
|
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
|
}
|