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/agents/debug.md +3 -0
- package/agents/scout.md +3 -0
- package/package.json +1 -1
- package/src/agents.ts +12 -1
- package/src/ambient.d.ts +1 -0
- package/src/batch/constants.ts +102 -0
- package/src/batch/execute.ts +526 -0
- package/src/batch/fuzzy-edit.ts +323 -0
- package/src/batch/index.ts +364 -0
- package/src/batch/render.ts +74 -0
- package/src/batch/symbols.ts +341 -0
- package/src/batch.ts +13 -1647
- package/src/cli-args.ts +11 -0
- package/src/config.ts +10 -0
- package/src/executor.ts +399 -0
- package/src/flow.ts +12 -11
- package/src/hooks.ts +112 -2
- package/src/index.ts +170 -229
- package/src/types.ts +42 -0
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 {
|
|
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 =
|
|
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
|
|
345
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
478
|
-
|
|
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(
|
|
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 =
|
|
539
|
+
const existing = cache.get(toolCallId) ?? [];
|
|
506
540
|
existing.push(compressed);
|
|
507
|
-
|
|
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,
|
|
723
|
+
return compressFlowToolResults(sanitized, cache);
|
|
690
724
|
}
|
|
691
725
|
|
|
692
726
|
function computeActiveTools(optimize: boolean): string[] {
|
|
693
727
|
return optimize
|
|
694
|
-
? ["batch_read", "
|
|
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
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
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
|
+
}
|