pi-agent-flow 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +104 -0
- package/agents/architect.md +35 -0
- package/agents/code.md +36 -0
- package/agents/debug.md +34 -0
- package/agents/explore.md +29 -0
- package/agents/review.md +35 -0
- package/agents.ts +214 -0
- package/flow.ts +415 -0
- package/index.ts +575 -0
- package/package.json +68 -0
- package/render.ts +376 -0
- package/runner-cli.js +225 -0
- package/runner-events.js +156 -0
- package/types.ts +149 -0
package/index.ts
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Flow Extension (fork-only)
|
|
3
|
+
*
|
|
4
|
+
* Delegates tasks to specialized flow states running as isolated pi processes.
|
|
5
|
+
* Each flow receives a forked snapshot of the current session context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import { type FlowConfig, discoverFlows } from "./agents.js";
|
|
11
|
+
import { renderFlowCall, renderFlowResult } from "./render.js";
|
|
12
|
+
import { getFlowSummaryText } from "./runner-events.js";
|
|
13
|
+
import { mapFlowConcurrent, runFlow } from "./flow.js";
|
|
14
|
+
import {
|
|
15
|
+
type SingleResult,
|
|
16
|
+
type FlowDetails,
|
|
17
|
+
emptyFlowUsage,
|
|
18
|
+
isFlowError,
|
|
19
|
+
isFlowSuccess,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Limits
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MAX_DELEGATION_DEPTH = 3;
|
|
27
|
+
const DEFAULT_PREVENT_CYCLE_DELEGATION = true;
|
|
28
|
+
const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
29
|
+
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
30
|
+
const FLOW_STACK_ENV = "PI_FLOW_STACK";
|
|
31
|
+
const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Tool parameter schema
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const FlowItem = Type.Object({
|
|
38
|
+
type: Type.String({
|
|
39
|
+
description: "Flow type. Must match an available flow name exactly: explore, debug, code, architect, review.",
|
|
40
|
+
}),
|
|
41
|
+
intent: Type.String({
|
|
42
|
+
description: "Clear, specific mission for this flow.",
|
|
43
|
+
}),
|
|
44
|
+
cwd: Type.Optional(
|
|
45
|
+
Type.String({ description: "Working directory override for this flow." }),
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const FlowParams = Type.Object({
|
|
50
|
+
flow: Type.Array(FlowItem, {
|
|
51
|
+
description:
|
|
52
|
+
"Array of flow tasks to execute. Each runs in its own forked process. " +
|
|
53
|
+
'Example: { flow: [{ type: "explore", "intent": "Find auth code" }, { type: "code", "intent": "Fix bug" }] }',
|
|
54
|
+
minItems: 1,
|
|
55
|
+
}),
|
|
56
|
+
confirmProjectFlows: Type.Optional(
|
|
57
|
+
Type.Boolean({
|
|
58
|
+
description: "Whether to prompt the user before running project-local flows. Default: true.",
|
|
59
|
+
default: true,
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Helpers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
interface FlowDepthConfig {
|
|
69
|
+
currentDepth: number;
|
|
70
|
+
maxDepth: number;
|
|
71
|
+
canDelegate: boolean;
|
|
72
|
+
ancestorFlowStack: string[];
|
|
73
|
+
preventCycles: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface SessionSnapshotSource {
|
|
77
|
+
getHeader: () => unknown;
|
|
78
|
+
getBranch: () => unknown[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildForkSessionSnapshotJsonl(
|
|
82
|
+
sessionManager: SessionSnapshotSource,
|
|
83
|
+
): string | null {
|
|
84
|
+
const header = sessionManager.getHeader();
|
|
85
|
+
if (!header || typeof header !== "object") return null;
|
|
86
|
+
|
|
87
|
+
const branchEntries = sessionManager.getBranch();
|
|
88
|
+
|
|
89
|
+
// Trim the current conversation turn from the fork.
|
|
90
|
+
let trimFrom = branchEntries.length;
|
|
91
|
+
for (let i = branchEntries.length - 1; i >= 0; i--) {
|
|
92
|
+
const entry = branchEntries[i] as Record<string, unknown>;
|
|
93
|
+
if (entry?.type === "message") {
|
|
94
|
+
const msg = entry.message as Record<string, unknown> | undefined;
|
|
95
|
+
if (msg?.role === "user") {
|
|
96
|
+
trimFrom = i;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const trimmedEntries = branchEntries.slice(0, trimFrom);
|
|
103
|
+
const lines = [JSON.stringify(header)];
|
|
104
|
+
for (const entry of trimmedEntries) lines.push(JSON.stringify(entry));
|
|
105
|
+
return `${lines.join("\n")}\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseNonNegativeInt(raw: unknown): number | null {
|
|
109
|
+
if (typeof raw !== "string") return null;
|
|
110
|
+
const trimmed = raw.trim();
|
|
111
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
112
|
+
const parsed = Number(trimmed);
|
|
113
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseBoolean(raw: unknown): boolean | null {
|
|
117
|
+
if (typeof raw === "boolean") return raw;
|
|
118
|
+
if (typeof raw !== "string") return null;
|
|
119
|
+
const normalized = raw.trim().toLowerCase();
|
|
120
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
121
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseFlowStack(raw: unknown): string[] | null {
|
|
126
|
+
if (raw === undefined) return [];
|
|
127
|
+
if (typeof raw !== "string") return null;
|
|
128
|
+
if (!raw.trim()) return [];
|
|
129
|
+
|
|
130
|
+
let parsed: unknown;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(raw);
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(parsed)) return null;
|
|
138
|
+
if (!parsed.every((value) => typeof value === "string")) return null;
|
|
139
|
+
return parsed
|
|
140
|
+
.map((value) => value.trim())
|
|
141
|
+
.filter((value) => value.length > 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getMaxDepthFlagFromArgv(argv: string[]): string | null {
|
|
145
|
+
for (let i = 2; i < argv.length; i++) {
|
|
146
|
+
const arg = argv[i];
|
|
147
|
+
if (arg === "--flow-max-depth") {
|
|
148
|
+
return argv[i + 1] ?? "";
|
|
149
|
+
}
|
|
150
|
+
if (arg.startsWith("--flow-max-depth=")) {
|
|
151
|
+
return arg.slice("--flow-max-depth=".length);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getPreventCyclesFlagFromArgv(
|
|
158
|
+
argv: string[],
|
|
159
|
+
): string | boolean | null {
|
|
160
|
+
for (let i = 2; i < argv.length; i++) {
|
|
161
|
+
const arg = argv[i];
|
|
162
|
+
if (arg === "--flow-prevent-cycles") {
|
|
163
|
+
const maybeValue = argv[i + 1];
|
|
164
|
+
if (maybeValue !== undefined && !maybeValue.startsWith("--")) {
|
|
165
|
+
return maybeValue;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (arg === "--no-flow-prevent-cycles") return false;
|
|
170
|
+
if (arg.startsWith("--flow-prevent-cycles=")) {
|
|
171
|
+
return arg.slice("--flow-prevent-cycles=".length);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveFlowDepthConfig(pi: ExtensionAPI): FlowDepthConfig {
|
|
178
|
+
const depthRaw = process.env[FLOW_DEPTH_ENV];
|
|
179
|
+
const parsedDepth = parseNonNegativeInt(depthRaw);
|
|
180
|
+
if (depthRaw !== undefined && parsedDepth === null) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`[pi-agent-flow] Ignoring invalid ${FLOW_DEPTH_ENV}="${depthRaw}". Expected a non-negative integer.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const currentDepth = parsedDepth ?? 0;
|
|
186
|
+
|
|
187
|
+
const stackRaw = process.env[FLOW_STACK_ENV];
|
|
188
|
+
const ancestorFlowStack = parseFlowStack(stackRaw);
|
|
189
|
+
if (stackRaw !== undefined && ancestorFlowStack === null) {
|
|
190
|
+
console.warn(
|
|
191
|
+
`[pi-agent-flow] Ignoring invalid ${FLOW_STACK_ENV} value. Expected a JSON array of flow names.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const envMaxDepthRaw = process.env[FLOW_MAX_DEPTH_ENV];
|
|
196
|
+
const envMaxDepth = parseNonNegativeInt(envMaxDepthRaw);
|
|
197
|
+
if (envMaxDepthRaw !== undefined && envMaxDepth === null) {
|
|
198
|
+
console.warn(
|
|
199
|
+
`[pi-agent-flow] Ignoring invalid ${FLOW_MAX_DEPTH_ENV}="${envMaxDepthRaw}". Expected a non-negative integer.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const argvFlagRaw = getMaxDepthFlagFromArgv(process.argv);
|
|
204
|
+
const argvFlagMaxDepth =
|
|
205
|
+
argvFlagRaw !== null ? parseNonNegativeInt(argvFlagRaw) : null;
|
|
206
|
+
if (argvFlagRaw !== null && argvFlagMaxDepth === null) {
|
|
207
|
+
console.warn(
|
|
208
|
+
`[pi-agent-flow] Ignoring invalid --flow-max-depth value "${argvFlagRaw}". Expected a non-negative integer.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const runtimeFlagValue = pi.getFlag("flow-max-depth");
|
|
213
|
+
const runtimeFlagMaxDepth =
|
|
214
|
+
typeof runtimeFlagValue === "string"
|
|
215
|
+
? parseNonNegativeInt(runtimeFlagValue)
|
|
216
|
+
: null;
|
|
217
|
+
if (
|
|
218
|
+
argvFlagRaw === null &&
|
|
219
|
+
typeof runtimeFlagValue === "string" &&
|
|
220
|
+
runtimeFlagMaxDepth === null
|
|
221
|
+
) {
|
|
222
|
+
console.warn(
|
|
223
|
+
`[pi-agent-flow] Ignoring invalid --flow-max-depth value "${runtimeFlagValue}". Expected a non-negative integer.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const envPreventCyclesRaw = process.env[FLOW_PREVENT_CYCLES_ENV];
|
|
228
|
+
const envPreventCycles = parseBoolean(envPreventCyclesRaw);
|
|
229
|
+
if (envPreventCyclesRaw !== undefined && envPreventCycles === null) {
|
|
230
|
+
console.warn(
|
|
231
|
+
`[pi-agent-flow] Ignoring invalid ${FLOW_PREVENT_CYCLES_ENV}="${envPreventCyclesRaw}". Expected true/false.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const argvPreventCyclesRaw = getPreventCyclesFlagFromArgv(process.argv);
|
|
236
|
+
const argvPreventCycles =
|
|
237
|
+
typeof argvPreventCyclesRaw === "boolean"
|
|
238
|
+
? argvPreventCyclesRaw
|
|
239
|
+
: parseBoolean(argvPreventCyclesRaw);
|
|
240
|
+
if (
|
|
241
|
+
typeof argvPreventCyclesRaw === "string" &&
|
|
242
|
+
argvPreventCycles === null
|
|
243
|
+
) {
|
|
244
|
+
console.warn(
|
|
245
|
+
`[pi-agent-flow] Ignoring invalid --flow-prevent-cycles value "${argvPreventCyclesRaw}". Expected true/false.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const runtimePreventCyclesRaw = pi.getFlag("flow-prevent-cycles");
|
|
250
|
+
const runtimePreventCycles = parseBoolean(runtimePreventCyclesRaw);
|
|
251
|
+
if (
|
|
252
|
+
argvPreventCyclesRaw === null &&
|
|
253
|
+
runtimePreventCyclesRaw !== undefined &&
|
|
254
|
+
runtimePreventCycles === null
|
|
255
|
+
) {
|
|
256
|
+
console.warn(
|
|
257
|
+
`[pi-agent-flow] Ignoring invalid --flow-prevent-cycles value "${String(runtimePreventCyclesRaw)}". Expected true/false.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const flagMaxDepth = argvFlagMaxDepth ?? runtimeFlagMaxDepth;
|
|
262
|
+
const maxDepth = flagMaxDepth ?? envMaxDepth ?? DEFAULT_MAX_DELEGATION_DEPTH;
|
|
263
|
+
const preventCycles =
|
|
264
|
+
argvPreventCycles ??
|
|
265
|
+
runtimePreventCycles ??
|
|
266
|
+
envPreventCycles ??
|
|
267
|
+
DEFAULT_PREVENT_CYCLE_DELEGATION;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
currentDepth,
|
|
271
|
+
maxDepth,
|
|
272
|
+
canDelegate: currentDepth < maxDepth,
|
|
273
|
+
ancestorFlowStack: ancestorFlowStack ?? [],
|
|
274
|
+
preventCycles,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function makeFlowDetailsFactory(projectFlowsDir: string | null) {
|
|
279
|
+
return (results: SingleResult[]): FlowDetails => ({
|
|
280
|
+
mode: "flow",
|
|
281
|
+
delegationMode: "fork",
|
|
282
|
+
projectAgentsDir: projectFlowsDir,
|
|
283
|
+
results,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function formatFlowNames(flows: FlowConfig[]): string {
|
|
288
|
+
return flows.map((f) => `${f.name} (${f.source})`).join(", ") || "none";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getFlowCycleViolations(
|
|
292
|
+
requestedNames: Set<string>,
|
|
293
|
+
ancestorFlowStack: string[],
|
|
294
|
+
): string[] {
|
|
295
|
+
if (requestedNames.size === 0 || ancestorFlowStack.length === 0) return [];
|
|
296
|
+
const stackSet = new Set(ancestorFlowStack);
|
|
297
|
+
return Array.from(requestedNames).filter((name) => stackSet.has(name));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Get project-local flows referenced by the current request. */
|
|
301
|
+
function getRequestedProjectFlows(
|
|
302
|
+
flows: FlowConfig[],
|
|
303
|
+
requestedNames: Set<string>,
|
|
304
|
+
): FlowConfig[] {
|
|
305
|
+
return Array.from(requestedNames)
|
|
306
|
+
.map((name) => flows.find((f) => f.name === name))
|
|
307
|
+
.filter((f): f is FlowConfig => f?.source === "project");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Prompt the user to confirm project-local flows if needed.
|
|
312
|
+
* Returns false if the user declines.
|
|
313
|
+
*/
|
|
314
|
+
async function confirmProjectFlowsIfNeeded(
|
|
315
|
+
projectFlows: FlowConfig[],
|
|
316
|
+
projectFlowsDir: string | null,
|
|
317
|
+
ctx: { ui: { confirm: (title: string, body: string) => Promise<boolean> } },
|
|
318
|
+
): Promise<boolean> {
|
|
319
|
+
if (projectFlows.length === 0) return true;
|
|
320
|
+
|
|
321
|
+
const names = projectFlows.map((f) => f.name).join(", ");
|
|
322
|
+
const dir = projectFlowsDir ?? "(unknown)";
|
|
323
|
+
return ctx.ui.confirm(
|
|
324
|
+
"Run project-local flows?",
|
|
325
|
+
`Flows: ${names}\nSource: ${dir}\n\nProject flows are repo-controlled. Only continue for trusted repositories.`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Extension entry point
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
export default function (pi: ExtensionAPI) {
|
|
334
|
+
pi.registerFlag("flow-max-depth", {
|
|
335
|
+
description: "Maximum allowed flow delegation depth (default: 3).",
|
|
336
|
+
type: "string",
|
|
337
|
+
});
|
|
338
|
+
pi.registerFlag("flow-prevent-cycles", {
|
|
339
|
+
description: "Block delegating to flows already in the current delegation stack (default: true).",
|
|
340
|
+
type: "boolean",
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const depthConfig = resolveFlowDepthConfig(pi);
|
|
344
|
+
const { currentDepth, maxDepth, canDelegate, ancestorFlowStack, preventCycles } =
|
|
345
|
+
depthConfig;
|
|
346
|
+
|
|
347
|
+
let discoveredFlows: FlowConfig[] = [];
|
|
348
|
+
|
|
349
|
+
// Auto-discover flows on session start
|
|
350
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
351
|
+
if (!canDelegate) return;
|
|
352
|
+
|
|
353
|
+
const discovery = discoverFlows(ctx.cwd, "all");
|
|
354
|
+
discoveredFlows = discovery.flows;
|
|
355
|
+
|
|
356
|
+
if (discoveredFlows.length > 0 && ctx.hasUI) {
|
|
357
|
+
const list = discoveredFlows
|
|
358
|
+
.map((f) => ` - ${f.name} (${f.source})`)
|
|
359
|
+
.join("\n");
|
|
360
|
+
ctx.ui.notify(
|
|
361
|
+
`Found ${discoveredFlows.length} flow(s):\n${list}`,
|
|
362
|
+
"info",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Inject available flows into the system prompt
|
|
368
|
+
pi.on("before_agent_start", async (event) => {
|
|
369
|
+
if (!canDelegate) return;
|
|
370
|
+
if (discoveredFlows.length === 0) return;
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
systemPrompt:
|
|
374
|
+
event.systemPrompt +
|
|
375
|
+
`\n\n## Flows
|
|
376
|
+
|
|
377
|
+
Before acting, reason about whether to dive into a flow:
|
|
378
|
+
|
|
379
|
+
- [explore] — when you need to understand first. Find files, trace code paths, map architecture.
|
|
380
|
+
- [debug] — when something is broken. Investigate logs, errors, stack traces to find root cause.
|
|
381
|
+
- [code] — when you are ready to build. Implement features, fix bugs, write tests.
|
|
382
|
+
- [architect] — when you need a plan. Design structure, break down requirements before building.
|
|
383
|
+
- [review] — when you need to verify. Audit security, quality, correctness.
|
|
384
|
+
|
|
385
|
+
Multiple independent flows? Batch them into one call:
|
|
386
|
+
|
|
387
|
+
✅ { "flow": [{ "type": "explore", "intent": "..." }, { "type": "review", "intent": "..." }] }
|
|
388
|
+
❌ Two separate calls — wastes time
|
|
389
|
+
|
|
390
|
+
Each call renders as:
|
|
391
|
+
|
|
392
|
+
routing to:
|
|
393
|
+
• flow [explore] — Map the full directory structure...
|
|
394
|
+
• flow [review] — Audit security and quality...
|
|
395
|
+
|
|
396
|
+
Each flow returns:
|
|
397
|
+
|
|
398
|
+
flow [type] accomplished
|
|
399
|
+
|
|
400
|
+
[Summary] — what happened
|
|
401
|
+
[Done] — completed with file:line references
|
|
402
|
+
[Not Done] — incomplete items and reasons
|
|
403
|
+
[Next Steps] — recommended follow-up
|
|
404
|
+
|
|
405
|
+
### Guards
|
|
406
|
+
- Depth: ${currentDepth}/${maxDepth} | Cycles: ${preventCycles ? "blocked" : "off"} | Stack: ${ancestorFlowStack.length > 0 ? ancestorFlowStack.join(" -> ") : "(root)"}
|
|
407
|
+
`,
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Register the flow tool
|
|
412
|
+
if (canDelegate) {
|
|
413
|
+
pi.registerTool({
|
|
414
|
+
name: "flow",
|
|
415
|
+
label: "Flow",
|
|
416
|
+
description: [
|
|
417
|
+
"Delegate work to flow states running in isolated pi processes.",
|
|
418
|
+
"Each flow receives a snapshot of your current session context.",
|
|
419
|
+
"All flows run in parallel — batch independent tasks into one call.",
|
|
420
|
+
"",
|
|
421
|
+
'Usage: { "flow": [{ "type": "explore", "intent": "..." }, ...] }',
|
|
422
|
+
].join("\n"),
|
|
423
|
+
parameters: FlowParams,
|
|
424
|
+
|
|
425
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
426
|
+
const discovery = discoverFlows(ctx.cwd, "all");
|
|
427
|
+
const { flows } = discovery;
|
|
428
|
+
const makeDetails = makeFlowDetailsFactory(discovery.projectFlowsDir);
|
|
429
|
+
|
|
430
|
+
// Build fork session snapshot (shared across all flows)
|
|
431
|
+
const forkSessionSnapshotJsonl = buildForkSessionSnapshotJsonl(
|
|
432
|
+
ctx.sessionManager,
|
|
433
|
+
);
|
|
434
|
+
if (!forkSessionSnapshotJsonl) {
|
|
435
|
+
return {
|
|
436
|
+
content: [{ type: "text", text: "Cannot use fork mode: failed to snapshot current session context." }],
|
|
437
|
+
details: makeDetails([]),
|
|
438
|
+
isError: true,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Collect all requested flow names
|
|
443
|
+
const requested = new Set(params.flow.map((f) => f.type));
|
|
444
|
+
|
|
445
|
+
// Cycle check
|
|
446
|
+
if (preventCycles) {
|
|
447
|
+
const violations = getFlowCycleViolations(requested, ancestorFlowStack);
|
|
448
|
+
if (violations.length > 0) {
|
|
449
|
+
const stack = ancestorFlowStack.join(" -> ") || "(root)";
|
|
450
|
+
return {
|
|
451
|
+
content: [{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: `Blocked: cycle detected. Flow(s) in stack: ${violations.join(", ")}\nStack: ${stack}`,
|
|
454
|
+
}],
|
|
455
|
+
details: makeDetails([]),
|
|
456
|
+
isError: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Project flow confirmation
|
|
462
|
+
const shouldConfirm = params.confirmProjectFlows ?? true;
|
|
463
|
+
if (shouldConfirm) {
|
|
464
|
+
const projectFlows = getRequestedProjectFlows(flows, requested);
|
|
465
|
+
if (projectFlows.length > 0) {
|
|
466
|
+
if (ctx.hasUI) {
|
|
467
|
+
const ok = await confirmProjectFlowsIfNeeded(projectFlows, discovery.projectFlowsDir, ctx);
|
|
468
|
+
if (!ok) {
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text: "Canceled: project-local flows not approved." }],
|
|
471
|
+
details: makeDetails([]),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
const names = projectFlows.map((f) => f.name).join(", ");
|
|
476
|
+
return {
|
|
477
|
+
content: [{
|
|
478
|
+
type: "text",
|
|
479
|
+
text: `Blocked: project-local flow confirmation required in non-UI mode.\nFlows: ${names}\nRe-run with confirmProjectFlows: false if trusted.`,
|
|
480
|
+
}],
|
|
481
|
+
details: makeDetails([]),
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Run all flows in parallel
|
|
489
|
+
const allResults: SingleResult[] = new Array(params.flow.length);
|
|
490
|
+
for (let i = 0; i < params.flow.length; i++) {
|
|
491
|
+
allResults[i] = {
|
|
492
|
+
type: params.flow[i].type,
|
|
493
|
+
agentSource: "unknown",
|
|
494
|
+
intent: params.flow[i].intent,
|
|
495
|
+
exitCode: -1,
|
|
496
|
+
messages: [],
|
|
497
|
+
stderr: "",
|
|
498
|
+
usage: emptyFlowUsage(),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const emitProgress = () => {
|
|
503
|
+
if (!onUpdate) return;
|
|
504
|
+
onUpdate({
|
|
505
|
+
content: [{ type: "text", text: `Flow: ${allResults.filter((r) => r.exitCode !== -1).length}/${allResults.length} done` }],
|
|
506
|
+
details: makeDetails([...allResults]),
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
let heartbeat: NodeJS.Timeout | undefined;
|
|
511
|
+
if (onUpdate) {
|
|
512
|
+
emitProgress();
|
|
513
|
+
heartbeat = setInterval(() => {
|
|
514
|
+
if (allResults.some((r) => r.exitCode === -1)) emitProgress();
|
|
515
|
+
}, 1000);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let results: SingleResult[];
|
|
519
|
+
try {
|
|
520
|
+
results = await mapFlowConcurrent(params.flow, 4, async (item, index) => {
|
|
521
|
+
const targetFlow = flows.find((f) => f.name === item.type);
|
|
522
|
+
const effectiveMaxDepth =
|
|
523
|
+
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
524
|
+
|
|
525
|
+
const result = await runFlow({
|
|
526
|
+
cwd: ctx.cwd,
|
|
527
|
+
flows,
|
|
528
|
+
flowName: item.type,
|
|
529
|
+
intent: item.intent,
|
|
530
|
+
taskCwd: item.cwd,
|
|
531
|
+
forkSessionSnapshotJsonl,
|
|
532
|
+
parentDepth: currentDepth,
|
|
533
|
+
parentFlowStack: ancestorFlowStack,
|
|
534
|
+
maxDepth: effectiveMaxDepth,
|
|
535
|
+
preventCycles,
|
|
536
|
+
signal,
|
|
537
|
+
onUpdate: (partial) => {
|
|
538
|
+
if (partial.details?.results[0]) {
|
|
539
|
+
allResults[index] = partial.details.results[0];
|
|
540
|
+
emitProgress();
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
makeDetails,
|
|
544
|
+
});
|
|
545
|
+
allResults[index] = result;
|
|
546
|
+
emitProgress();
|
|
547
|
+
return result;
|
|
548
|
+
});
|
|
549
|
+
} finally {
|
|
550
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Build tool result with FULL flow output — no truncation
|
|
554
|
+
const successCount = results.filter((r) => isFlowSuccess(r)).length;
|
|
555
|
+
const flowReports = results.map((r) => {
|
|
556
|
+
const output = getFlowSummaryText(r);
|
|
557
|
+
const status = isFlowError(r) ? "failed" : "accomplished";
|
|
558
|
+
return `flow [${r.type}] ${status}\n\n${output}`;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
content: [{
|
|
563
|
+
type: "text" as const,
|
|
564
|
+
text: `Flow: ${successCount}/${results.length} completed\n\n${flowReports.join("\n\n---\n\n")}`,
|
|
565
|
+
}],
|
|
566
|
+
details: makeDetails(results),
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
renderCall: (args, theme) => renderFlowCall(args, theme),
|
|
571
|
+
renderResult: (result, { expanded }, theme) =>
|
|
572
|
+
renderFlowResult(result, expanded, theme),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-agent-flow",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Flow-state delegation extension for Pi coding agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"agents.ts",
|
|
10
|
+
"flow.ts",
|
|
11
|
+
"runner-cli.js",
|
|
12
|
+
"runner-events.js",
|
|
13
|
+
"render.ts",
|
|
14
|
+
"types.ts",
|
|
15
|
+
"agents/",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"pi": {
|
|
19
|
+
"extensions": [
|
|
20
|
+
"./index.ts"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"pi",
|
|
25
|
+
"flow",
|
|
26
|
+
"fork",
|
|
27
|
+
"pi-package"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/tuanhung303/pi-agent-flow"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"lint": "tsc --noEmit",
|
|
39
|
+
"test": "echo \"no tests yet\" && exit 0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@mariozechner/pi-agent-core": ">=0.37.0",
|
|
46
|
+
"@mariozechner/pi-ai": ">=0.37.0",
|
|
47
|
+
"@mariozechner/pi-coding-agent": ">=0.37.0",
|
|
48
|
+
"@mariozechner/pi-tui": ">=0.37.0",
|
|
49
|
+
"@sinclair/typebox": ">=0.34.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@mariozechner/pi-agent-core": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@mariozechner/pi-coding-agent": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@mariozechner/pi-tui": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"@mariozechner/pi-ai": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"@sinclair/typebox": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|