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/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 = 250;
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
- ...(smoothedTps > 0 ? { smoothedTps: Math.max(actual.smoothedTps ?? 0, smoothedTps) } : {}),
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
- if (flow.tools && flow.tools.length > 0) {
131
- args.push("--tools", flow.tools.join(","));
132
- } else if (flow.tools === undefined) {
133
- if (inheritedCliArgs.fallbackTools !== undefined) {
134
- args.push("--tools", inheritedCliArgs.fallbackTools);
135
- } else if (inheritedCliArgs.fallbackNoTools) {
136
- args.push("--no-tools");
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 = flow.tools?.join(", ") ?? "all";
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
- proc.kill("SIGTERM");
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) proc.kill("SIGKILL");
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
- clearSemanticCompletionTimer();
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 review flow after a successful code flow. */
71
+ /** Suggest audit flow after a successful build flow. */
72
72
  registerHook({
73
- name: "pi-agent-flow/code-to-review",
74
- trigger: { flowTypes: ["code"], onlyOnSuccess: true },
73
+ name: "pi-agent-flow/build-to-audit",
74
+ trigger: { flowTypes: ["build"], onlyOnSuccess: true },
75
75
  action: (ctx) => {
76
- const reviewWasRequested = ctx.params.some(
77
- (p) => p.type.toLowerCase() === "review",
76
+ const auditWasRequested = ctx.params.some(
77
+ (p) => p.type.toLowerCase() === "audit",
78
78
  );
79
- if (reviewWasRequested) return null;
79
+ if (auditWasRequested) return null;
80
80
 
81
81
  return {
82
82
  content:
83
- "Consider running a [review] flow to audit the changes for security, correctness, and code quality.",
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 code flow after a successful debug flow. */
89
+ /** Suggest build flow after a successful debug flow. */
90
90
  registerHook({
91
- name: "pi-agent-flow/debug-to-code",
91
+ name: "pi-agent-flow/debug-to-build",
92
92
  trigger: { flowTypes: ["debug"], onlyOnSuccess: true },
93
93
  action: (ctx) => {
94
- const codeWasRequested = ctx.params.some(
95
- (p) => p.type.toLowerCase() === "code",
94
+ const buildWasRequested = ctx.params.some(
95
+ (p) => p.type.toLowerCase() === "build",
96
96
  );
97
- if (codeWasRequested) return null;
97
+ if (buildWasRequested) return null;
98
98
 
99
99
  return {
100
100
  content:
101
- "The root cause has been identified. Consider running a [code] flow to implement the fix.",
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 explore, debug, code, architect, review, or brainstorm.",
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: "explore", "intent": "Find auth code" }, { type: "code", "intent": "Fix bug" }] }',
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 (explore, debug).",
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 (code, review).",
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 (brainstorm, architect).",
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
- // Inject available flows into the system prompt
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 (!canDelegate) return;
410
- if (discoveredFlows.length === 0) return;
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
- event.systemPrompt +
501
+ systemPrompt +
415
502
  `\n\n## Flows
416
503
 
417
504
  Before acting, reason about whether to dive into a flow:
418
505
 
419
- - [explore] — when you need to understand first. Find files, trace code paths, map architecture.
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
- - [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.
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": "explore", "intent": "..." }, { "type": "review", "intent": "..." }] }
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 [explore] — Map the full directory structure...
434
- • flow [review] — Audit security and quality...
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": "explore", "intent": "..." }, ...] }',
486
- "States: explore (tanken), debug (kensh\u014d), code (shokunin), architect (keikaku), review (kanshi), brainstorm (mushin).",
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
  }