pi-agent-flow 0.1.0-alpha.7 → 0.1.0-alpha.8
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 +83 -34
- package/agents/brainstorm.md +24 -0
- package/agents.ts +22 -0
- package/flow.ts +10 -21
- package/index.ts +5 -10
- package/package.json +1 -1
- package/render-utils.ts +2 -2
- package/render.ts +1 -9
- package/types.ts +3 -1
package/README.md
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><code>pi install /path/to/pi-agent-flow</code></p>
|
|
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
|
+
<p align="center">
|
|
4
|
+
<img src="https://github.com/user-attachments/assets/pi-agent-flow-demo.png" alt="Pi Agent Flow demo" width="80%" />
|
|
5
|
+
</p>
|
|
6
|
+
<br/>
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Delegates tasks to specialized flow states running as isolated pi processes. Each flow receives a snapshot of the current session context and returns a structured report.
|
|
8
|
+
---
|
|
6
9
|
|
|
7
|
-
##
|
|
10
|
+
## Quickstart
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- Parallel execution — batch independent flows into one call
|
|
11
|
-
- Structured reports — every flow returns [Summary], [Done], [Not Done], [Next Steps]
|
|
12
|
-
- Depth guards — configurable max delegation depth (default: 3)
|
|
13
|
-
- Cycle prevention — blocks recursive delegation chains
|
|
14
|
-
- Flow discovery — reads definitions from ~/.pi/agent/agents/ and .pi/agents/
|
|
15
|
-
- TUI rendering — rich collapsed/expanded display in interactive mode
|
|
12
|
+
### Installing Pi Agent Flow
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
Install via the Pi CLI with your local path:
|
|
18
15
|
|
|
19
|
-
```
|
|
16
|
+
```shell
|
|
17
|
+
# Install from a local path
|
|
20
18
|
pi install /path/to/pi-agent-flow
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
Or add to
|
|
21
|
+
Or add it to your Pi settings:
|
|
24
22
|
|
|
25
|
-
```
|
|
23
|
+
```shell
|
|
24
|
+
# ~/.pi/agent/settings.json
|
|
26
25
|
{
|
|
27
26
|
"packages": [
|
|
28
27
|
"./path/to/pi-agent-flow"
|
|
@@ -30,9 +29,36 @@ Or add to ~/.pi/agent/settings.json:
|
|
|
30
29
|
}
|
|
31
30
|
```
|
|
32
31
|
|
|
32
|
+
Then start Pi and delegate tasks using flow states.
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary>You can also clone this repository and use it directly.</summary>
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
git clone https://github.com/your-org/pi-agent-flow.git
|
|
39
|
+
cd pi-agent-flow
|
|
40
|
+
pi install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
</details>
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Isolated context** — flows always receive your current session snapshot (or start clean when configured)
|
|
50
|
+
- **Parallel execution** — batch independent flows into one call
|
|
51
|
+
- **Structured reports** — every flow returns `[Summary]`, `[Done]`, `[Not Done]`, `[Next Steps]`
|
|
52
|
+
- **Depth guards** — configurable max delegation depth (default: `3`)
|
|
53
|
+
- **Cycle prevention** — blocks recursive delegation chains
|
|
54
|
+
- **Flow discovery** — reads definitions from `~/.pi/agent/agents/` and `.pi/agents/`
|
|
55
|
+
- **TUI rendering** — rich collapsed/expanded display in interactive mode
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
33
59
|
## Flow Definitions
|
|
34
60
|
|
|
35
|
-
Create
|
|
61
|
+
Create `.md` files in `~/.pi/agent/agents/` or `.pi/agents/`:
|
|
36
62
|
|
|
37
63
|
```markdown
|
|
38
64
|
---
|
|
@@ -59,21 +85,32 @@ flow [explore] accomplished
|
|
|
59
85
|
- recommended follow-up
|
|
60
86
|
```
|
|
61
87
|
|
|
88
|
+
---
|
|
89
|
+
|
|
62
90
|
## Bundled Flows
|
|
63
91
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
| Flow | Purpose |
|
|
93
|
+
|------|---------|
|
|
94
|
+
| `[explore]` | Discover files, trace code paths, map architecture |
|
|
95
|
+
| `[debug]` | Investigate logs, errors, stack traces, root causes |
|
|
96
|
+
| `[code]` | Implement features, fix bugs, write tests |
|
|
97
|
+
| `[architect]` | Plan structure, break down requirements, design solutions |
|
|
98
|
+
| `[review]` | Audit security, quality, correctness |
|
|
99
|
+
| `[brainstorm]` | Generate ideas and explore possibilities with a clean slate |
|
|
100
|
+
|
|
101
|
+
> **Note:** Some flows — like `[brainstorm]` — start with a **clean slate** and do not inherit the current session context. They receive only the intent, making them ideal for unbiased, creative thinking.
|
|
102
|
+
|
|
103
|
+
---
|
|
69
104
|
|
|
70
105
|
## Usage
|
|
71
106
|
|
|
107
|
+
### Single flow
|
|
108
|
+
|
|
72
109
|
```json
|
|
73
110
|
{ "flow": [{ "type": "explore", "intent": "Find all authentication-related code" }] }
|
|
74
111
|
```
|
|
75
112
|
|
|
76
|
-
Batch multiple flows
|
|
113
|
+
### Batch multiple flows
|
|
77
114
|
|
|
78
115
|
```json
|
|
79
116
|
{
|
|
@@ -84,21 +121,33 @@ Batch multiple flows:
|
|
|
84
121
|
}
|
|
85
122
|
```
|
|
86
123
|
|
|
124
|
+
---
|
|
125
|
+
|
|
87
126
|
## Configuration
|
|
88
127
|
|
|
89
|
-
Flags (passed to parent pi process)
|
|
128
|
+
### Flags (passed to parent pi process)
|
|
90
129
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
130
|
+
| Flag | Description | Default |
|
|
131
|
+
|------|-------------|---------|
|
|
132
|
+
| `--flow-max-depth [n]` | Maximum delegation depth | `3` |
|
|
133
|
+
| `--flow-prevent-cycles` | Block cyclic delegation | `true` |
|
|
134
|
+
| `--no-flow-prevent-cycles` | Disable cycle prevention | — |
|
|
94
135
|
|
|
95
|
-
Environment variables (propagated to child processes)
|
|
136
|
+
### Environment variables (propagated to child processes)
|
|
137
|
+
|
|
138
|
+
| Variable | Description |
|
|
139
|
+
|----------|-------------|
|
|
140
|
+
| `PI_FLOW_DEPTH` | Current depth |
|
|
141
|
+
| `PI_FLOW_MAX_DEPTH` | Max allowed depth |
|
|
142
|
+
| `PI_FLOW_STACK` | JSON array of ancestor flow names |
|
|
143
|
+
| `PI_FLOW_PREVENT_CYCLES` | `"1"` or `"0"` |
|
|
144
|
+
|
|
145
|
+
---
|
|
96
146
|
|
|
97
|
-
|
|
98
|
-
- PI_FLOW_MAX_DEPTH — Max allowed depth
|
|
99
|
-
- PI_FLOW_STACK — JSON array of ancestor flow names
|
|
100
|
-
- PI_FLOW_PREVENT_CYCLES — "1" or "0"
|
|
147
|
+
## Docs
|
|
101
148
|
|
|
102
|
-
|
|
149
|
+
- [**Pi Documentation**](https://pi.dev)
|
|
150
|
+
- [**Contributing**](./docs/contributing.md) *(if available)*
|
|
151
|
+
- [**License**](./LICENSE)
|
|
103
152
|
|
|
104
|
-
MIT
|
|
153
|
+
This repository is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainstorm
|
|
3
|
+
description: Generate ideas, explore possibilities, and think creatively without context bias
|
|
4
|
+
tools: read, bash
|
|
5
|
+
inheritContext: false
|
|
6
|
+
maxDepth: 0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are the brainstorm flow — your mission is to generate ideas and think creatively. You start with a clean slate and only have the intent to guide you. Stay focused on your intent at all times.
|
|
10
|
+
|
|
11
|
+
When your mission is accomplished, end your response with:
|
|
12
|
+
|
|
13
|
+
flow [brainstorm] accomplished
|
|
14
|
+
|
|
15
|
+
[Summary] what was brainstormed
|
|
16
|
+
|
|
17
|
+
[Done]
|
|
18
|
+
- ideas generated
|
|
19
|
+
|
|
20
|
+
[Not Done]
|
|
21
|
+
- incomplete items and reasons
|
|
22
|
+
|
|
23
|
+
[Next Steps]
|
|
24
|
+
- recommended follow-up
|
package/agents.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface FlowConfig {
|
|
|
24
24
|
model?: string;
|
|
25
25
|
thinking?: string;
|
|
26
26
|
maxDepth?: number;
|
|
27
|
+
inheritContext?: boolean;
|
|
27
28
|
systemPrompt: string;
|
|
28
29
|
source: "user" | "project" | "bundled";
|
|
29
30
|
filePath: string;
|
|
@@ -140,6 +141,26 @@ function parseFlowFile(filePath: string, source: "user" | "project" | "bundled")
|
|
|
140
141
|
if (Number.isFinite(parsed) && parsed >= 0) maxDepth = parsed;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
let inheritContext: boolean | undefined;
|
|
145
|
+
if (typeof frontmatter.inheritContext === "boolean") {
|
|
146
|
+
inheritContext = frontmatter.inheritContext;
|
|
147
|
+
} else if (typeof frontmatter.inheritContext === "string") {
|
|
148
|
+
const normalized = frontmatter.inheritContext.trim().toLowerCase();
|
|
149
|
+
if (normalized === "true" || normalized === "yes" || normalized === "1") {
|
|
150
|
+
inheritContext = true;
|
|
151
|
+
} else if (normalized === "false" || normalized === "no" || normalized === "0") {
|
|
152
|
+
inheritContext = false;
|
|
153
|
+
} else {
|
|
154
|
+
console.warn(
|
|
155
|
+
`[pi-agent-flow] Ignoring invalid inheritContext value "${frontmatter.inheritContext}" in "${filePath}". Expected true/false.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
} else if (frontmatter.inheritContext !== undefined) {
|
|
159
|
+
console.warn(
|
|
160
|
+
`[pi-agent-flow] Ignoring invalid inheritContext field in "${filePath}". Expected boolean or string.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
return {
|
|
144
165
|
name,
|
|
145
166
|
description,
|
|
@@ -147,6 +168,7 @@ function parseFlowFile(filePath: string, source: "user" | "project" | "bundled")
|
|
|
147
168
|
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
148
169
|
thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
149
170
|
maxDepth,
|
|
171
|
+
inheritContext,
|
|
150
172
|
systemPrompt: body,
|
|
151
173
|
source,
|
|
152
174
|
filePath,
|
package/flow.ts
CHANGED
|
@@ -143,8 +143,8 @@ export interface RunFlowOptions {
|
|
|
143
143
|
intent: string;
|
|
144
144
|
/** Optional override working directory. */
|
|
145
145
|
taskCwd?: string;
|
|
146
|
-
/** Serialized parent session snapshot for fork mode. */
|
|
147
|
-
forkSessionSnapshotJsonl: string;
|
|
146
|
+
/** Serialized parent session snapshot for fork mode. Null when the flow starts with a clean slate. */
|
|
147
|
+
forkSessionSnapshotJsonl: string | null;
|
|
148
148
|
/** Current delegation depth of the caller process. */
|
|
149
149
|
parentDepth: number;
|
|
150
150
|
/** Delegation stack from the caller process (ancestor flow names). */
|
|
@@ -197,21 +197,6 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
if (!forkSessionSnapshotJsonl || !forkSessionSnapshotJsonl.trim()) {
|
|
201
|
-
return {
|
|
202
|
-
type: flowName,
|
|
203
|
-
agentSource: flow.source,
|
|
204
|
-
intent,
|
|
205
|
-
exitCode: 1,
|
|
206
|
-
messages: [],
|
|
207
|
-
stderr: "Cannot run in fork mode: missing parent session snapshot context.",
|
|
208
|
-
usage: emptyFlowUsage(),
|
|
209
|
-
model: flow.model,
|
|
210
|
-
stopReason: "error",
|
|
211
|
-
errorMessage: "Cannot run in fork mode: missing parent session snapshot context.",
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
200
|
const result: SingleResult = {
|
|
216
201
|
type: flowName,
|
|
217
202
|
agentSource: flow.source,
|
|
@@ -236,10 +221,14 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
236
221
|
});
|
|
237
222
|
};
|
|
238
223
|
|
|
239
|
-
// Write forked session snapshot to temp file
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
224
|
+
// Write forked session snapshot to temp file only when provided
|
|
225
|
+
let forkSessionTmpDir: string | null = null;
|
|
226
|
+
let forkSessionTmpPath: string | null = null;
|
|
227
|
+
if (forkSessionSnapshotJsonl) {
|
|
228
|
+
const forkTmp = writeFlowSessionToTempFile(flow.name, forkSessionSnapshotJsonl);
|
|
229
|
+
forkSessionTmpDir = forkTmp.dir;
|
|
230
|
+
forkSessionTmpPath = forkTmp.filePath;
|
|
231
|
+
}
|
|
243
232
|
|
|
244
233
|
try {
|
|
245
234
|
const piArgs = buildFlowArgs(
|
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 match an available flow name exactly: explore, debug, code, architect, review.",
|
|
39
|
+
description: "Flow type. Must match an available flow name exactly: explore, debug, code, architect, review, brainstorm.",
|
|
40
40
|
}),
|
|
41
41
|
intent: Type.String({
|
|
42
42
|
description: "Clear, specific mission for this flow.",
|
|
@@ -371,6 +371,7 @@ Before acting, reason about whether to dive into a flow:
|
|
|
371
371
|
- [code] — when you are ready to build. Implement features, fix bugs, write tests.
|
|
372
372
|
- [architect] — when you need a plan. Design structure, break down requirements before building.
|
|
373
373
|
- [review] — when you need to verify. Audit security, quality, correctness.
|
|
374
|
+
- [brainstorm] — when you need fresh ideas. Start from a clean slate with only the intent.
|
|
374
375
|
|
|
375
376
|
Multiple independent flows? Batch them into one call:
|
|
376
377
|
|
|
@@ -416,17 +417,10 @@ flow [type] accomplished
|
|
|
416
417
|
const { flows } = discovery;
|
|
417
418
|
const makeDetails = makeFlowDetailsFactory(discovery.projectFlowsDir);
|
|
418
419
|
|
|
419
|
-
// Build fork session snapshot (shared across all flows)
|
|
420
|
+
// Build fork session snapshot (shared across all flows that inherit context)
|
|
420
421
|
const forkSessionSnapshotJsonl = buildForkSessionSnapshotJsonl(
|
|
421
422
|
ctx.sessionManager,
|
|
422
423
|
);
|
|
423
|
-
if (!forkSessionSnapshotJsonl) {
|
|
424
|
-
return {
|
|
425
|
-
content: [{ type: "text", text: "Cannot use fork mode: failed to snapshot current session context." }],
|
|
426
|
-
details: makeDetails([]),
|
|
427
|
-
isError: true,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
424
|
|
|
431
425
|
// Collect all requested flow names
|
|
432
426
|
const requested = new Set(params.flow.map((f) => f.type));
|
|
@@ -513,13 +507,14 @@ flow [type] accomplished
|
|
|
513
507
|
const effectiveMaxDepth =
|
|
514
508
|
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
515
509
|
|
|
510
|
+
const shouldInheritContext = targetFlow?.inheritContext !== false;
|
|
516
511
|
const result = await runFlow({
|
|
517
512
|
cwd: ctx.cwd,
|
|
518
513
|
flows,
|
|
519
514
|
flowName: item.type,
|
|
520
515
|
intent: item.intent,
|
|
521
516
|
taskCwd: item.cwd,
|
|
522
|
-
forkSessionSnapshotJsonl,
|
|
517
|
+
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
523
518
|
parentDepth: currentDepth,
|
|
524
519
|
parentFlowStack: ancestorFlowStack,
|
|
525
520
|
maxDepth: effectiveMaxDepth,
|
package/package.json
CHANGED
package/render-utils.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function formatFlowUsage(usage: Partial<UsageStats>, model?: string): str
|
|
|
21
21
|
if (usage.cacheWrite) parts.push(`CW:${formatTokens(usage.cacheWrite)}`);
|
|
22
22
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
23
23
|
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
24
|
-
if (model) parts.push(model);
|
|
24
|
+
if (model) parts.push(`model:${model}`);
|
|
25
25
|
return parts.join(" ");
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ export function formatCompactStats(usage: Partial<UsageStats>, model?: string):
|
|
|
31
31
|
parts.push(`↓${formatTokens(usage.output || 0)}`);
|
|
32
32
|
if (usage.cacheRead) parts.push(`cr:${formatTokens(usage.cacheRead)}`);
|
|
33
33
|
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
34
|
-
if (model) parts.push(model);
|
|
34
|
+
if (model) parts.push(`model:${model}`);
|
|
35
35
|
return parts.join(" ");
|
|
36
36
|
}
|
|
37
37
|
|
package/render.ts
CHANGED
|
@@ -109,19 +109,11 @@ function flowStatusIcon(r: SingleResult, theme: { fg: ThemeFg }): string {
|
|
|
109
109
|
// renderFlowCall — shown while the flow is being invoked
|
|
110
110
|
// ---------------------------------------------------------------------------
|
|
111
111
|
|
|
112
|
-
function truncateText(text: string): string {
|
|
113
|
-
const words = text.split(/\s+/);
|
|
114
|
-
if (words.length <= 12) return text;
|
|
115
|
-
return `${words.slice(0, 3).join(" ")} ... ${words.slice(-8).join(" ")}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
112
|
export function renderFlowCall(args: Record<string, any>, theme: FlowTheme): Text {
|
|
119
113
|
const flows = args.flow as Array<{ type: string; intent: string }> | undefined;
|
|
120
114
|
|
|
121
115
|
// Minimal — renderFlowResult owns the full display
|
|
122
116
|
return new Text("", 0, 0);
|
|
123
|
-
|
|
124
|
-
return new Text(theme.fg("muted", "(empty flow call)"), 0, 0);
|
|
125
117
|
}
|
|
126
118
|
|
|
127
119
|
// ---------------------------------------------------------------------------
|
|
@@ -352,7 +344,7 @@ function renderMultiFlowCollapsed(
|
|
|
352
344
|
|
|
353
345
|
// Intent line
|
|
354
346
|
if (r.intent) {
|
|
355
|
-
const intentPrefix =
|
|
347
|
+
const intentPrefix = "├─";
|
|
356
348
|
text += `\n${theme.fg("dim", indent + intentPrefix + " int:")} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
|
|
357
349
|
}
|
|
358
350
|
|
package/types.ts
CHANGED
|
@@ -140,7 +140,9 @@ export function getFlowDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
140
140
|
if (part.type === "text") {
|
|
141
141
|
items.push({ type: "text", text: part.text });
|
|
142
142
|
} else if (part.type === "toolCall") {
|
|
143
|
-
|
|
143
|
+
const name = part.name ?? part.toolName ?? "unknown";
|
|
144
|
+
const args = (part.arguments ?? part.input ?? {}) as Record<string, unknown>;
|
|
145
|
+
items.push({ type: "toolCall", name, args });
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
}
|