pi-agent-flow 1.2.3 → 1.2.5

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/src/index.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  * Each flow receives a forked snapshot of the current session context.
6
6
  */
7
7
 
8
+ import { randomUUID } from "node:crypto";
9
+ import * as os from "node:os";
8
10
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
11
  import { Type } from "@sinclair/typebox";
10
12
  import { type FlowConfig, discoverFlows, getFlowTier } from "./agents.js";
@@ -15,17 +17,20 @@ import {
15
17
  selectFlowModelStrategy,
16
18
  type LoadedFlowModelConfigs,
17
19
  } from "./config.js";
18
- import { parseFlowCliArgs } from "./cli-args.js";
20
+ import { getInheritedCliArgs } from "./cli-args.js";
19
21
  import { renderFlowCall, renderFlowResult } from "./render.js";
20
22
  import { getFlowSummaryText } from "./runner-events.js";
21
- import { runHooks } from "./hooks.js";
23
+ import { runHooks, runHooksDetailed, getRegisteredHooks, registerHook } from "./hooks.js";
22
24
  import { mapFlowConcurrent, runFlow } from "./flow.js";
25
+ import { executeFlows } from "./executor.js";
23
26
  import {
24
27
  type SingleResult,
25
28
  type FlowDetails,
26
29
  type CompressedFlowResult,
30
+ type FlowMetrics,
27
31
  type FileEntry,
28
32
  type CommandEntry,
33
+ type AutoTransition,
29
34
  emptyFlowUsage,
30
35
  isFlowError,
31
36
  isFlowSuccess,
@@ -86,7 +91,7 @@ const FlowParams = Type.Object({
86
91
  ),
87
92
  });
88
93
 
89
- const inheritedCliArgs = parseFlowCliArgs(process.argv);
94
+ const inheritedCliArgs = getInheritedCliArgs();
90
95
 
91
96
  // ---------------------------------------------------------------------------
92
97
  // Helpers
@@ -341,13 +346,15 @@ async function confirmProjectFlowsIfNeeded(
341
346
  // before the latest user message by the context handler.
342
347
  // ---------------------------------------------------------------------------
343
348
 
344
- const SLIDING_PROMPT_OPEN_TAG = "<pi-flow-sliding-system>";
345
- const SLIDING_PROMPT_CLOSE_TAG = "</pi-flow-sliding-system>";
349
+ const SLIDING_PROMPT_UUID = randomUUID();
350
+ const SLIDING_PROMPT_OPEN_TAG = `<pi-flow-sliding-system id="${SLIDING_PROMPT_UUID}">`;
351
+ const SLIDING_PROMPT_CLOSE_TAG = `</pi-flow-sliding-system id="${SLIDING_PROMPT_UUID}">`;
346
352
 
347
353
  const SLIDING_PROMPT =
348
354
  `${SLIDING_PROMPT_OPEN_TAG}\n` +
349
355
  `You are operating with pi-agent-flow routing.\n` +
350
356
  `If the answer is already in context, answer directly; otherwise delegate to the appropriate flow.\n` +
357
+ `For git, bash, CLI, or terminal tasks, delegate to [build].\n` +
351
358
  `${SLIDING_PROMPT_CLOSE_TAG}`;
352
359
 
353
360
  const SLIDING_PROMPT_RE = new RegExp(
@@ -357,9 +364,12 @@ const SLIDING_PROMPT_RE = new RegExp(
357
364
  "g",
358
365
  );
359
366
 
367
+ /** Legacy regex to strip old bare sliding system prompt tags (no id attribute). */
368
+ const LEGACY_SLIDING_PROMPT_RE = /<pi-flow-sliding-system(?:\s[^>]*)?>[\s\S]*?<\/pi-flow-sliding-system(?:\s[^>]*)?>/g;
369
+
360
370
  /** Strip any old sliding system prompt tags from a string. */
361
371
  function stripSlidingPromptText(text: string): string {
362
- return text.replace(SLIDING_PROMPT_RE, "");
372
+ return text.replace(SLIDING_PROMPT_RE, "").replace(LEGACY_SLIDING_PROMPT_RE, "");
363
373
  }
364
374
 
365
375
  /** Strip sliding prompt tags from content (string or text-part array). */
@@ -378,17 +388,31 @@ function stripSlidingPromptFromContent(
378
388
  }
379
389
 
380
390
  /** Check whether content (string or text-part array) contains the sliding tag. */
381
- function contentContainsSlidingTag(content: any): boolean {
391
+ /** Input-message content types (string or multipart text-part array). */
392
+ type MessageContent = string | Array<{ type: string; text?: string }>;
393
+
394
+ /** Type guard: is this a text-part with a string .text? */
395
+ function isTextPart(part: unknown): part is { type: "text"; text: string } {
396
+ return (
397
+ part != null &&
398
+ typeof part === "object" &&
399
+ "type" in part &&
400
+ part.type === "text" &&
401
+ "text" in part &&
402
+ typeof (part as { text?: unknown }).text === "string"
403
+ );
404
+ }
405
+
406
+ /** Check whether content (string or text-part array) contains the sliding tag. */
407
+ function contentContainsSlidingTag(content: MessageContent): boolean {
408
+ const hasCurrent = (text: string) => text.includes(SLIDING_PROMPT_OPEN_TAG);
409
+ const hasLegacy = (text: string) => text.includes("<pi-flow-sliding-system>");
410
+ const check = (text: string) => hasCurrent(text) || hasLegacy(text);
382
411
  if (typeof content === "string") {
383
- return content.includes(SLIDING_PROMPT_OPEN_TAG);
412
+ return check(content);
384
413
  }
385
414
  if (Array.isArray(content)) {
386
- return content.some(
387
- (part: any) =>
388
- part.type === "text" &&
389
- typeof part.text === "string" &&
390
- part.text.includes(SLIDING_PROMPT_OPEN_TAG),
391
- );
415
+ return content.some((part) => isTextPart(part) && check(part.text));
392
416
  }
393
417
  return false;
394
418
  }
@@ -474,24 +498,34 @@ function stripReasoningFromAssistantMessage(message: any): {
474
498
  return { message: next, changed };
475
499
  }
476
500
 
477
- function isJsonEqual(a: any, b: any): boolean {
478
- return JSON.stringify(a) === JSON.stringify(b);
501
+ /** Deep-equality check that handles unordered object keys (unlike JSON.stringify). */
502
+ function isJsonEqual(a: unknown, b: unknown): boolean {
503
+ if (Object.is(a, b)) return true;
504
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
505
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
506
+
507
+ const keysA = Object.keys(a as Record<string, unknown>);
508
+ const keysB = Object.keys(b as Record<string, unknown>);
509
+ if (keysA.length !== keysB.length) return false;
510
+
511
+ for (const key of keysA) {
512
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
513
+ if (!isJsonEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) return false;
514
+ }
515
+ return true;
479
516
  }
480
517
  // ---------------------------------------------------------------------------
481
518
  // Flow result compression
482
519
  // ---------------------------------------------------------------------------
483
520
 
484
- /**
485
- * Module-level cache populated after each flow tool execution.
486
- * Maps toolCallId → compressed flow result.
487
- * Consumed by compressFlowToolResults at fork time.
488
- */
489
- const flowResultCache = new Map<string, CompressedFlowResult[]>();
490
-
491
521
  /**
492
522
  * Build compressed representations of flow results and cache them by toolCallId.
493
523
  */
494
- function cacheFlowResults(toolCallId: string, results: SingleResult[]): void {
524
+ function cacheFlowResults(
525
+ cache: Map<string, CompressedFlowResult[]>,
526
+ toolCallId: string,
527
+ results: SingleResult[],
528
+ ): void {
495
529
  for (const result of results) {
496
530
  const so = result.structuredOutput;
497
531
  if (!so) continue;
@@ -502,9 +536,9 @@ function cacheFlowResults(toolCallId: string, results: SingleResult[]): void {
502
536
  if (so.files.length > 0) compressed.files = so.files;
503
537
  if (so.commands.length > 0) compressed.commands = so.commands;
504
538
  if (result.errorMessage) compressed.error = result.errorMessage;
505
- const existing = flowResultCache.get(toolCallId) ?? [];
539
+ const existing = cache.get(toolCallId) ?? [];
506
540
  existing.push(compressed);
507
- flowResultCache.set(toolCallId, existing);
541
+ cache.set(toolCallId, existing);
508
542
  }
509
543
  }
510
544
 
@@ -629,7 +663,7 @@ export function compressFlowToolResults(snapshot: string, cache: Map<string, Com
629
663
  * artifacts before passing full parent context to child flows: sliding system
630
664
  * prompts, legacy reminders, and assistant reasoning/thinking.
631
665
  */
632
- function sanitizeForkSnapshot(snapshot: string | null): string | null {
666
+ function sanitizeForkSnapshot(snapshot: string | null, cache: Map<string, CompressedFlowResult[]> = new Map()): string | null {
633
667
  if (!snapshot) return snapshot;
634
668
 
635
669
  const lines = snapshot.trimEnd().split("\n");
@@ -686,12 +720,12 @@ function sanitizeForkSnapshot(snapshot: string | null): string | null {
686
720
  }
687
721
 
688
722
  const sanitized = `${sanitizedLines.join("\n")}\n`;
689
- return compressFlowToolResults(sanitized, flowResultCache);
723
+ return compressFlowToolResults(sanitized, cache);
690
724
  }
691
725
 
692
726
  function computeActiveTools(optimize: boolean): string[] {
693
727
  return optimize
694
- ? ["batch_read", "bash", "flow"]
728
+ ? ["batch_read", "flow"]
695
729
  : ["read", "write", "edit", "batch", "bash", "flow", "web"];
696
730
  }
697
731
 
@@ -724,6 +758,14 @@ export default function (pi: ExtensionAPI) {
724
758
  description: "Model for full-tier flows (ideas, craft).",
725
759
  type: "string",
726
760
  });
761
+ pi.registerFlag("flow-max-concurrency", {
762
+ description: "Maximum number of flows to execute in parallel (default: 4).",
763
+ type: "string",
764
+ });
765
+ pi.registerFlag("auto-transition", {
766
+ description: "Automatically queue follow-up flows based on hook transitions (default: false).",
767
+ type: "boolean",
768
+ });
727
769
  pi.registerFlag("tool-optimize", {
728
770
  description: "Use the unified batch tool instead of separate read/write/edit tools (default: true).",
729
771
  type: "boolean",
@@ -737,6 +779,8 @@ export default function (pi: ExtensionAPI) {
737
779
  let toolOptimize = true;
738
780
  // structuredOutput: settings.json > default (true)
739
781
  let structuredOutput = true;
782
+ let maxConcurrency = 4;
783
+ let autoTransition = false;
740
784
  const envToolOptimize = process.env[FLOW_TOOL_OPTIMIZE_ENV];
741
785
  if (envToolOptimize !== undefined) {
742
786
  const parsed = parseBoolean(envToolOptimize);
@@ -771,6 +815,41 @@ export default function (pi: ExtensionAPI) {
771
815
  if (typeof flowSettings.structuredOutput === "boolean") {
772
816
  structuredOutput = flowSettings.structuredOutput;
773
817
  }
818
+ if (typeof flowSettings.maxConcurrency === "number") {
819
+ maxConcurrency = flowSettings.maxConcurrency;
820
+ }
821
+ if (typeof flowSettings.autoTransition === "boolean") {
822
+ autoTransition = flowSettings.autoTransition;
823
+ }
824
+ }
825
+
826
+ // Resolve maxConcurrency: CLI flag > env var > settings.json > default
827
+ const cliConcurrency = pi.getFlag("flow-max-concurrency");
828
+ if (typeof cliConcurrency === "string") {
829
+ const parsed = Number(cliConcurrency);
830
+ if (Number.isSafeInteger(parsed) && parsed >= 1) maxConcurrency = parsed;
831
+ } else if (typeof cliConcurrency === "number" && Number.isSafeInteger(cliConcurrency) && cliConcurrency >= 1) {
832
+ maxConcurrency = cliConcurrency;
833
+ }
834
+ const envConcurrency = process.env["PI_FLOW_MAX_CONCURRENCY"];
835
+ if (envConcurrency !== undefined && typeof cliConcurrency === "undefined") {
836
+ const parsed = Number(envConcurrency);
837
+ if (Number.isSafeInteger(parsed) && parsed >= 1) maxConcurrency = parsed;
838
+ }
839
+ // Cap concurrency to the number of available CPUs
840
+ if (typeof os.availableParallelism === "function") {
841
+ const hwConcurrency = os.availableParallelism();
842
+ if (hwConcurrency > 0) maxConcurrency = Math.min(maxConcurrency, hwConcurrency);
843
+ }
844
+
845
+ // Resolve autoTransition: CLI flag > settings.json > default
846
+ const cliAutoTransition = pi.getFlag("auto-transition");
847
+ if (typeof cliAutoTransition === "boolean") {
848
+ autoTransition = cliAutoTransition;
849
+ } else if (typeof cliAutoTransition === "string") {
850
+ const parsed = parseBoolean(cliAutoTransition);
851
+ if (parsed !== null) autoTransition = parsed;
852
+
774
853
  }
775
854
 
776
855
  // Only restrict tools for the main orchestrator (depth 0).
@@ -836,6 +915,13 @@ export default function (pi: ExtensionAPI) {
836
915
  return { systemPrompt };
837
916
  }
838
917
 
918
+ const flowList = discoveredFlows
919
+ .map((f) => {
920
+ const badge = f.source === "project" ? " 🔒" : f.source === "user" ? " ⚙" : "";
921
+ return `- [${f.name}]${badge} — ${f.description}`;
922
+ })
923
+ .join("\n");
924
+
839
925
  return {
840
926
  systemPrompt:
841
927
  systemPrompt +
@@ -843,12 +929,7 @@ export default function (pi: ExtensionAPI) {
843
929
 
844
930
  Before acting, reason about whether to dive into a flow:
845
931
 
846
- - [scout] — when you need to understand first. Find files, trace code paths, map architecture.
847
- - [debug] — when something is broken. Investigate logs, errors, stack traces to find root cause.
848
- - [build] — when you are ready to build. Implement features, fix bugs, write tests.
849
- - [craft] — when you need a plan. Design structure, break down requirements before building.
850
- - [audit] — when you need to verify and remediate. Audit security, quality, and correctness; fix safe issues directly.
851
- - [ideas] — when you need fresh ideas. Use inherited context as background while exploring alternatives.
932
+ ${flowList}
852
933
 
853
934
  Multiple independent flows? Batch them into one call:
854
935
 
@@ -942,17 +1023,15 @@ flow [type] accomplished
942
1023
 
943
1024
  async execute(toolCallId, params, signal, onUpdate, ctx) {
944
1025
  const discovery = discoverFlows(ctx.cwd, "all");
945
- flowResultCache.clear();
946
1026
  const { flows } = discovery;
947
1027
  const makeDetails = makeFlowDetailsFactory(discovery.projectFlowsDir);
948
1028
 
949
- const cliFlowModelConfig =
950
- typeof pi.getFlag("flow-model-config") === "string"
951
- ? (pi.getFlag("flow-model-config") as string)
952
- : inheritedCliArgs.flowModelConfig;
953
- const selectedFlowModelConfig = selectFlowModelStrategy(
954
- loadedFlowModelConfigs.configs,
955
- cliFlowModelConfig ?? loadedFlowModelConfigs.selectedName,
1029
+ // Build the full fork session snapshot and sanitize only non-inheritable
1030
+ // artifacts before passing it to child flows.
1031
+ const flowResultCache = new Map<string, CompressedFlowResult[]>();
1032
+ const forkSessionSnapshotJsonl = sanitizeForkSnapshot(
1033
+ buildForkSessionSnapshotJsonl(ctx.sessionManager),
1034
+ flowResultCache,
956
1035
  );
957
1036
 
958
1037
  const getTierOverride = (tier: "lite" | "flash" | "full"): string | undefined => {
@@ -968,195 +1047,42 @@ flow [type] accomplished
968
1047
  return typeof inheritedValue === "string" && inheritedValue.trim() ? inheritedValue.trim() : undefined;
969
1048
  };
970
1049
 
971
- const shouldFailover = (result: SingleResult): boolean => {
972
- if (result.stopReason === "aborted") return false;
973
- const text = `${result.errorMessage ?? ""}\n${result.stderr ?? ""}`.toLowerCase();
974
- if (!text.trim()) return false;
975
- if (text.includes("permission") || text.includes("invalid tool") || text.includes("bad settings")) {
976
- return false;
977
- }
978
- return result.exitCode > 0;
979
- };
980
-
981
- // Build the full fork session snapshot and sanitize only non-inheritable
982
- // artifacts before passing it to child flows.
983
- const forkSessionSnapshotJsonl = sanitizeForkSnapshot(
984
- buildForkSessionSnapshotJsonl(ctx.sessionManager),
985
- );
986
-
987
- // Collect all requested flow names
988
- const requested = new Set<string>(params.flow.map((f: any) => f.type.toLowerCase()));
989
-
990
- // Cycle check
991
- if (preventCycles) {
992
- const violations = getFlowCycleViolations(requested, ancestorFlowStack);
993
- if (violations.length > 0) {
994
- const stack = ancestorFlowStack.join(" -> ") || "(root)";
995
- return {
996
- content: [{
997
- type: "text",
998
- text: `Blocked: cycle detected. Flow(s) in stack: ${violations.join(", ")}\nStack: ${stack}`,
999
- }],
1000
- details: makeDetails([]),
1001
- isError: true,
1002
- };
1003
- }
1004
- }
1005
-
1006
- // Project flow confirmation
1007
- const shouldConfirm = params.confirmProjectFlows ?? true;
1008
- if (shouldConfirm) {
1009
- const projectFlows = getRequestedProjectFlows(flows, requested);
1010
- if (projectFlows.length > 0) {
1011
- if (ctx.hasUI) {
1012
- const ok = await confirmProjectFlowsIfNeeded(projectFlows, discovery.projectFlowsDir, ctx);
1013
- if (!ok) {
1014
- return {
1015
- content: [{ type: "text", text: "Canceled: project-local flows not approved." }],
1016
- details: makeDetails([]),
1017
- };
1018
- }
1019
- } else {
1020
- const names = projectFlows.map((f) => f.name).join(", ");
1021
- return {
1022
- content: [{
1023
- type: "text",
1024
- text: `Blocked: project-local flow confirmation required in non-UI mode.\nFlows: ${names}\nRe-run with confirmProjectFlows: false if trusted.`,
1025
- }],
1026
- details: makeDetails([]),
1027
- isError: true,
1028
- };
1029
- }
1030
- }
1031
- }
1032
-
1033
- // Run all flows in parallel
1034
- const allResults: SingleResult[] = new Array(params.flow.length);
1035
- for (let i = 0; i < params.flow.length; i++) {
1036
- allResults[i] = {
1037
- type: params.flow[i].type,
1038
- agentSource: "unknown",
1039
- intent: params.flow[i].intent,
1040
- aim: params.flow[i].aim,
1041
- exitCode: -1,
1042
- messages: [],
1043
- stderr: "",
1044
- usage: emptyFlowUsage(),
1045
- };
1046
- }
1047
-
1048
- let lastStreamingText = "";
1049
- let lastEmittedSignature: string | undefined;
1050
- const emitProgress = (streamingText?: string) => {
1051
- if (!onUpdate) return;
1052
- if (streamingText !== undefined) lastStreamingText = streamingText;
1053
- const text = lastStreamingText || "";
1054
- const signature =
1055
- text +
1056
- "|" +
1057
- allResults
1058
- .map(
1059
- (r) =>
1060
- `${r.messages.length}:${r.usage.toolCalls}:${r.usage.input}:${r.usage.output}:${r.usage.contextTokens}:${r.usage.smoothedTps ?? 0}:${r.errorMessage ?? ""}`,
1061
- )
1062
- .join(";");
1063
- if (signature === lastEmittedSignature) return;
1064
- lastEmittedSignature = signature;
1065
- onUpdate({
1066
- content: [{ type: "text", text }],
1067
- details: makeDetails([...allResults]),
1068
- });
1069
- };
1070
-
1071
- if (onUpdate) emitProgress();
1072
-
1073
- const results = await mapFlowConcurrent(params.flow, 4, async (item: any, index: number) => {
1074
- const normalizedType = item.type.toLowerCase();
1075
- const targetFlow = flows.find((f) => f.name === normalizedType);
1076
- const effectiveMaxDepth =
1077
- targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
1078
-
1079
- const shouldInheritContext = targetFlow?.inheritContext !== false;
1080
- const tier = getFlowTier(normalizedType);
1081
- const { candidates } = resolveFlowModelCandidates({
1082
- tier,
1083
- flowModel: targetFlow?.model,
1084
- cliTierOverride: getTierOverride(tier),
1085
- strategy: selectedFlowModelConfig.strategy,
1050
+ const result = await executeFlows(
1051
+ {
1052
+ flows,
1053
+ currentDepth,
1054
+ maxDepth,
1055
+ ancestorFlowStack,
1056
+ preventCycles,
1057
+ toolOptimize,
1058
+ structuredOutput,
1059
+ cwd: ctx.cwd,
1060
+ loadedFlowModelConfigs,
1061
+ maxConcurrency,
1062
+ autoTransition,
1063
+ signal,
1064
+ onUpdate,
1065
+ makeDetails,
1066
+ getFlag: (name: string) => pi.getFlag(name),
1067
+ tierOverrideResolver: getTierOverride,
1086
1068
  fallbackModel: inheritedCliArgs.fallbackModel,
1087
- });
1088
- const attemptModels = candidates.length > 0 ? candidates : [undefined];
1089
- const attemptedModels: string[] = [];
1090
- let result = allResults[index];
1091
-
1092
- for (let attempt = 0; attempt < attemptModels.length; attempt++) {
1093
- const candidateModel = attemptModels[attempt];
1094
- if (candidateModel) attemptedModels.push(candidateModel);
1095
- result = await runFlow({
1096
- cwd: ctx.cwd,
1097
- flows,
1098
- flowName: normalizedType,
1099
- intent: item.intent,
1100
- aim: item.aim,
1101
- taskCwd: item.cwd,
1102
- forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
1103
- parentDepth: currentDepth,
1104
- parentFlowStack: ancestorFlowStack,
1105
- maxDepth: effectiveMaxDepth,
1106
- preventCycles,
1107
- toolOptimize,
1108
- structuredOutput,
1109
- model: candidateModel,
1110
- signal,
1111
- onUpdate: (partial) => {
1112
- if (partial.details?.results[0]) {
1113
- allResults[index] = partial.details.results[0];
1114
- emitProgress(partial.content?.[0]?.text);
1115
- }
1116
- },
1117
- makeDetails,
1118
- });
1119
- allResults[index] = result;
1120
- emitProgress();
1121
- if (isFlowSuccess(result) || signal?.aborted) break;
1122
- if (attempt < attemptModels.length - 1 && shouldFailover(result)) {
1123
- continue;
1124
- }
1125
- break;
1126
- }
1127
-
1128
- if (result && !isFlowSuccess(result) && attemptedModels.length > 1) {
1129
- const summary = `Model failover attempts: ${attemptedModels.join(" -> ")}`;
1130
- const baseStderr = result.stderr.trim();
1131
- result.stderr = baseStderr ? `${baseStderr}\n\n${summary}` : summary;
1132
- allResults[index] = result;
1133
- emitProgress();
1134
- }
1135
-
1136
- return result;
1137
- });
1138
-
1139
- cacheFlowResults(toolCallId, results);
1140
- // Build tool result with FULL flow output — no truncation
1141
- const successCount = results.filter((r) => isFlowSuccess(r)).length;
1142
- const flowReports = results.map((r) => {
1143
- const output = getFlowSummaryText(r);
1144
- const status = isFlowError(r) ? "failed" : "accomplished";
1145
- return `flow [${r.type}] ${status}\n\n${output}`;
1146
- });
1147
-
1148
- // Post-flow hooks — inject advisory messages
1149
- const advisors = runHooks(params.flow, results);
1150
- const advisorBlock = advisors.length > 0
1151
- ? "\n\n---\n\n💡 " + advisors.join("\n💡 ")
1152
- : "";
1069
+ forkSessionSnapshotJsonl,
1070
+ flowResultCache,
1071
+ projectFlowsDir: discovery.projectFlowsDir,
1072
+ sessionManager: ctx.sessionManager,
1073
+ hasUI: ctx.hasUI,
1074
+ uiConfirm: (title, body) => ctx.ui.confirm(title, body),
1075
+ onFlowMetrics: (metrics) => pi.emit("pi-agent-flow:complete", metrics),
1076
+ confirmProjectFlows: params.confirmProjectFlows,
1077
+ },
1078
+ params.flow.map((f: any) => ({ type: f.type, intent: f.intent, aim: f.aim, cwd: f.cwd })),
1079
+ toolCallId,
1080
+ );
1153
1081
 
1154
1082
  return {
1155
- content: [{
1156
- type: "text" as const,
1157
- text: `Flow: ${successCount}/${results.length} completed\n\n${flowReports.join("\n\n---\n\n")}${advisorBlock}`,
1158
- }],
1159
- details: makeDetails(results),
1083
+ content: result.content,
1084
+ details: result.details,
1085
+ isError: result.isError,
1160
1086
  };
1161
1087
  },
1162
1088
 
@@ -1166,4 +1092,19 @@ flow [type] accomplished
1166
1092
  });
1167
1093
  }
1168
1094
 
1095
+ // -------------------------------------------------------------------------
1096
+ // Public plugin API — expose for third-party extensions
1097
+ // -------------------------------------------------------------------------
1098
+
1099
+ // Emit a ready event with the API surface so external plugins can extend.
1100
+ if (typeof pi.emit === "function") {
1101
+ pi.emit("pi-agent-flow:ready", {
1102
+ registerHook,
1103
+ getRegisteredHooks,
1104
+ discoverFlows: (cwd: string) => discoverFlows(cwd, "all"),
1105
+ getFlowTier: (name: string) => getFlowTier(name),
1106
+ getSettings: () => ({ toolOptimize, structuredOutput, maxConcurrency, autoTransition }),
1107
+ });
1108
+ }
1109
+
1169
1110
  }
package/src/types.ts CHANGED
@@ -315,6 +315,8 @@ export interface PostFlowHookResult {
315
315
  content: string;
316
316
  /** Ordering key; lower runs first. Default: 0. */
317
317
  priority?: number;
318
+ /** Optional auto-transition to queue after this flow completes. */
319
+ autoTransition?: AutoTransition;
318
320
  }
319
321
 
320
322
  /** A post-flow hook that injects advisory messages into tool results. */
@@ -326,3 +328,43 @@ export interface PostFlowHook {
326
328
  /** Returns advisory text, or null to skip. */
327
329
  action: (ctx: PostFlowHookContext) => PostFlowHookResult | null;
328
330
  }
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // Telemetry types
334
+ // ---------------------------------------------------------------------------
335
+
336
+ /** Metrics emitted after each flow completes. */
337
+ export interface FlowMetrics {
338
+ /** Flow type name. */
339
+ type: string;
340
+ /** Duration in milliseconds. */
341
+ durationMs: number;
342
+ /** Final exit code. */
343
+ exitCode: number;
344
+ /** Whether the flow succeeded. */
345
+ success: boolean;
346
+ /** Model used for this execution. */
347
+ model?: string;
348
+ /** Number of failover attempts. */
349
+ failoverCount: number;
350
+ /** Token usage. */
351
+ usage: UsageStats;
352
+ /** Flow source. */
353
+ source: string;
354
+ /** Current delegation depth. */
355
+ depth: number;
356
+ }
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // Auto-transition types
360
+ // ---------------------------------------------------------------------------
361
+
362
+ /** Descriptor for an auto-queued follow-up flow. */
363
+ export interface AutoTransition {
364
+ /** Flow type to run next. */
365
+ type: string;
366
+ /** Intent for the follow-up flow. */
367
+ intent: string;
368
+ /** Confidence score (0-1). Higher means more certain. */
369
+ confidence: number;
370
+ }