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