pi-agent-flow 0.2.7 → 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 +7 -6
- package/render.ts +21 -21
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 {
|
|
@@ -153,10 +153,11 @@ function truncateAnsi(text: string, max: number): string {
|
|
|
153
153
|
|
|
154
154
|
const tailRaw = takeVisibleFromEnd(text, tail);
|
|
155
155
|
|
|
156
|
-
return headResult.raw + "
|
|
156
|
+
return headResult.raw + " ... " + tailRaw;
|
|
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
|
}
|
package/render.ts
CHANGED
|
@@ -193,12 +193,12 @@ function renderFlowExpanded(
|
|
|
193
193
|
|
|
194
194
|
// Intent
|
|
195
195
|
container.addChild(new Spacer(1));
|
|
196
|
-
container.addChild(new Text(theme.fg("muted", "───
|
|
196
|
+
container.addChild(new Text(theme.fg("muted", "─── intent ───"), 0, 0));
|
|
197
197
|
container.addChild(new Text(theme.fg("dim", r.intent), 0, 0));
|
|
198
198
|
|
|
199
199
|
// Flow report (structured output)
|
|
200
200
|
container.addChild(new Spacer(1));
|
|
201
|
-
container.addChild(new Text(theme.fg("muted", "───
|
|
201
|
+
container.addChild(new Text(theme.fg("muted", "─── report ───"), 0, 0));
|
|
202
202
|
if (flowOutput) {
|
|
203
203
|
container.addChild(new Markdown(flowOutput.trim(), 0, 0, mdTheme));
|
|
204
204
|
} else {
|
|
@@ -210,7 +210,7 @@ function renderFlowExpanded(
|
|
|
210
210
|
const toolTraces = renderToolTraces(displayItems, theme);
|
|
211
211
|
if (toolTraces) {
|
|
212
212
|
container.addChild(new Spacer(1));
|
|
213
|
-
container.addChild(new Text(theme.fg("muted", "───
|
|
213
|
+
container.addChild(new Text(theme.fg("muted", "─── tool calls ───"), 0, 0));
|
|
214
214
|
container.addChild(new Text(toolTraces, 0, 0));
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -232,27 +232,27 @@ function renderFlowCollapsed(
|
|
|
232
232
|
let text = `${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
233
233
|
if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
234
234
|
|
|
235
|
-
//
|
|
235
|
+
// dir: line (intent/objective)
|
|
236
236
|
if (r.intent) {
|
|
237
|
-
text += `\n${theme.fg("dim", "├─
|
|
237
|
+
text += `\n${theme.fg("dim", "├─ dir:")} ${theme.fg("dim", truncateChars(r.intent, 50))}`;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
//
|
|
240
|
+
// exe: line (last tool call)
|
|
241
241
|
const lastTool = getLastToolCall(r.messages);
|
|
242
242
|
if (lastTool) {
|
|
243
243
|
const exeStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
244
|
-
text += `\n${theme.fg("dim", "├─
|
|
244
|
+
text += `\n${theme.fg("dim", "├─ exe:")} ${truncateChars(exeStr, 50)}`;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
//
|
|
247
|
+
// log: line (last assistant text or streaming)
|
|
248
248
|
if (flowOutput) {
|
|
249
|
-
text += `\n${theme.fg("dim", "└─
|
|
249
|
+
text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("dim", truncateChars(flowOutput, 50))}`;
|
|
250
250
|
} else if (streamingText) {
|
|
251
|
-
text += `\n${theme.fg("dim", "└─
|
|
251
|
+
text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("dim", tailText(streamingText, 50))}`;
|
|
252
252
|
} else if (error && r.errorMessage) {
|
|
253
|
-
text += `\n${theme.fg("dim", "└─
|
|
253
|
+
text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("error", truncateChars(r.errorMessage, 50))}`;
|
|
254
254
|
} else {
|
|
255
|
-
text += `\n${theme.fg("dim", "└─
|
|
255
|
+
text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("dim", "[n/a]")}`;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
return new Text(text, 0, 0);
|
|
@@ -323,7 +323,7 @@ function renderMultiFlowExpanded(
|
|
|
323
323
|
const toolTraces = renderToolTraces(displayItems, theme);
|
|
324
324
|
if (toolTraces) {
|
|
325
325
|
container.addChild(new Spacer(1));
|
|
326
|
-
container.addChild(new Text(theme.fg("muted", "───
|
|
326
|
+
container.addChild(new Text(theme.fg("muted", "─── tool calls ───"), 0, 0));
|
|
327
327
|
container.addChild(new Text(toolTraces, 0, 0));
|
|
328
328
|
}
|
|
329
329
|
}
|
|
@@ -367,26 +367,26 @@ function renderActivityPanel(
|
|
|
367
367
|
// Continuation indent for sub-lines
|
|
368
368
|
const indent = isLast ? " " : "│ ";
|
|
369
369
|
|
|
370
|
-
//
|
|
370
|
+
// dir: line (intent/objective)
|
|
371
371
|
if (r.intent) {
|
|
372
|
-
container.addChild(new Text(`${theme.fg("dim", indent + "├─
|
|
372
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "├─ dir:")} ${theme.fg("dim", truncateChars(r.intent, 50))}`, 0, 0));
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
//
|
|
375
|
+
// exe: line (last tool call)
|
|
376
376
|
const lastTool = getLastToolCall(r.messages);
|
|
377
377
|
if (lastTool) {
|
|
378
378
|
const exeStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
379
|
-
container.addChild(new Text(`${theme.fg("dim", indent + "├─
|
|
379
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "├─ exe:")} ${truncateChars(exeStr, 50)}`, 0, 0));
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
//
|
|
382
|
+
// log: line (last assistant text)
|
|
383
383
|
const lastText = getLastAssistantText(r.messages);
|
|
384
384
|
if (lastText) {
|
|
385
|
-
container.addChild(new Text(`${theme.fg("dim", indent + "└─
|
|
385
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("dim", truncateChars(lastText, 50))}`, 0, 0));
|
|
386
386
|
} else if (error && r.errorMessage) {
|
|
387
|
-
container.addChild(new Text(`${theme.fg("dim", indent + "└─
|
|
387
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("error", truncateChars(r.errorMessage, 50))}`, 0, 0));
|
|
388
388
|
} else {
|
|
389
|
-
container.addChild(new Text(`${theme.fg("dim", indent + "└─
|
|
389
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("dim", "[n/a]")}`, 0, 0));
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
// Add blank line separator between flows (with continuation pipe)
|