pi-agent-flow 1.0.8 → 1.1.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 +2 -2
- package/agents/audit.md +36 -0
- package/agents/{code.md → build.md} +5 -4
- package/agents/craft.md +37 -0
- package/agents/debug.md +2 -1
- package/agents/{brainstorm.md → ideas.md} +4 -4
- package/agents/scout.md +31 -0
- package/agents.ts +5 -8
- package/batch.ts +1083 -0
- package/config.ts +35 -0
- package/flow.ts +84 -18
- package/hooks.ts +31 -13
- package/index.ts +126 -28
- package/package.json +11 -3
- package/render-utils.ts +3 -3
- package/render.ts +55 -24
- package/runner-events.js +280 -126
- package/types.ts +3 -2
- package/web-tool.ts +663 -0
- package/agents/architect.md +0 -35
- package/agents/explore.md +0 -29
- package/agents/review.md +0 -35
package/config.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface FlowModelConfig {
|
|
|
15
15
|
full?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface FlowSettings {
|
|
19
|
+
toolOptimize?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
function readSettingsJson(filePath: string): Record<string, unknown> | null {
|
|
19
23
|
try {
|
|
20
24
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -40,6 +44,20 @@ function extractFlowModels(settings: Record<string, unknown> | null): FlowModelC
|
|
|
40
44
|
return result;
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
function extractFlowSettings(settings: Record<string, unknown> | null): FlowSettings {
|
|
48
|
+
if (!settings) return {};
|
|
49
|
+
const flowSettings = settings.flowSettings;
|
|
50
|
+
if (!flowSettings || typeof flowSettings !== "object" || Array.isArray(flowSettings)) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
const obj = flowSettings as Record<string, unknown>;
|
|
54
|
+
const result: FlowSettings = {};
|
|
55
|
+
if (typeof obj.toolOptimize === "boolean") {
|
|
56
|
+
result.toolOptimize = obj.toolOptimize;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
function getGlobalSettingsPath(): string {
|
|
44
62
|
const agentDir = process.env["PI_CODING_AGENT_DIR"]?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
45
63
|
return path.join(agentDir, "settings.json");
|
|
@@ -65,3 +83,20 @@ export function loadFlowModels(cwd: string): FlowModelConfig {
|
|
|
65
83
|
...projectModels,
|
|
66
84
|
};
|
|
67
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load flowSettings from global and project settings.json.
|
|
89
|
+
* Project overrides global (shallow merge per key).
|
|
90
|
+
*/
|
|
91
|
+
export function loadFlowSettings(cwd: string): FlowSettings {
|
|
92
|
+
const globalSettings = readSettingsJson(getGlobalSettingsPath());
|
|
93
|
+
const globalFlowSettings = extractFlowSettings(globalSettings);
|
|
94
|
+
|
|
95
|
+
const projectSettings = readSettingsJson(getProjectSettingsPath(cwd));
|
|
96
|
+
const projectFlowSettings = extractFlowSettings(projectSettings);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...globalFlowSettings,
|
|
100
|
+
...projectFlowSettings,
|
|
101
|
+
};
|
|
102
|
+
}
|
package/flow.ts
CHANGED
|
@@ -23,11 +23,14 @@ import {
|
|
|
23
23
|
|
|
24
24
|
const isWindows = process.platform === "win32";
|
|
25
25
|
const SIGKILL_TIMEOUT_MS = 5000;
|
|
26
|
-
const AGENT_END_GRACE_MS =
|
|
26
|
+
const AGENT_END_GRACE_MS = 2000;
|
|
27
|
+
const DEFAULT_FLOW_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
27
28
|
const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
28
29
|
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
29
30
|
const FLOW_STACK_ENV = "PI_FLOW_STACK";
|
|
30
31
|
const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
32
|
+
const FLOW_TIMEOUT_ENV = "PI_FLOW_TIMEOUT_MS";
|
|
33
|
+
export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
|
|
31
34
|
const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
32
35
|
|
|
33
36
|
type FlowUpdateCallback = (partial: AgentToolResult<FlowDetails>) => void;
|
|
@@ -49,7 +52,8 @@ function mergeStreamingUsage(
|
|
|
49
52
|
...actual,
|
|
50
53
|
...(estimatedOutputTokens > 0 ? { output: Math.max(actual.output, estimatedOutputTokens) } : {}),
|
|
51
54
|
...(ctxEstimate > 0 ? { contextTokens: Math.max(actual.contextTokens, ctxEstimate) } : {}),
|
|
52
|
-
|
|
55
|
+
// Preserve the peak smoothedTPS seen during the stream rather than the live EMA value.
|
|
56
|
+
...(smoothedTps > 0 ? { smoothedTps: Math.max(actual.smoothedTps || 0, smoothedTps) } : {}),
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
|
|
@@ -99,6 +103,27 @@ function cleanupFlowTempDir(dir: string | null): void {
|
|
|
99
103
|
|
|
100
104
|
const inheritedCliArgs = parseFlowCliArgs(process.argv);
|
|
101
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Transform a flow's tool list when toolOptimize is enabled.
|
|
108
|
+
* Replaces separate read/write/edit tools with the unified batch tool.
|
|
109
|
+
*/
|
|
110
|
+
export function getOptimizedTools(
|
|
111
|
+
flowTools: string[] | undefined,
|
|
112
|
+
toolOptimize: boolean,
|
|
113
|
+
): string[] | undefined {
|
|
114
|
+
if (!toolOptimize || !flowTools) return flowTools;
|
|
115
|
+
const hasLegacyTools = flowTools.some(
|
|
116
|
+
(t) => t === "read" || t === "write" || t === "edit",
|
|
117
|
+
);
|
|
118
|
+
if (!hasLegacyTools) return flowTools;
|
|
119
|
+
const filtered = flowTools.filter(
|
|
120
|
+
(t) => t !== "read" && t !== "write" && t !== "edit" && t !== "batch",
|
|
121
|
+
);
|
|
122
|
+
return filtered.includes("batch")
|
|
123
|
+
? filtered
|
|
124
|
+
: [...filtered, "batch"];
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
function buildFlowArgs(
|
|
103
128
|
flow: FlowConfig,
|
|
104
129
|
intent: string,
|
|
@@ -106,6 +131,7 @@ function buildFlowArgs(
|
|
|
106
131
|
tieredModels?: { lite?: string; flash?: string; full?: string },
|
|
107
132
|
parentDepth: number = 0,
|
|
108
133
|
maxDepth: number = 0,
|
|
134
|
+
toolOptimize: boolean = false,
|
|
109
135
|
): string[] {
|
|
110
136
|
const args: string[] = [
|
|
111
137
|
"--mode",
|
|
@@ -127,15 +153,22 @@ function buildFlowArgs(
|
|
|
127
153
|
const thinking = flow.thinking ?? inheritedCliArgs.fallbackThinking;
|
|
128
154
|
if (thinking) args.push("--thinking", thinking);
|
|
129
155
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
// Child flows get their configured tools from flow.tools, optimized by
|
|
157
|
+
// getOptimizedTools, with web explicitly filtered out.
|
|
158
|
+
// When flow.tools is undefined and toolOptimize=true, default to batch+bash+web
|
|
159
|
+
// (flow is unnecessary since the child is already inside a flow).
|
|
160
|
+
// When toolOptimize=false, include batch and web alongside legacy tools.
|
|
161
|
+
const defaultTools = toolOptimize
|
|
162
|
+
? ["batch", "bash", "web"]
|
|
163
|
+
: ["read", "write", "edit", "batch", "bash", "flow", "web"];
|
|
164
|
+
const optimizedTools = getOptimizedTools(flow.tools, toolOptimize) ?? defaultTools;
|
|
165
|
+
let harnessTools = optimizedTools.filter((t) => t !== "web");
|
|
166
|
+
// If the flow explicitly listed only "web" (or nothing after filtering),
|
|
167
|
+
// fall back to defaultTools so the child isn't orphaned with zero tools.
|
|
168
|
+
if (harnessTools.length === 0) {
|
|
169
|
+
harnessTools = defaultTools.filter((t) => t !== "web");
|
|
138
170
|
}
|
|
171
|
+
args.push("--tools", harnessTools.join(","));
|
|
139
172
|
|
|
140
173
|
// No --append-system-prompt: child inherits parent's system prompt for cache hits.
|
|
141
174
|
// Flow instructions go in the intent message instead.
|
|
@@ -143,7 +176,7 @@ function buildFlowArgs(
|
|
|
143
176
|
const currentDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
144
177
|
const effectiveMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
145
178
|
const canDelegate = currentDepth < effectiveMaxDepth;
|
|
146
|
-
const availableTools =
|
|
179
|
+
const availableTools = harnessTools.join(", ");
|
|
147
180
|
|
|
148
181
|
// Phase 1: Context seal — sharp boundary declaring history sealed
|
|
149
182
|
const contextSeal =
|
|
@@ -196,6 +229,8 @@ export interface RunFlowOptions {
|
|
|
196
229
|
flowName: string;
|
|
197
230
|
/** Intent description. */
|
|
198
231
|
intent: string;
|
|
232
|
+
/** Short headline for display. */
|
|
233
|
+
aim: string;
|
|
199
234
|
/** Optional override working directory. */
|
|
200
235
|
taskCwd?: string;
|
|
201
236
|
/** Serialized parent session snapshot for fork mode. Null when the flow starts with a clean slate. */
|
|
@@ -208,6 +243,8 @@ export interface RunFlowOptions {
|
|
|
208
243
|
maxDepth: number;
|
|
209
244
|
/** Whether cycle prevention should be enforced in child processes. */
|
|
210
245
|
preventCycles: boolean;
|
|
246
|
+
/** Whether to transform tool lists to use batch. */
|
|
247
|
+
toolOptimize?: boolean;
|
|
211
248
|
/** Tiered model overrides (lite/flash/full). */
|
|
212
249
|
tieredModels?: { lite?: string; flash?: string; full?: string };
|
|
213
250
|
/** Abort signal for cancellation. */
|
|
@@ -216,6 +253,8 @@ export interface RunFlowOptions {
|
|
|
216
253
|
onUpdate?: FlowUpdateCallback;
|
|
217
254
|
/** Factory to wrap results into FlowDetails. */
|
|
218
255
|
makeDetails: (results: SingleResult[]) => FlowDetails;
|
|
256
|
+
/** Max execution time in ms before child is terminated. Default: 10 minutes. */
|
|
257
|
+
timeoutMs?: number;
|
|
219
258
|
}
|
|
220
259
|
|
|
221
260
|
/**
|
|
@@ -229,12 +268,14 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
229
268
|
flows,
|
|
230
269
|
flowName,
|
|
231
270
|
intent,
|
|
271
|
+
aim,
|
|
232
272
|
taskCwd,
|
|
233
273
|
forkSessionSnapshotJsonl,
|
|
234
274
|
parentDepth,
|
|
235
275
|
parentFlowStack,
|
|
236
276
|
maxDepth,
|
|
237
277
|
preventCycles,
|
|
278
|
+
toolOptimize = false,
|
|
238
279
|
signal,
|
|
239
280
|
onUpdate,
|
|
240
281
|
makeDetails,
|
|
@@ -248,6 +289,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
248
289
|
type: normalizedFlowName,
|
|
249
290
|
agentSource: "unknown",
|
|
250
291
|
intent,
|
|
292
|
+
aim,
|
|
251
293
|
exitCode: 1,
|
|
252
294
|
messages: [],
|
|
253
295
|
stderr: `Unknown flow: "${flowName}". Available flows: ${available}.`,
|
|
@@ -260,6 +302,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
260
302
|
type: normalizedFlowName,
|
|
261
303
|
agentSource: flow.source,
|
|
262
304
|
intent,
|
|
305
|
+
aim,
|
|
263
306
|
exitCode: -1,
|
|
264
307
|
messages: [],
|
|
265
308
|
stderr: "",
|
|
@@ -302,9 +345,18 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
302
345
|
opts.tieredModels,
|
|
303
346
|
parentDepth,
|
|
304
347
|
maxDepth,
|
|
348
|
+
toolOptimize,
|
|
305
349
|
);
|
|
306
350
|
let wasAborted = false;
|
|
307
351
|
|
|
352
|
+
// Resolve timeout: explicit option > env var > default (10 min)
|
|
353
|
+
const envTimeoutRaw = process.env[FLOW_TIMEOUT_ENV];
|
|
354
|
+
const envTimeout = envTimeoutRaw !== undefined ? (() => {
|
|
355
|
+
const n = Number(envTimeoutRaw);
|
|
356
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
357
|
+
})() : null;
|
|
358
|
+
const effectiveTimeout = opts.timeoutMs ?? envTimeout ?? DEFAULT_FLOW_TIMEOUT_MS;
|
|
359
|
+
|
|
308
360
|
const exitCode = await new Promise<number>((resolve) => {
|
|
309
361
|
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
310
362
|
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
@@ -314,12 +366,15 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
314
366
|
cwd: taskCwd ?? cwd,
|
|
315
367
|
shell: false,
|
|
316
368
|
stdio: ["pipe", "pipe", "pipe"],
|
|
369
|
+
// Process group on Unix so we can kill all descendants on timeout/abort.
|
|
370
|
+
detached: !isWindows,
|
|
317
371
|
env: {
|
|
318
372
|
...process.env,
|
|
319
373
|
[FLOW_DEPTH_ENV]: String(nextDepth),
|
|
320
374
|
[FLOW_MAX_DEPTH_ENV]: String(propagatedMaxDepth),
|
|
321
375
|
[FLOW_STACK_ENV]: JSON.stringify(propagatedStack),
|
|
322
376
|
[FLOW_PREVENT_CYCLES_ENV]: preventCycles ? "1" : "0",
|
|
377
|
+
[FLOW_TOOL_OPTIMIZE_ENV]: toolOptimize ? "1" : "0",
|
|
323
378
|
[PI_OFFLINE_ENV]: "1",
|
|
324
379
|
},
|
|
325
380
|
});
|
|
@@ -353,9 +408,12 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
353
408
|
return;
|
|
354
409
|
}
|
|
355
410
|
|
|
356
|
-
|
|
411
|
+
// Kill the entire process group (negative PID).
|
|
412
|
+
if (proc.pid === undefined) { proc.kill("SIGTERM"); } else { try { process.kill(-proc.pid, "SIGTERM"); } catch { proc.kill("SIGTERM"); } }
|
|
357
413
|
const sigkillTimer = setTimeout(() => {
|
|
358
|
-
if (!didClose)
|
|
414
|
+
if (!didClose) {
|
|
415
|
+
if (proc.pid === undefined) { proc.kill("SIGKILL"); } else { try { process.kill(-proc.pid, "SIGKILL"); } catch { proc.kill("SIGKILL"); } }
|
|
416
|
+
}
|
|
359
417
|
}, SIGKILL_TIMEOUT_MS);
|
|
360
418
|
sigkillTimer.unref();
|
|
361
419
|
};
|
|
@@ -381,19 +439,17 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
381
439
|
}
|
|
382
440
|
};
|
|
383
441
|
|
|
442
|
+
let semanticCompletionTimerArmed = false;
|
|
384
443
|
const maybeFinishFromAgentEnd = () => {
|
|
385
|
-
if (!result.sawAgentEnd || didClose || settled) return;
|
|
386
|
-
|
|
444
|
+
if (!result.sawAgentEnd || didClose || settled || semanticCompletionTimerArmed) return;
|
|
445
|
+
semanticCompletionTimerArmed = true;
|
|
387
446
|
semanticCompletionTimer = setTimeout(() => {
|
|
388
447
|
if (didClose || settled || !result.sawAgentEnd) return;
|
|
389
448
|
if (buffer.trim()) {
|
|
390
449
|
flushBufferedLines(buffer);
|
|
391
450
|
buffer = "";
|
|
392
451
|
}
|
|
393
|
-
proc.stdout.removeListener("data", onStdoutData);
|
|
394
|
-
proc.stderr.removeListener("data", onStderrData);
|
|
395
452
|
finish(0);
|
|
396
|
-
terminateChild();
|
|
397
453
|
}, AGENT_END_GRACE_MS);
|
|
398
454
|
semanticCompletionTimer.unref();
|
|
399
455
|
};
|
|
@@ -433,6 +489,16 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
433
489
|
if (signal.aborted) abortHandler();
|
|
434
490
|
else signal.addEventListener("abort", abortHandler, { once: true });
|
|
435
491
|
}
|
|
492
|
+
|
|
493
|
+
// Execution timeout — kill child if it runs too long
|
|
494
|
+
if (effectiveTimeout > 0) {
|
|
495
|
+
const timeoutTimer = setTimeout(() => {
|
|
496
|
+
if (didClose || settled) return;
|
|
497
|
+
result.stderr += `\nFlow timed out after ${Math.round(effectiveTimeout / 1000)}s.`;
|
|
498
|
+
terminateChild();
|
|
499
|
+
}, effectiveTimeout);
|
|
500
|
+
timeoutTimer.unref();
|
|
501
|
+
}
|
|
436
502
|
});
|
|
437
503
|
|
|
438
504
|
result.exitCode = exitCode;
|
package/hooks.ts
CHANGED
|
@@ -68,38 +68,56 @@ export function clearHooks(): void {
|
|
|
68
68
|
// Built-in hooks
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
70
70
|
|
|
71
|
-
/** Suggest
|
|
71
|
+
/** Suggest audit flow after a successful build flow. */
|
|
72
72
|
registerHook({
|
|
73
|
-
name: "pi-agent-flow/
|
|
74
|
-
trigger: { flowTypes: ["
|
|
73
|
+
name: "pi-agent-flow/build-to-audit",
|
|
74
|
+
trigger: { flowTypes: ["build"], onlyOnSuccess: true },
|
|
75
75
|
action: (ctx) => {
|
|
76
|
-
const
|
|
77
|
-
(p) => p.type.toLowerCase() === "
|
|
76
|
+
const auditWasRequested = ctx.params.some(
|
|
77
|
+
(p) => p.type.toLowerCase() === "audit",
|
|
78
78
|
);
|
|
79
|
-
if (
|
|
79
|
+
if (auditWasRequested) return null;
|
|
80
80
|
|
|
81
81
|
return {
|
|
82
82
|
content:
|
|
83
|
-
"Consider running a [
|
|
83
|
+
"Consider running a [audit] flow to audit the changes for security, correctness, and code quality.",
|
|
84
84
|
priority: 10,
|
|
85
85
|
};
|
|
86
86
|
},
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
/** Suggest
|
|
89
|
+
/** Suggest build flow after a successful debug flow. */
|
|
90
90
|
registerHook({
|
|
91
|
-
name: "pi-agent-flow/debug-to-
|
|
91
|
+
name: "pi-agent-flow/debug-to-build",
|
|
92
92
|
trigger: { flowTypes: ["debug"], onlyOnSuccess: true },
|
|
93
93
|
action: (ctx) => {
|
|
94
|
-
const
|
|
95
|
-
(p) => p.type.toLowerCase() === "
|
|
94
|
+
const buildWasRequested = ctx.params.some(
|
|
95
|
+
(p) => p.type.toLowerCase() === "build",
|
|
96
96
|
);
|
|
97
|
-
if (
|
|
97
|
+
if (buildWasRequested) return null;
|
|
98
98
|
|
|
99
99
|
return {
|
|
100
100
|
content:
|
|
101
|
-
"The root cause has been identified. Consider running a [
|
|
101
|
+
"The root cause has been identified. Consider running a [build] flow to implement the fix.",
|
|
102
102
|
priority: 10,
|
|
103
103
|
};
|
|
104
104
|
},
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
/** Suggest explore flow after a successful audit flow. */
|
|
108
|
+
registerHook({
|
|
109
|
+
name: "pi-agent-flow/audit-to-explore",
|
|
110
|
+
trigger: { flowTypes: ["audit"], onlyOnSuccess: true },
|
|
111
|
+
action: (ctx) => {
|
|
112
|
+
const exploreWasRequested = ctx.params.some(
|
|
113
|
+
(p) => p.type.toLowerCase() === "explore",
|
|
114
|
+
);
|
|
115
|
+
if (exploreWasRequested) return null;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content:
|
|
119
|
+
"Audit complete. Consider running an [explore] flow to review the audit findings.",
|
|
120
|
+
priority: 15,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
package/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
10
|
import { type FlowConfig, discoverFlows } from "./agents.js";
|
|
11
|
-
import { loadFlowModels, type FlowModelConfig } from "./config.js";
|
|
11
|
+
import { loadFlowModels, loadFlowSettings, type FlowModelConfig } from "./config.js";
|
|
12
12
|
import { renderFlowCall, renderFlowResult } from "./render.js";
|
|
13
13
|
import { getFlowSummaryText } from "./runner-events.js";
|
|
14
14
|
import { runHooks } from "./hooks.js";
|
|
@@ -20,6 +20,12 @@ import {
|
|
|
20
20
|
isFlowError,
|
|
21
21
|
isFlowSuccess,
|
|
22
22
|
} from "./types.js";
|
|
23
|
+
import { createBatchTool } from "./batch.js";
|
|
24
|
+
import {
|
|
25
|
+
createWebTool,
|
|
26
|
+
looksLikeUrlPrompt,
|
|
27
|
+
looksLikeWebSearchPrompt,
|
|
28
|
+
} from "./web-tool.js";
|
|
23
29
|
|
|
24
30
|
// ---------------------------------------------------------------------------
|
|
25
31
|
// Limits
|
|
@@ -31,6 +37,7 @@ const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
|
31
37
|
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
32
38
|
const FLOW_STACK_ENV = "PI_FLOW_STACK";
|
|
33
39
|
const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
40
|
+
export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
|
|
34
41
|
|
|
35
42
|
// ---------------------------------------------------------------------------
|
|
36
43
|
// Tool parameter schema
|
|
@@ -38,11 +45,14 @@ const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
|
38
45
|
|
|
39
46
|
const FlowItem = Type.Object({
|
|
40
47
|
type: Type.String({
|
|
41
|
-
description: "Flow type. Matching is case-insensitive. Must correspond to an available flow name such as
|
|
48
|
+
description: "Flow type. Matching is case-insensitive. Must correspond to an available flow name such as scout, debug, build, craft, audit, or ideas.",
|
|
42
49
|
}),
|
|
43
50
|
intent: Type.String({
|
|
44
51
|
description: "Clear, specific mission for this flow.",
|
|
45
52
|
}),
|
|
53
|
+
aim: Type.String({
|
|
54
|
+
description: "Extreme short intent — one sentence, 5-7 words, headline-style summary of what this flow does.",
|
|
55
|
+
}),
|
|
46
56
|
cwd: Type.Optional(
|
|
47
57
|
Type.String({ description: "Working directory override for this flow." }),
|
|
48
58
|
),
|
|
@@ -52,7 +62,7 @@ const FlowParams = Type.Object({
|
|
|
52
62
|
flow: Type.Array(FlowItem, {
|
|
53
63
|
description:
|
|
54
64
|
"Array of flow tasks to execute. Each runs in its own forked process. " +
|
|
55
|
-
'Example: { flow: [{ type: "
|
|
65
|
+
'Example: { flow: [{ type: "scout", "intent": "Find all authentication-related code and trace JWT validation", "aim": "Find auth code and trace JWT" }, { type: "build", "intent": "Fix the bug in user registration", "aim": "Fix registration bug" }] }',
|
|
56
66
|
minItems: 1,
|
|
57
67
|
}),
|
|
58
68
|
confirmProjectFlows: Type.Optional(
|
|
@@ -362,6 +372,12 @@ function appendReminder(
|
|
|
362
372
|
return copy;
|
|
363
373
|
}
|
|
364
374
|
|
|
375
|
+
function computeActiveTools(optimize: boolean): string[] {
|
|
376
|
+
return optimize
|
|
377
|
+
? ["batch", "bash", "flow", "web"]
|
|
378
|
+
: ["read", "write", "edit", "batch", "bash", "flow", "web"];
|
|
379
|
+
}
|
|
380
|
+
|
|
365
381
|
// ---------------------------------------------------------------------------
|
|
366
382
|
// Extension entry point
|
|
367
383
|
// ---------------------------------------------------------------------------
|
|
@@ -376,62 +392,133 @@ export default function (pi: ExtensionAPI) {
|
|
|
376
392
|
type: "boolean",
|
|
377
393
|
});
|
|
378
394
|
pi.registerFlag("flow-lite-model", {
|
|
379
|
-
description: "Model for lite-tier flows (
|
|
395
|
+
description: "Model for lite-tier flows (scout, debug).",
|
|
380
396
|
type: "string",
|
|
381
397
|
});
|
|
382
398
|
pi.registerFlag("flow-flash-model", {
|
|
383
|
-
description: "Model for flash-tier flows (
|
|
399
|
+
description: "Model for flash-tier flows (build, audit).",
|
|
384
400
|
type: "string",
|
|
385
401
|
});
|
|
386
402
|
pi.registerFlag("flow-full-model", {
|
|
387
|
-
description: "Model for full-tier flows (
|
|
403
|
+
description: "Model for full-tier flows (ideas, craft).",
|
|
388
404
|
type: "string",
|
|
389
405
|
});
|
|
406
|
+
pi.registerFlag("tool-optimize", {
|
|
407
|
+
description: "Use the unified batch tool instead of separate read/write/edit tools (default: true).",
|
|
408
|
+
type: "boolean",
|
|
409
|
+
});
|
|
390
410
|
|
|
391
411
|
const depthConfig = resolveFlowDepthConfig(pi);
|
|
392
412
|
const { currentDepth, maxDepth, canDelegate, ancestorFlowStack, preventCycles } =
|
|
393
413
|
depthConfig;
|
|
394
414
|
|
|
415
|
+
// toolOptimize: CLI flag > env var > settings.json > default (true)
|
|
416
|
+
let toolOptimize = true;
|
|
417
|
+
const envToolOptimize = process.env[FLOW_TOOL_OPTIMIZE_ENV];
|
|
418
|
+
if (envToolOptimize !== undefined) {
|
|
419
|
+
const parsed = parseBoolean(envToolOptimize);
|
|
420
|
+
if (parsed !== null) toolOptimize = parsed;
|
|
421
|
+
}
|
|
422
|
+
|
|
395
423
|
let discoveredFlows: FlowConfig[] = [];
|
|
396
424
|
let flowModelConfig: FlowModelConfig = {};
|
|
397
425
|
|
|
398
426
|
// Auto-discover flows on session start
|
|
399
427
|
pi.on("session_start", async (_event, ctx) => {
|
|
400
|
-
if (!canDelegate) return;
|
|
401
|
-
|
|
402
428
|
const discovery = discoverFlows(ctx.cwd, "all");
|
|
403
429
|
discoveredFlows = discovery.flows;
|
|
404
430
|
flowModelConfig = loadFlowModels(ctx.cwd);
|
|
431
|
+
|
|
432
|
+
// Resolve toolOptimize: CLI flag > env var > settings.json > default
|
|
433
|
+
const cliFlag = pi.getFlag("tool-optimize");
|
|
434
|
+
if (typeof cliFlag === "boolean") {
|
|
435
|
+
toolOptimize = cliFlag;
|
|
436
|
+
} else if (typeof cliFlag === "string") {
|
|
437
|
+
const parsed = parseBoolean(cliFlag);
|
|
438
|
+
if (parsed !== null) toolOptimize = parsed;
|
|
439
|
+
} else {
|
|
440
|
+
const flowSettings = loadFlowSettings(ctx.cwd);
|
|
441
|
+
if (typeof flowSettings.toolOptimize === "boolean") {
|
|
442
|
+
toolOptimize = flowSettings.toolOptimize;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Only restrict tools for the main orchestrator (depth 0).
|
|
447
|
+
// Child flows (depth > 0) receive their tools via --tools CLI arg;
|
|
448
|
+
// overriding them here would strip bash/batch from children.
|
|
449
|
+
if (currentDepth === 0) {
|
|
450
|
+
pi.setActiveTools(computeActiveTools(toolOptimize));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Register batch so it is available for both main agent and child flows.
|
|
454
|
+
if (toolOptimize) {
|
|
455
|
+
pi.registerTool(createBatchTool());
|
|
456
|
+
}
|
|
405
457
|
});
|
|
406
458
|
|
|
407
|
-
//
|
|
459
|
+
// Re-apply active tools every turn to survive registry refreshes.
|
|
460
|
+
// Skip for child flows — they get tools from --tools CLI arg.
|
|
461
|
+
pi.on("turn_start", () => {
|
|
462
|
+
if (currentDepth > 0) return;
|
|
463
|
+
pi.setActiveTools(computeActiveTools(toolOptimize));
|
|
464
|
+
});
|
|
465
|
+
// Inject available flows into the system prompt.
|
|
466
|
+
// Skip entirely for child flows (depth > 0) — they get their instructions
|
|
467
|
+
// from the 4-part prompt structure in buildFlowArgs and have no web tool.
|
|
408
468
|
pi.on("before_agent_start", async (event) => {
|
|
409
|
-
if (
|
|
410
|
-
|
|
469
|
+
if (currentDepth > 0) return undefined;
|
|
470
|
+
|
|
471
|
+
const prompt = event.prompt;
|
|
472
|
+
const hasUrl = looksLikeUrlPrompt(prompt);
|
|
473
|
+
const likelyNeedsWeb = looksLikeWebSearchPrompt(prompt);
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
const webInstructions: string[] = [];
|
|
477
|
+
if (hasUrl) {
|
|
478
|
+
webInstructions.push(
|
|
479
|
+
"The prompt includes a URL. Use web tool with op: { o: 'fetch', u: '<url>' } before answering about that page.",
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
if (likelyNeedsWeb) {
|
|
483
|
+
webInstructions.push(
|
|
484
|
+
"The prompt likely needs external or current info. Prefer web tool with op: [{ o: 'search', q: '<query>' }] over memory.",
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let systemPrompt = event.systemPrompt;
|
|
489
|
+
if (webInstructions.length > 0) {
|
|
490
|
+
systemPrompt +=
|
|
491
|
+
"\n\n## pi-web steering\n" +
|
|
492
|
+
webInstructions.map((line) => `- ${line}`).join("\n");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!canDelegate || discoveredFlows.length === 0) {
|
|
496
|
+
return webInstructions.length > 0 ? { systemPrompt } : undefined;
|
|
497
|
+
}
|
|
411
498
|
|
|
412
499
|
return {
|
|
413
500
|
systemPrompt:
|
|
414
|
-
|
|
501
|
+
systemPrompt +
|
|
415
502
|
`\n\n## Flows
|
|
416
503
|
|
|
417
504
|
Before acting, reason about whether to dive into a flow:
|
|
418
505
|
|
|
419
|
-
- [
|
|
506
|
+
- [scout] — when you need to understand first. Find files, trace code paths, map architecture.
|
|
420
507
|
- [debug] — when something is broken. Investigate logs, errors, stack traces to find root cause.
|
|
421
|
-
- [
|
|
422
|
-
- [
|
|
423
|
-
- [
|
|
424
|
-
- [
|
|
508
|
+
- [build] — when you are ready to build. Implement features, fix bugs, write tests.
|
|
509
|
+
- [craft] — when you need a plan. Design structure, break down requirements before building.
|
|
510
|
+
- [audit] — when you need to verify. Audit security, quality, correctness.
|
|
511
|
+
- [ideas] — when you need fresh ideas. Start from a clean slate with only the intent.
|
|
425
512
|
|
|
426
513
|
Multiple independent flows? Batch them into one call:
|
|
427
514
|
|
|
428
|
-
✅ { "flow": [{ "type": "
|
|
515
|
+
✅ { "flow": [{ "type": "scout", "intent": "..." }, { "type": "audit", "intent": "..." }] }
|
|
429
516
|
❌ Two separate calls — wastes time
|
|
430
517
|
|
|
431
518
|
Each call renders as:
|
|
432
519
|
|
|
433
|
-
• flow [
|
|
434
|
-
• flow [
|
|
520
|
+
• flow [scout] — Map the full directory structure...
|
|
521
|
+
• flow [audit] — Audit security and quality...
|
|
435
522
|
|
|
436
523
|
Each flow returns:
|
|
437
524
|
|
|
@@ -448,18 +535,22 @@ flow [type] accomplished
|
|
|
448
535
|
};
|
|
449
536
|
});
|
|
450
537
|
|
|
451
|
-
// Sliding reminder: strip from all earlier user messages, append to latest
|
|
538
|
+
// Sliding reminder: strip from all earlier user messages, append to latest.
|
|
539
|
+
// Skip for child flows — they have explicit <mission> instructions and
|
|
540
|
+
// injecting the reminder is noise.
|
|
452
541
|
pi.on("context", async (event) => {
|
|
542
|
+
if (currentDepth > 0) return undefined;
|
|
543
|
+
|
|
453
544
|
const messages = event.messages;
|
|
454
545
|
const userIndices = messages
|
|
455
|
-
.map((m, i) => (m.role === "user" ? i : -1))
|
|
456
|
-
.filter((i) => i !== -1);
|
|
546
|
+
.map((m: any, i: number) => (m.role === "user" ? i : -1))
|
|
547
|
+
.filter((i: number) => i !== -1);
|
|
457
548
|
|
|
458
549
|
if (userIndices.length === 0) return undefined;
|
|
459
550
|
|
|
460
551
|
const lastUserIndex = userIndices[userIndices.length - 1];
|
|
461
552
|
|
|
462
|
-
const modified = messages.map((msg, idx) => {
|
|
553
|
+
const modified = messages.map((msg: any, idx: number) => {
|
|
463
554
|
if (msg.role !== "user") return msg;
|
|
464
555
|
|
|
465
556
|
const content = stripReminder(msg.content);
|
|
@@ -472,6 +563,9 @@ flow [type] accomplished
|
|
|
472
563
|
return { messages: modified };
|
|
473
564
|
});
|
|
474
565
|
|
|
566
|
+
// Register the web tool
|
|
567
|
+
pi.registerTool(createWebTool());
|
|
568
|
+
|
|
475
569
|
// Register the flow tool
|
|
476
570
|
if (canDelegate) {
|
|
477
571
|
pi.registerTool({
|
|
@@ -482,8 +576,8 @@ flow [type] accomplished
|
|
|
482
576
|
"You MUST enter to the following flow states, with tool call method.",
|
|
483
577
|
"",
|
|
484
578
|
"Flow states are isolated \u03c0 processes with forked session snapshots. They run in parallel.",
|
|
485
|
-
'Invoke: { "flow": [{ "type": "
|
|
486
|
-
"States:
|
|
579
|
+
'Invoke: { "flow": [{ "type": "scout", "intent": "..." }, ...] }',
|
|
580
|
+
"States: scout (tanken), debug (kensh\u014d), build (shokunin), craft (keikaku), audit (kanshi), ideas (mushin).",
|
|
487
581
|
"Custom states configs in (create if not exists): .md files in .pi/agents/ or ~/.pi/agent/agents/.",
|
|
488
582
|
].join("\n"),
|
|
489
583
|
parameters: FlowParams,
|
|
@@ -506,7 +600,7 @@ flow [type] accomplished
|
|
|
506
600
|
);
|
|
507
601
|
|
|
508
602
|
// Collect all requested flow names
|
|
509
|
-
const requested = new Set(params.flow.map((f) => f.type.toLowerCase()));
|
|
603
|
+
const requested = new Set<string>(params.flow.map((f: any) => f.type.toLowerCase()));
|
|
510
604
|
|
|
511
605
|
// Cycle check
|
|
512
606
|
if (preventCycles) {
|
|
@@ -558,6 +652,7 @@ flow [type] accomplished
|
|
|
558
652
|
type: params.flow[i].type,
|
|
559
653
|
agentSource: "unknown",
|
|
560
654
|
intent: params.flow[i].intent,
|
|
655
|
+
aim: params.flow[i].aim,
|
|
561
656
|
exitCode: -1,
|
|
562
657
|
messages: [],
|
|
563
658
|
stderr: "",
|
|
@@ -581,7 +676,7 @@ flow [type] accomplished
|
|
|
581
676
|
|
|
582
677
|
if (onUpdate) emitProgress();
|
|
583
678
|
|
|
584
|
-
const results = await mapFlowConcurrent(params.flow, 4, async (item, index) => {
|
|
679
|
+
const results = await mapFlowConcurrent(params.flow, 4, async (item: any, index: number) => {
|
|
585
680
|
const normalizedType = item.type.toLowerCase();
|
|
586
681
|
const targetFlow = flows.find((f) => f.name === normalizedType);
|
|
587
682
|
const effectiveMaxDepth =
|
|
@@ -593,12 +688,14 @@ flow [type] accomplished
|
|
|
593
688
|
flows,
|
|
594
689
|
flowName: normalizedType,
|
|
595
690
|
intent: item.intent,
|
|
691
|
+
aim: item.aim,
|
|
596
692
|
taskCwd: item.cwd,
|
|
597
693
|
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
598
694
|
parentDepth: currentDepth,
|
|
599
695
|
parentFlowStack: ancestorFlowStack,
|
|
600
696
|
maxDepth: effectiveMaxDepth,
|
|
601
697
|
preventCycles,
|
|
698
|
+
toolOptimize,
|
|
602
699
|
tieredModels,
|
|
603
700
|
signal,
|
|
604
701
|
onUpdate: (partial) => {
|
|
@@ -642,4 +739,5 @@ flow [type] accomplished
|
|
|
642
739
|
renderFlowResult(result, expanded, theme, args),
|
|
643
740
|
});
|
|
644
741
|
}
|
|
742
|
+
|
|
645
743
|
}
|