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/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
- }