pi-agent-flow 1.1.0 → 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/README.md +158 -44
- package/agents/audit.md +37 -17
- package/agents/build.md +42 -38
- package/agents/craft.md +33 -17
- package/agents/debug.md +32 -15
- package/agents/ideas.md +33 -11
- package/agents/scout.md +32 -12
- package/package.json +4 -15
- package/{agents.ts → src/agents.ts} +10 -4
- package/src/ambient.d.ts +85 -0
- package/{batch.ts → src/batch.ts} +256 -118
- package/src/cli-args.ts +283 -0
- package/src/config.ts +305 -0
- package/{flow.ts → src/flow.ts} +39 -18
- package/{hooks.ts → src/hooks.ts} +6 -6
- package/{index.ts → src/index.ts} +361 -102
- package/{render-utils.ts → src/render-utils.ts} +2 -2
- package/{render.ts → src/render.ts} +15 -10
- package/src/runner-events.ts +692 -0
- package/{types.ts → src/types.ts} +2 -0
- package/config.ts +0 -102
- package/runner-cli.js +0 -254
- package/runner-events.js +0 -559
- /package/{web-tool.ts → src/web-tool.ts} +0 -0
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
|
-
import { type FlowConfig, discoverFlows } from "./agents.js";
|
|
11
|
-
import {
|
|
10
|
+
import { type FlowConfig, discoverFlows, getFlowTier } from "./agents.js";
|
|
11
|
+
import {
|
|
12
|
+
loadFlowModelConfigs,
|
|
13
|
+
loadFlowSettings,
|
|
14
|
+
resolveFlowModelCandidates,
|
|
15
|
+
selectFlowModelStrategy,
|
|
16
|
+
type LoadedFlowModelConfigs,
|
|
17
|
+
} from "./config.js";
|
|
18
|
+
import { parseFlowCliArgs } from "./cli-args.js";
|
|
12
19
|
import { renderFlowCall, renderFlowResult } from "./render.js";
|
|
13
20
|
import { getFlowSummaryText } from "./runner-events.js";
|
|
14
21
|
import { runHooks } from "./hooks.js";
|
|
@@ -20,7 +27,7 @@ import {
|
|
|
20
27
|
isFlowError,
|
|
21
28
|
isFlowSuccess,
|
|
22
29
|
} from "./types.js";
|
|
23
|
-
import { createBatchTool } from "./batch.js";
|
|
30
|
+
import { createBatchTool, createBatchReadTool } from "./batch.js";
|
|
24
31
|
import {
|
|
25
32
|
createWebTool,
|
|
26
33
|
looksLikeUrlPrompt,
|
|
@@ -73,6 +80,8 @@ const FlowParams = Type.Object({
|
|
|
73
80
|
),
|
|
74
81
|
});
|
|
75
82
|
|
|
83
|
+
const inheritedCliArgs = parseFlowCliArgs(process.argv);
|
|
84
|
+
|
|
76
85
|
// ---------------------------------------------------------------------------
|
|
77
86
|
// Helpers
|
|
78
87
|
// ---------------------------------------------------------------------------
|
|
@@ -97,23 +106,8 @@ function buildForkSessionSnapshotJsonl(
|
|
|
97
106
|
if (!header || typeof header !== "object") return null;
|
|
98
107
|
|
|
99
108
|
const branchEntries = sessionManager.getBranch();
|
|
100
|
-
|
|
101
|
-
// Trim the current conversation turn from the fork.
|
|
102
|
-
let trimFrom = branchEntries.length;
|
|
103
|
-
for (let i = branchEntries.length - 1; i >= 0; i--) {
|
|
104
|
-
const entry = branchEntries[i] as Record<string, unknown>;
|
|
105
|
-
if (entry?.type === "message") {
|
|
106
|
-
const msg = entry.message as Record<string, unknown> | undefined;
|
|
107
|
-
if (msg?.role === "user") {
|
|
108
|
-
trimFrom = i;
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const trimmedEntries = branchEntries.slice(0, trimFrom);
|
|
115
109
|
const lines = [JSON.stringify(header)];
|
|
116
|
-
for (const entry of
|
|
110
|
+
for (const entry of branchEntries) lines.push(JSON.stringify(entry));
|
|
117
111
|
return `${lines.join("\n")}\n`;
|
|
118
112
|
}
|
|
119
113
|
|
|
@@ -335,46 +329,216 @@ async function confirmProjectFlowsIfNeeded(
|
|
|
335
329
|
}
|
|
336
330
|
|
|
337
331
|
// ---------------------------------------------------------------------------
|
|
338
|
-
//
|
|
332
|
+
// Sliding system prompt (short, inserted before latest user message)
|
|
333
|
+
// Always active for root flows. Appended to the static system prompt in
|
|
334
|
+
// before_agent_start, and inserted as a separate system message immediately
|
|
335
|
+
// before the latest user message by the context handler.
|
|
339
336
|
// ---------------------------------------------------------------------------
|
|
340
337
|
|
|
341
|
-
const
|
|
342
|
-
|
|
338
|
+
const SLIDING_PROMPT_OPEN_TAG = "<pi-flow-sliding-system>";
|
|
339
|
+
const SLIDING_PROMPT_CLOSE_TAG = "</pi-flow-sliding-system>";
|
|
340
|
+
|
|
341
|
+
const SLIDING_PROMPT =
|
|
342
|
+
`${SLIDING_PROMPT_OPEN_TAG}\n` +
|
|
343
|
+
`You are operating with pi-agent-flow routing.\n` +
|
|
344
|
+
`If the answer is already in context, answer directly; otherwise delegate to the appropriate flow.\n` +
|
|
345
|
+
`${SLIDING_PROMPT_CLOSE_TAG}`;
|
|
346
|
+
|
|
347
|
+
const SLIDING_PROMPT_RE = new RegExp(
|
|
348
|
+
SLIDING_PROMPT_OPEN_TAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +
|
|
349
|
+
"[\\s\\S]*?" +
|
|
350
|
+
SLIDING_PROMPT_CLOSE_TAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
351
|
+
"g",
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
/** Strip any old sliding system prompt tags from a string. */
|
|
355
|
+
function stripSlidingPromptText(text: string): string {
|
|
356
|
+
return text.replace(SLIDING_PROMPT_RE, "");
|
|
357
|
+
}
|
|
343
358
|
|
|
344
|
-
|
|
359
|
+
/** Strip sliding prompt tags from content (string or text-part array). */
|
|
360
|
+
function stripSlidingPromptFromContent(
|
|
345
361
|
content: string | { type: string; text?: string }[],
|
|
346
362
|
): string | { type: string; text?: string }[] {
|
|
347
363
|
if (typeof content === "string") {
|
|
348
|
-
return content
|
|
364
|
+
return stripSlidingPromptText(content);
|
|
349
365
|
}
|
|
350
366
|
return content.map((c) => {
|
|
351
367
|
if (c.type === "text" && typeof c.text === "string") {
|
|
352
|
-
return { ...c, text: c.text
|
|
368
|
+
return { ...c, text: stripSlidingPromptText(c.text) };
|
|
353
369
|
}
|
|
354
370
|
return c;
|
|
355
371
|
});
|
|
356
372
|
}
|
|
357
373
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
): string | { type: string; text?: string }[] {
|
|
374
|
+
/** Check whether content (string or text-part array) contains the sliding tag. */
|
|
375
|
+
function contentContainsSlidingTag(content: any): boolean {
|
|
361
376
|
if (typeof content === "string") {
|
|
362
|
-
return content
|
|
377
|
+
return content.includes(SLIDING_PROMPT_OPEN_TAG);
|
|
378
|
+
}
|
|
379
|
+
if (Array.isArray(content)) {
|
|
380
|
+
return content.some(
|
|
381
|
+
(part: any) =>
|
|
382
|
+
part.type === "text" &&
|
|
383
|
+
typeof part.text === "string" &&
|
|
384
|
+
part.text.includes(SLIDING_PROMPT_OPEN_TAG),
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Remove any existing sliding-system-prompt system messages and strip tags from user messages.
|
|
391
|
+
* Returns the sanitized messages and a flag indicating whether anything changed.
|
|
392
|
+
*/
|
|
393
|
+
function stripSlidingPromptsFromMessages(messages: any[]): { messages: any[]; changed: boolean } {
|
|
394
|
+
let changed = false;
|
|
395
|
+
const result = messages
|
|
396
|
+
.filter((msg) => {
|
|
397
|
+
// Remove dedicated sliding system prompt messages
|
|
398
|
+
if (msg.role === "system" && contentContainsSlidingTag(msg.content)) {
|
|
399
|
+
changed = true;
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
})
|
|
404
|
+
.map((msg) => {
|
|
405
|
+
// Also strip stray tags embedded in user/assistant messages
|
|
406
|
+
if (!("content" in msg)) return msg;
|
|
407
|
+
const stripped = stripSlidingPromptFromContent(msg.content);
|
|
408
|
+
if (isJsonEqual(stripped, msg.content)) return msg;
|
|
409
|
+
changed = true;
|
|
410
|
+
return { ...msg, content: stripped };
|
|
411
|
+
});
|
|
412
|
+
return { messages: result, changed };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Build a system message containing the sliding prompt. */
|
|
416
|
+
function makeSlidingPromptMessage(referenceMessage?: any): any {
|
|
417
|
+
return {
|
|
418
|
+
role: "system",
|
|
419
|
+
content: SLIDING_PROMPT,
|
|
420
|
+
timestamp: referenceMessage?.timestamp,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const REASONING_PART_TYPES = new Set([
|
|
425
|
+
"thinking",
|
|
426
|
+
"reasoning",
|
|
427
|
+
"reasoning_content",
|
|
428
|
+
"reasoningContent",
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
const REASONING_FIELDS = [
|
|
432
|
+
"thinking",
|
|
433
|
+
"thinkingSignature",
|
|
434
|
+
"thinking_signature",
|
|
435
|
+
"reasoning",
|
|
436
|
+
"reasoningContent",
|
|
437
|
+
"reasoning_content",
|
|
438
|
+
"reasoningSignature",
|
|
439
|
+
"reasoning_signature",
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
function stripReasoningFromAssistantMessage(message: any): {
|
|
443
|
+
message: any;
|
|
444
|
+
changed: boolean;
|
|
445
|
+
} {
|
|
446
|
+
let next = message;
|
|
447
|
+
let changed = false;
|
|
448
|
+
|
|
449
|
+
for (const field of REASONING_FIELDS) {
|
|
450
|
+
if (field in next) {
|
|
451
|
+
if (next === message) next = { ...message };
|
|
452
|
+
delete next[field];
|
|
453
|
+
changed = true;
|
|
454
|
+
}
|
|
363
455
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
456
|
+
|
|
457
|
+
if (Array.isArray(message.content)) {
|
|
458
|
+
const filteredContent = message.content.filter(
|
|
459
|
+
(part: any) => !REASONING_PART_TYPES.has(part?.type),
|
|
460
|
+
);
|
|
461
|
+
if (filteredContent.length !== message.content.length) {
|
|
462
|
+
if (next === message) next = { ...message };
|
|
463
|
+
next.content = filteredContent;
|
|
464
|
+
changed = true;
|
|
465
|
+
}
|
|
371
466
|
}
|
|
372
|
-
|
|
467
|
+
|
|
468
|
+
return { message: next, changed };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isJsonEqual(a: any, b: any): boolean {
|
|
472
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Sanitize a fork session snapshot JSONL to remove only non-inheritable
|
|
477
|
+
* artifacts before passing full parent context to child flows: sliding system
|
|
478
|
+
* prompts, legacy reminders, and assistant reasoning/thinking.
|
|
479
|
+
*/
|
|
480
|
+
function sanitizeForkSnapshot(snapshot: string | null): string | null {
|
|
481
|
+
if (!snapshot) return snapshot;
|
|
482
|
+
|
|
483
|
+
const lines = snapshot.trimEnd().split("\n");
|
|
484
|
+
const sanitizedLines: string[] = [];
|
|
485
|
+
|
|
486
|
+
for (const line of lines) {
|
|
487
|
+
let entry: any;
|
|
488
|
+
try {
|
|
489
|
+
entry = JSON.parse(line);
|
|
490
|
+
} catch {
|
|
491
|
+
sanitizedLines.push(line);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
let changed = false;
|
|
496
|
+
|
|
497
|
+
// Drop sliding system prompt messages entirely.
|
|
498
|
+
if (
|
|
499
|
+
entry?.type === "message" &&
|
|
500
|
+
entry.message?.role === "system" &&
|
|
501
|
+
contentContainsSlidingTag(entry.message?.content)
|
|
502
|
+
) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (entry?.type === "message" && entry.message) {
|
|
507
|
+
let message = entry.message;
|
|
508
|
+
|
|
509
|
+
if (message.role === "assistant") {
|
|
510
|
+
const stripped = stripReasoningFromAssistantMessage(message);
|
|
511
|
+
message = stripped.message;
|
|
512
|
+
changed ||= stripped.changed;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if ("content" in message) {
|
|
516
|
+
const originalContent = message.content;
|
|
517
|
+
const strippedContent = stripSlidingPromptFromContent(originalContent);
|
|
518
|
+
|
|
519
|
+
if (!isJsonEqual(strippedContent, originalContent)) {
|
|
520
|
+
message = {
|
|
521
|
+
...message,
|
|
522
|
+
content: strippedContent,
|
|
523
|
+
};
|
|
524
|
+
changed = true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (changed) {
|
|
529
|
+
entry = { ...entry, message };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
sanitizedLines.push(changed ? JSON.stringify(entry) : line);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return `${sanitizedLines.join("\n")}\n`;
|
|
373
537
|
}
|
|
374
538
|
|
|
375
539
|
function computeActiveTools(optimize: boolean): string[] {
|
|
376
540
|
return optimize
|
|
377
|
-
? ["
|
|
541
|
+
? ["batch_read", "bash", "flow"]
|
|
378
542
|
: ["read", "write", "edit", "batch", "bash", "flow", "web"];
|
|
379
543
|
}
|
|
380
544
|
|
|
@@ -391,6 +555,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
391
555
|
description: "Block delegating to flows already in the current delegation stack (default: true).",
|
|
392
556
|
type: "boolean",
|
|
393
557
|
});
|
|
558
|
+
pi.registerFlag("flow-model-config", {
|
|
559
|
+
description: "Named flow model strategy from settings.json flowModelConfigs.",
|
|
560
|
+
type: "string",
|
|
561
|
+
});
|
|
394
562
|
pi.registerFlag("flow-lite-model", {
|
|
395
563
|
description: "Model for lite-tier flows (scout, debug).",
|
|
396
564
|
type: "string",
|
|
@@ -421,13 +589,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
589
|
}
|
|
422
590
|
|
|
423
591
|
let discoveredFlows: FlowConfig[] = [];
|
|
424
|
-
let
|
|
592
|
+
let loadedFlowModelConfigs: LoadedFlowModelConfigs = {
|
|
593
|
+
selectedName: "default",
|
|
594
|
+
configs: { default: {} },
|
|
595
|
+
strategy: {},
|
|
596
|
+
};
|
|
425
597
|
|
|
426
598
|
// Auto-discover flows on session start
|
|
427
599
|
pi.on("session_start", async (_event, ctx) => {
|
|
428
600
|
const discovery = discoverFlows(ctx.cwd, "all");
|
|
429
601
|
discoveredFlows = discovery.flows;
|
|
430
|
-
|
|
602
|
+
loadedFlowModelConfigs = loadFlowModelConfigs(ctx.cwd);
|
|
431
603
|
|
|
432
604
|
// Resolve toolOptimize: CLI flag > env var > settings.json > default
|
|
433
605
|
const cliFlag = pi.getFlag("tool-optimize");
|
|
@@ -450,8 +622,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
450
622
|
pi.setActiveTools(computeActiveTools(toolOptimize));
|
|
451
623
|
}
|
|
452
624
|
|
|
453
|
-
// Register batch so
|
|
625
|
+
// Register batch and batch_read so they are available for main agent and child flows.
|
|
454
626
|
if (toolOptimize) {
|
|
627
|
+
pi.registerTool(createBatchReadTool());
|
|
455
628
|
pi.registerTool(createBatchTool());
|
|
456
629
|
}
|
|
457
630
|
});
|
|
@@ -486,14 +659,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
486
659
|
}
|
|
487
660
|
|
|
488
661
|
let systemPrompt = event.systemPrompt;
|
|
489
|
-
if (webInstructions.length > 0) {
|
|
662
|
+
if (!toolOptimize && webInstructions.length > 0) {
|
|
490
663
|
systemPrompt +=
|
|
491
664
|
"\n\n## pi-web steering\n" +
|
|
492
665
|
webInstructions.map((line) => `- ${line}`).join("\n");
|
|
493
666
|
}
|
|
494
667
|
|
|
668
|
+
// Append sliding prompt to static system prompt unconditionally.
|
|
669
|
+
systemPrompt += "\n\n" + SLIDING_PROMPT;
|
|
670
|
+
|
|
495
671
|
if (!canDelegate || discoveredFlows.length === 0) {
|
|
496
|
-
return
|
|
672
|
+
return { systemPrompt };
|
|
497
673
|
}
|
|
498
674
|
|
|
499
675
|
return {
|
|
@@ -507,8 +683,8 @@ Before acting, reason about whether to dive into a flow:
|
|
|
507
683
|
- [debug] — when something is broken. Investigate logs, errors, stack traces to find root cause.
|
|
508
684
|
- [build] — when you are ready to build. Implement features, fix bugs, write tests.
|
|
509
685
|
- [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.
|
|
686
|
+
- [audit] — when you need to verify and remediate. Audit security, quality, and correctness; fix safe issues directly.
|
|
687
|
+
- [ideas] — when you need fresh ideas. Use inherited context as background while exploring alternatives.
|
|
512
688
|
|
|
513
689
|
Multiple independent flows? Batch them into one call:
|
|
514
690
|
|
|
@@ -518,16 +694,16 @@ Multiple independent flows? Batch them into one call:
|
|
|
518
694
|
Each call renders as:
|
|
519
695
|
|
|
520
696
|
• flow [scout] — Map the full directory structure...
|
|
521
|
-
• flow [audit] — Audit security and quality...
|
|
697
|
+
• flow [audit] — Audit security and quality, then fix safe issues...
|
|
522
698
|
|
|
523
699
|
Each flow returns:
|
|
524
700
|
|
|
525
701
|
flow [type] accomplished
|
|
526
702
|
|
|
527
|
-
[Summary] — what happened
|
|
528
|
-
[Done] — completed with file:line references
|
|
529
|
-
[Not Done] — incomplete items and reasons
|
|
530
|
-
[Next Steps] — recommended follow-up
|
|
703
|
+
[Summary] — what happened and current status
|
|
704
|
+
[Done] — completed work with file:line references and verification results
|
|
705
|
+
[Not Done] — incomplete items, skipped checks, blockers, and reasons
|
|
706
|
+
[Next Steps] — specific recommended follow-up or next flow
|
|
531
707
|
|
|
532
708
|
### Guards
|
|
533
709
|
- Depth: ${currentDepth}/${maxDepth} | Cycles: ${preventCycles ? "blocked" : "off"} | Stack: ${ancestorFlowStack.length > 0 ? ancestorFlowStack.join(" -> ") : "(root)"}
|
|
@@ -535,32 +711,50 @@ flow [type] accomplished
|
|
|
535
711
|
};
|
|
536
712
|
});
|
|
537
713
|
|
|
538
|
-
// Sliding
|
|
539
|
-
//
|
|
540
|
-
//
|
|
714
|
+
// Sliding system prompt: insert as a separate system message immediately
|
|
715
|
+
// before the latest user message each turn. Strips from the static
|
|
716
|
+
// systemPrompt to avoid duplication, then inserts separately.
|
|
717
|
+
// Skipped for child flows (depth > 0) — they have explicit <mission> directives.
|
|
541
718
|
pi.on("context", async (event) => {
|
|
542
719
|
if (currentDepth > 0) return undefined;
|
|
543
720
|
|
|
544
|
-
|
|
721
|
+
// Always strip old sliding prompt messages to prevent accumulation
|
|
722
|
+
const { messages, changed: messagesChanged } = stripSlidingPromptsFromMessages(event.messages);
|
|
723
|
+
|
|
724
|
+
// Find latest user message
|
|
545
725
|
const userIndices = messages
|
|
546
726
|
.map((m: any, i: number) => (m.role === "user" ? i : -1))
|
|
547
727
|
.filter((i: number) => i !== -1);
|
|
548
728
|
|
|
549
|
-
if (userIndices.length === 0)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const modified = messages.map((msg: any, idx: number) => {
|
|
554
|
-
if (msg.role !== "user") return msg;
|
|
729
|
+
if (userIndices.length === 0) {
|
|
730
|
+
// No user message yet: keep sliding prompt in the static system prompt only.
|
|
731
|
+
return messagesChanged ? { messages } : undefined;
|
|
732
|
+
}
|
|
555
733
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
734
|
+
// Strip sliding from the static systemPrompt so it only appears once,
|
|
735
|
+
// as a separate message right before the latest user message.
|
|
736
|
+
let systemPrompt = event.systemPrompt;
|
|
737
|
+
let systemPromptChanged = false;
|
|
738
|
+
if (typeof systemPrompt === "string") {
|
|
739
|
+
const stripped = stripSlidingPromptText(systemPrompt);
|
|
740
|
+
if (stripped !== systemPrompt) {
|
|
741
|
+
systemPrompt = stripped;
|
|
742
|
+
systemPromptChanged = true;
|
|
559
743
|
}
|
|
560
|
-
|
|
561
|
-
});
|
|
744
|
+
}
|
|
562
745
|
|
|
563
|
-
|
|
746
|
+
const lastUserIndex = userIndices[userIndices.length - 1];
|
|
747
|
+
const modified = [
|
|
748
|
+
...messages.slice(0, lastUserIndex),
|
|
749
|
+
makeSlidingPromptMessage(messages[lastUserIndex]),
|
|
750
|
+
...messages.slice(lastUserIndex),
|
|
751
|
+
];
|
|
752
|
+
|
|
753
|
+
const result: any = { messages: modified };
|
|
754
|
+
if (systemPromptChanged) {
|
|
755
|
+
result.systemPrompt = systemPrompt;
|
|
756
|
+
}
|
|
757
|
+
return result;
|
|
564
758
|
});
|
|
565
759
|
|
|
566
760
|
// Register the web tool
|
|
@@ -577,7 +771,7 @@ flow [type] accomplished
|
|
|
577
771
|
"",
|
|
578
772
|
"Flow states are isolated \u03c0 processes with forked session snapshots. They run in parallel.",
|
|
579
773
|
'Invoke: { "flow": [{ "type": "scout", "intent": "..." }, ...] }',
|
|
580
|
-
"States: scout
|
|
774
|
+
"States: scout, debug, build, craft, audit, ideas.",
|
|
581
775
|
"Custom states configs in (create if not exists): .md files in .pi/agents/ or ~/.pi/agent/agents/.",
|
|
582
776
|
].join("\n"),
|
|
583
777
|
parameters: FlowParams,
|
|
@@ -587,16 +781,42 @@ flow [type] accomplished
|
|
|
587
781
|
const { flows } = discovery;
|
|
588
782
|
const makeDetails = makeFlowDetailsFactory(discovery.projectFlowsDir);
|
|
589
783
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
784
|
+
const cliFlowModelConfig =
|
|
785
|
+
typeof pi.getFlag("flow-model-config") === "string"
|
|
786
|
+
? (pi.getFlag("flow-model-config") as string)
|
|
787
|
+
: inheritedCliArgs.flowModelConfig;
|
|
788
|
+
const selectedFlowModelConfig = selectFlowModelStrategy(
|
|
789
|
+
loadedFlowModelConfigs.configs,
|
|
790
|
+
cliFlowModelConfig ?? loadedFlowModelConfigs.selectedName,
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const getTierOverride = (tier: "lite" | "flash" | "full"): string | undefined => {
|
|
794
|
+
const flagName =
|
|
795
|
+
tier === "lite"
|
|
796
|
+
? "flow-lite-model"
|
|
797
|
+
: tier === "flash"
|
|
798
|
+
? "flow-flash-model"
|
|
799
|
+
: "flow-full-model";
|
|
800
|
+
const runtimeValue = pi.getFlag(flagName);
|
|
801
|
+
if (typeof runtimeValue === "string" && runtimeValue.trim()) return runtimeValue.trim();
|
|
802
|
+
const inheritedValue = inheritedCliArgs.tieredModels?.[tier];
|
|
803
|
+
return typeof inheritedValue === "string" && inheritedValue.trim() ? inheritedValue.trim() : undefined;
|
|
595
804
|
};
|
|
596
805
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
806
|
+
const shouldFailover = (result: SingleResult): boolean => {
|
|
807
|
+
if (result.stopReason === "aborted") return false;
|
|
808
|
+
const text = `${result.errorMessage ?? ""}\n${result.stderr ?? ""}`.toLowerCase();
|
|
809
|
+
if (!text.trim()) return false;
|
|
810
|
+
if (text.includes("permission") || text.includes("invalid tool") || text.includes("bad settings")) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
return result.exitCode > 0;
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
// Build the full fork session snapshot and sanitize only non-inheritable
|
|
817
|
+
// artifacts before passing it to child flows.
|
|
818
|
+
const forkSessionSnapshotJsonl = sanitizeForkSnapshot(
|
|
819
|
+
buildForkSessionSnapshotJsonl(ctx.sessionManager),
|
|
600
820
|
);
|
|
601
821
|
|
|
602
822
|
// Collect all requested flow names
|
|
@@ -661,13 +881,22 @@ flow [type] accomplished
|
|
|
661
881
|
}
|
|
662
882
|
|
|
663
883
|
let lastStreamingText = "";
|
|
664
|
-
let
|
|
884
|
+
let lastEmittedSignature: string | undefined;
|
|
665
885
|
const emitProgress = (streamingText?: string) => {
|
|
666
886
|
if (!onUpdate) return;
|
|
667
887
|
if (streamingText !== undefined) lastStreamingText = streamingText;
|
|
668
888
|
const text = lastStreamingText || "";
|
|
669
|
-
|
|
670
|
-
|
|
889
|
+
const signature =
|
|
890
|
+
text +
|
|
891
|
+
"|" +
|
|
892
|
+
allResults
|
|
893
|
+
.map(
|
|
894
|
+
(r) =>
|
|
895
|
+
`${r.messages.length}:${r.usage.toolCalls}:${r.usage.input}:${r.usage.output}:${r.usage.contextTokens}:${r.usage.smoothedTps ?? 0}:${r.errorMessage ?? ""}`,
|
|
896
|
+
)
|
|
897
|
+
.join(";");
|
|
898
|
+
if (signature === lastEmittedSignature) return;
|
|
899
|
+
lastEmittedSignature = signature;
|
|
671
900
|
onUpdate({
|
|
672
901
|
content: [{ type: "text", text }],
|
|
673
902
|
details: makeDetails([...allResults]),
|
|
@@ -683,31 +912,61 @@ flow [type] accomplished
|
|
|
683
912
|
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
684
913
|
|
|
685
914
|
const shouldInheritContext = targetFlow?.inheritContext !== false;
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
694
|
-
parentDepth: currentDepth,
|
|
695
|
-
parentFlowStack: ancestorFlowStack,
|
|
696
|
-
maxDepth: effectiveMaxDepth,
|
|
697
|
-
preventCycles,
|
|
698
|
-
toolOptimize,
|
|
699
|
-
tieredModels,
|
|
700
|
-
signal,
|
|
701
|
-
onUpdate: (partial) => {
|
|
702
|
-
if (partial.details?.results[0]) {
|
|
703
|
-
allResults[index] = partial.details.results[0];
|
|
704
|
-
emitProgress(partial.content?.[0]?.text);
|
|
705
|
-
}
|
|
706
|
-
},
|
|
707
|
-
makeDetails,
|
|
915
|
+
const tier = getFlowTier(normalizedType);
|
|
916
|
+
const { candidates } = resolveFlowModelCandidates({
|
|
917
|
+
tier,
|
|
918
|
+
flowModel: targetFlow?.model,
|
|
919
|
+
cliTierOverride: getTierOverride(tier),
|
|
920
|
+
strategy: selectedFlowModelConfig.strategy,
|
|
921
|
+
fallbackModel: inheritedCliArgs.fallbackModel,
|
|
708
922
|
});
|
|
709
|
-
|
|
710
|
-
|
|
923
|
+
const attemptModels = candidates.length > 0 ? candidates : [undefined];
|
|
924
|
+
const attemptedModels: string[] = [];
|
|
925
|
+
let result = allResults[index];
|
|
926
|
+
|
|
927
|
+
for (let attempt = 0; attempt < attemptModels.length; attempt++) {
|
|
928
|
+
const candidateModel = attemptModels[attempt];
|
|
929
|
+
if (candidateModel) attemptedModels.push(candidateModel);
|
|
930
|
+
result = await runFlow({
|
|
931
|
+
cwd: ctx.cwd,
|
|
932
|
+
flows,
|
|
933
|
+
flowName: normalizedType,
|
|
934
|
+
intent: item.intent,
|
|
935
|
+
aim: item.aim,
|
|
936
|
+
taskCwd: item.cwd,
|
|
937
|
+
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
938
|
+
parentDepth: currentDepth,
|
|
939
|
+
parentFlowStack: ancestorFlowStack,
|
|
940
|
+
maxDepth: effectiveMaxDepth,
|
|
941
|
+
preventCycles,
|
|
942
|
+
toolOptimize,
|
|
943
|
+
model: candidateModel,
|
|
944
|
+
signal,
|
|
945
|
+
onUpdate: (partial) => {
|
|
946
|
+
if (partial.details?.results[0]) {
|
|
947
|
+
allResults[index] = partial.details.results[0];
|
|
948
|
+
emitProgress(partial.content?.[0]?.text);
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
makeDetails,
|
|
952
|
+
});
|
|
953
|
+
allResults[index] = result;
|
|
954
|
+
emitProgress();
|
|
955
|
+
if (isFlowSuccess(result) || signal?.aborted) break;
|
|
956
|
+
if (attempt < attemptModels.length - 1 && shouldFailover(result)) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (result && !isFlowSuccess(result) && attemptedModels.length > 1) {
|
|
963
|
+
const summary = `Model failover attempts: ${attemptedModels.join(" -> ")}`;
|
|
964
|
+
const baseStderr = result.stderr.trim();
|
|
965
|
+
result.stderr = baseStderr ? `${baseStderr}\n\n${summary}` : summary;
|
|
966
|
+
allResults[index] = result;
|
|
967
|
+
emitProgress();
|
|
968
|
+
}
|
|
969
|
+
|
|
711
970
|
return result;
|
|
712
971
|
});
|
|
713
972
|
|
|
@@ -50,8 +50,8 @@ export function formatCompactStats(usage: Partial<UsageStats>, model?: string, m
|
|
|
50
50
|
parts.push(`tps: ${formatTps(usage.smoothedTps)}`);
|
|
51
51
|
parts.push(`ctx: ${formatFixedTokens(usage.contextTokens || 0)}`);
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const displayModel = model ? model.replace(/^[^/]+\//, "") : undefined;
|
|
54
|
+
let result = parts.join(" · ") + (displayModel ? ` · ${displayModel}` : "");
|
|
55
55
|
if (maxWidth && visibleLength(result) > maxWidth) {
|
|
56
56
|
// Drop model first
|
|
57
57
|
let narrow = parts.join(" · ");
|