pi-agent-flow 1.1.0 → 1.2.2
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 +19 -24
- package/agents/build.md +28 -49
- package/agents/craft.md +16 -24
- package/agents/debug.md +14 -23
- package/agents/ideas.md +16 -17
- package/agents/scout.md +15 -20
- 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} +687 -121
- package/src/cli-args.ts +283 -0
- package/src/config.ts +310 -0
- package/{flow.ts → src/flow.ts} +93 -21
- package/{hooks.ts → src/hooks.ts} +6 -6
- package/{index.ts → src/index.ts} +522 -103
- package/{render-utils.ts → src/render-utils.ts} +2 -2
- package/{render.ts → src/render.ts} +54 -10
- package/src/runner-events.ts +692 -0
- package/src/structured-output.ts +97 -0
- package/{types.ts → src/types.ts} +100 -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";
|
|
@@ -16,11 +23,16 @@ import { mapFlowConcurrent, runFlow } from "./flow.js";
|
|
|
16
23
|
import {
|
|
17
24
|
type SingleResult,
|
|
18
25
|
type FlowDetails,
|
|
26
|
+
type CompressedFlowResult,
|
|
27
|
+
type FileEntry,
|
|
28
|
+
type CommandEntry,
|
|
19
29
|
emptyFlowUsage,
|
|
20
30
|
isFlowError,
|
|
21
31
|
isFlowSuccess,
|
|
32
|
+
getFlowOutput,
|
|
22
33
|
} from "./types.js";
|
|
23
|
-
import {
|
|
34
|
+
import { extractStructuredOutput } from "./structured-output.js";
|
|
35
|
+
import { createBatchTool, createBatchReadTool } from "./batch.js";
|
|
24
36
|
import {
|
|
25
37
|
createWebTool,
|
|
26
38
|
looksLikeUrlPrompt,
|
|
@@ -73,6 +85,8 @@ const FlowParams = Type.Object({
|
|
|
73
85
|
),
|
|
74
86
|
});
|
|
75
87
|
|
|
88
|
+
const inheritedCliArgs = parseFlowCliArgs(process.argv);
|
|
89
|
+
|
|
76
90
|
// ---------------------------------------------------------------------------
|
|
77
91
|
// Helpers
|
|
78
92
|
// ---------------------------------------------------------------------------
|
|
@@ -97,23 +111,8 @@ function buildForkSessionSnapshotJsonl(
|
|
|
97
111
|
if (!header || typeof header !== "object") return null;
|
|
98
112
|
|
|
99
113
|
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
114
|
const lines = [JSON.stringify(header)];
|
|
116
|
-
for (const entry of
|
|
115
|
+
for (const entry of branchEntries) lines.push(JSON.stringify(entry));
|
|
117
116
|
return `${lines.join("\n")}\n`;
|
|
118
117
|
}
|
|
119
118
|
|
|
@@ -335,46 +334,363 @@ async function confirmProjectFlowsIfNeeded(
|
|
|
335
334
|
}
|
|
336
335
|
|
|
337
336
|
// ---------------------------------------------------------------------------
|
|
338
|
-
//
|
|
337
|
+
// Sliding system prompt (short, inserted before latest user message)
|
|
338
|
+
// Always active for root flows. Appended to the static system prompt in
|
|
339
|
+
// before_agent_start, and inserted as a separate system message immediately
|
|
340
|
+
// before the latest user message by the context handler.
|
|
339
341
|
// ---------------------------------------------------------------------------
|
|
340
342
|
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
+
const SLIDING_PROMPT_OPEN_TAG = "<pi-flow-sliding-system>";
|
|
344
|
+
const SLIDING_PROMPT_CLOSE_TAG = "</pi-flow-sliding-system>";
|
|
345
|
+
|
|
346
|
+
const SLIDING_PROMPT =
|
|
347
|
+
`${SLIDING_PROMPT_OPEN_TAG}\n` +
|
|
348
|
+
`You are operating with pi-agent-flow routing.\n` +
|
|
349
|
+
`If the answer is already in context, answer directly; otherwise delegate to the appropriate flow.\n` +
|
|
350
|
+
`${SLIDING_PROMPT_CLOSE_TAG}`;
|
|
351
|
+
|
|
352
|
+
const SLIDING_PROMPT_RE = new RegExp(
|
|
353
|
+
SLIDING_PROMPT_OPEN_TAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +
|
|
354
|
+
"[\\s\\S]*?" +
|
|
355
|
+
SLIDING_PROMPT_CLOSE_TAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
356
|
+
"g",
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
/** Strip any old sliding system prompt tags from a string. */
|
|
360
|
+
function stripSlidingPromptText(text: string): string {
|
|
361
|
+
return text.replace(SLIDING_PROMPT_RE, "");
|
|
362
|
+
}
|
|
343
363
|
|
|
344
|
-
|
|
364
|
+
/** Strip sliding prompt tags from content (string or text-part array). */
|
|
365
|
+
function stripSlidingPromptFromContent(
|
|
345
366
|
content: string | { type: string; text?: string }[],
|
|
346
367
|
): string | { type: string; text?: string }[] {
|
|
347
368
|
if (typeof content === "string") {
|
|
348
|
-
return content
|
|
369
|
+
return stripSlidingPromptText(content);
|
|
349
370
|
}
|
|
350
371
|
return content.map((c) => {
|
|
351
372
|
if (c.type === "text" && typeof c.text === "string") {
|
|
352
|
-
return { ...c, text: c.text
|
|
373
|
+
return { ...c, text: stripSlidingPromptText(c.text) };
|
|
353
374
|
}
|
|
354
375
|
return c;
|
|
355
376
|
});
|
|
356
377
|
}
|
|
357
378
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
): string | { type: string; text?: string }[] {
|
|
379
|
+
/** Check whether content (string or text-part array) contains the sliding tag. */
|
|
380
|
+
function contentContainsSlidingTag(content: any): boolean {
|
|
361
381
|
if (typeof content === "string") {
|
|
362
|
-
return content
|
|
382
|
+
return content.includes(SLIDING_PROMPT_OPEN_TAG);
|
|
383
|
+
}
|
|
384
|
+
if (Array.isArray(content)) {
|
|
385
|
+
return content.some(
|
|
386
|
+
(part: any) =>
|
|
387
|
+
part.type === "text" &&
|
|
388
|
+
typeof part.text === "string" &&
|
|
389
|
+
part.text.includes(SLIDING_PROMPT_OPEN_TAG),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Remove any existing sliding-system-prompt system messages and strip tags from user messages.
|
|
396
|
+
* Returns the sanitized messages and a flag indicating whether anything changed.
|
|
397
|
+
*/
|
|
398
|
+
function stripSlidingPromptsFromMessages(messages: any[]): { messages: any[]; changed: boolean } {
|
|
399
|
+
let changed = false;
|
|
400
|
+
const result = messages
|
|
401
|
+
.filter((msg) => {
|
|
402
|
+
// Remove dedicated sliding system prompt messages
|
|
403
|
+
if (msg.role === "system" && contentContainsSlidingTag(msg.content)) {
|
|
404
|
+
changed = true;
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
})
|
|
409
|
+
.map((msg) => {
|
|
410
|
+
// Also strip stray tags embedded in user/assistant messages
|
|
411
|
+
if (!("content" in msg)) return msg;
|
|
412
|
+
const stripped = stripSlidingPromptFromContent(msg.content);
|
|
413
|
+
if (isJsonEqual(stripped, msg.content)) return msg;
|
|
414
|
+
changed = true;
|
|
415
|
+
return { ...msg, content: stripped };
|
|
416
|
+
});
|
|
417
|
+
return { messages: result, changed };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Build a system message containing the sliding prompt. */
|
|
421
|
+
function makeSlidingPromptMessage(referenceMessage?: any): any {
|
|
422
|
+
return {
|
|
423
|
+
role: "system",
|
|
424
|
+
content: SLIDING_PROMPT,
|
|
425
|
+
timestamp: referenceMessage?.timestamp,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const REASONING_PART_TYPES = new Set([
|
|
430
|
+
"thinking",
|
|
431
|
+
"reasoning",
|
|
432
|
+
"reasoning_content",
|
|
433
|
+
"reasoningContent",
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
const REASONING_FIELDS = [
|
|
437
|
+
"thinking",
|
|
438
|
+
"thinkingSignature",
|
|
439
|
+
"thinking_signature",
|
|
440
|
+
"reasoning",
|
|
441
|
+
"reasoningContent",
|
|
442
|
+
"reasoning_content",
|
|
443
|
+
"reasoningSignature",
|
|
444
|
+
"reasoning_signature",
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
function stripReasoningFromAssistantMessage(message: any): {
|
|
448
|
+
message: any;
|
|
449
|
+
changed: boolean;
|
|
450
|
+
} {
|
|
451
|
+
let next = message;
|
|
452
|
+
let changed = false;
|
|
453
|
+
|
|
454
|
+
for (const field of REASONING_FIELDS) {
|
|
455
|
+
if (field in next) {
|
|
456
|
+
if (next === message) next = { ...message };
|
|
457
|
+
delete next[field];
|
|
458
|
+
changed = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (Array.isArray(message.content)) {
|
|
463
|
+
const filteredContent = message.content.filter(
|
|
464
|
+
(part: any) => !REASONING_PART_TYPES.has(part?.type),
|
|
465
|
+
);
|
|
466
|
+
if (filteredContent.length !== message.content.length) {
|
|
467
|
+
if (next === message) next = { ...message };
|
|
468
|
+
next.content = filteredContent;
|
|
469
|
+
changed = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { message: next, changed };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isJsonEqual(a: any, b: any): boolean {
|
|
477
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
478
|
+
}
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
// Flow result compression
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Module-level cache populated after each flow tool execution.
|
|
485
|
+
* Maps toolCallId → compressed flow result.
|
|
486
|
+
* Consumed by compressFlowToolResults at fork time.
|
|
487
|
+
*/
|
|
488
|
+
const flowResultCache = new Map<string, CompressedFlowResult[]>();
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Build compressed representations of flow results and cache them by toolCallId.
|
|
492
|
+
*/
|
|
493
|
+
function cacheFlowResults(toolCallId: string, results: SingleResult[]): void {
|
|
494
|
+
for (const result of results) {
|
|
495
|
+
const so = result.structuredOutput;
|
|
496
|
+
if (!so) continue;
|
|
497
|
+
const compressed: CompressedFlowResult = {
|
|
498
|
+
type: result.type,
|
|
499
|
+
status: isFlowError(result) ? "failed" : "accomplished",
|
|
500
|
+
};
|
|
501
|
+
if (so.files.length > 0) compressed.files = so.files;
|
|
502
|
+
if (so.commands.length > 0) compressed.commands = so.commands;
|
|
503
|
+
if (result.errorMessage) compressed.error = result.errorMessage;
|
|
504
|
+
const existing = flowResultCache.get(toolCallId) ?? [];
|
|
505
|
+
existing.push(compressed);
|
|
506
|
+
flowResultCache.set(toolCallId, existing);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Render a compressed flow result as compact text for child context.
|
|
512
|
+
*/
|
|
513
|
+
function renderCompressedFlowResult(r: CompressedFlowResult): string {
|
|
514
|
+
const parts: string[] = [`[Flow: ${r.type} ${r.status}]`];
|
|
515
|
+
if (r.files?.length) {
|
|
516
|
+
const fileLines = r.files.map((f) => {
|
|
517
|
+
const role = f.role ? ` (${f.role})` : "";
|
|
518
|
+
const desc = f.description ? ` — ${f.description}` : "";
|
|
519
|
+
return ` ${f.path}${role}${desc}`;
|
|
520
|
+
});
|
|
521
|
+
parts.push(`Files:\n${fileLines.join("\n")}`);
|
|
522
|
+
}
|
|
523
|
+
if (r.commands?.length) {
|
|
524
|
+
const cmdLines = r.commands.map((c) => {
|
|
525
|
+
const result = c.result ? ` (${c.result})` : "";
|
|
526
|
+
const purpose = c.purpose ? ` — ${c.purpose}` : "";
|
|
527
|
+
return ` ${c.tool ?? "cmd"}: ${c.command}${result}${purpose}`;
|
|
528
|
+
});
|
|
529
|
+
parts.push(`Commands:\n${cmdLines.join("\n")}`);
|
|
530
|
+
}
|
|
531
|
+
if (r.error) parts.push(`Error: ${r.error}`);
|
|
532
|
+
return parts.join("\n");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Compress flow tool results in a sanitized session snapshot.
|
|
537
|
+
*
|
|
538
|
+
* Scans for tool result messages that correspond to flow invocations
|
|
539
|
+
* and replaces their content with compact compressed output.
|
|
540
|
+
*/
|
|
541
|
+
export function compressFlowToolResults(snapshot: string, cache: Map<string, CompressedFlowResult[]>): string {
|
|
542
|
+
if (cache.size === 0) return snapshot;
|
|
543
|
+
|
|
544
|
+
const lines = snapshot.trimEnd().split("\n");
|
|
545
|
+
const result: string[] = [];
|
|
546
|
+
|
|
547
|
+
// First pass: map toolCallId → tool name from assistant messages
|
|
548
|
+
const toolCallIdToName = new Map<string, string>();
|
|
549
|
+
for (const line of lines) {
|
|
550
|
+
let entry: any;
|
|
551
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
552
|
+
if (entry?.type !== "message" || entry.message?.role !== "assistant") continue;
|
|
553
|
+
const content = entry.message.content;
|
|
554
|
+
if (!Array.isArray(content)) continue;
|
|
555
|
+
for (const part of content) {
|
|
556
|
+
if (part.type === "toolCall" && part.toolCallId && part.name) {
|
|
557
|
+
toolCallIdToName.set(part.toolCallId, part.name);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Second pass: compress flow tool results
|
|
563
|
+
for (const line of lines) {
|
|
564
|
+
let entry: any;
|
|
565
|
+
try { entry = JSON.parse(line); } catch { result.push(line); continue; }
|
|
566
|
+
|
|
567
|
+
if (entry?.type !== "message" || entry.message?.role !== "tool") {
|
|
568
|
+
result.push(line);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Extract toolCallId — either from message-level or content-level toolResult
|
|
573
|
+
let toolCallId: string | undefined;
|
|
574
|
+
if (typeof entry.message.toolCallId === "string") {
|
|
575
|
+
toolCallId = entry.message.toolCallId;
|
|
576
|
+
} else if (Array.isArray(entry.message.content)) {
|
|
577
|
+
for (const part of entry.message.content) {
|
|
578
|
+
if (part.type === "toolResult" && part.toolCallId) {
|
|
579
|
+
toolCallId = part.toolCallId;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!toolCallId) { result.push(line); continue; }
|
|
586
|
+
|
|
587
|
+
const toolName = toolCallIdToName.get(toolCallId);
|
|
588
|
+
if (toolName !== "flow") { result.push(line); continue; }
|
|
589
|
+
|
|
590
|
+
const compressed = cache.get(toolCallId);
|
|
591
|
+
if (!compressed || compressed.length === 0) { result.push(line); continue; }
|
|
592
|
+
|
|
593
|
+
const rendered = compressed.map(renderCompressedFlowResult).join("\n\n");
|
|
594
|
+
|
|
595
|
+
// Replace content in the tool result message
|
|
596
|
+
if (typeof entry.message.toolCallId === "string") {
|
|
597
|
+
// Format 1: toolCallId at message level, content is text array
|
|
598
|
+
entry = {
|
|
599
|
+
...entry,
|
|
600
|
+
message: {
|
|
601
|
+
...entry.message,
|
|
602
|
+
content: [{ type: "text", text: rendered }],
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
} else {
|
|
606
|
+
// Format 2: toolCallId inside content array
|
|
607
|
+
entry = {
|
|
608
|
+
...entry,
|
|
609
|
+
message: {
|
|
610
|
+
...entry.message,
|
|
611
|
+
content: entry.message.content.map((part: any) =>
|
|
612
|
+
part.type === "toolResult" && part.toolCallId === toolCallId
|
|
613
|
+
? { ...part, content: rendered }
|
|
614
|
+
: part,
|
|
615
|
+
),
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
result.push(JSON.stringify(entry));
|
|
363
621
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
622
|
+
|
|
623
|
+
return `${result.join("\n")}\n`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Sanitize a fork session snapshot JSONL to remove only non-inheritable
|
|
628
|
+
* artifacts before passing full parent context to child flows: sliding system
|
|
629
|
+
* prompts, legacy reminders, and assistant reasoning/thinking.
|
|
630
|
+
*/
|
|
631
|
+
function sanitizeForkSnapshot(snapshot: string | null): string | null {
|
|
632
|
+
if (!snapshot) return snapshot;
|
|
633
|
+
|
|
634
|
+
const lines = snapshot.trimEnd().split("\n");
|
|
635
|
+
const sanitizedLines: string[] = [];
|
|
636
|
+
|
|
637
|
+
for (const line of lines) {
|
|
638
|
+
let entry: any;
|
|
639
|
+
try {
|
|
640
|
+
entry = JSON.parse(line);
|
|
641
|
+
} catch {
|
|
642
|
+
sanitizedLines.push(line);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let changed = false;
|
|
647
|
+
|
|
648
|
+
// Drop sliding system prompt messages entirely.
|
|
649
|
+
if (
|
|
650
|
+
entry?.type === "message" &&
|
|
651
|
+
entry.message?.role === "system" &&
|
|
652
|
+
contentContainsSlidingTag(entry.message?.content)
|
|
653
|
+
) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (entry?.type === "message" && entry.message) {
|
|
658
|
+
let message = entry.message;
|
|
659
|
+
|
|
660
|
+
if (message.role === "assistant") {
|
|
661
|
+
const stripped = stripReasoningFromAssistantMessage(message);
|
|
662
|
+
message = stripped.message;
|
|
663
|
+
changed ||= stripped.changed;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if ("content" in message) {
|
|
667
|
+
const originalContent = message.content;
|
|
668
|
+
const strippedContent = stripSlidingPromptFromContent(originalContent);
|
|
669
|
+
|
|
670
|
+
if (!isJsonEqual(strippedContent, originalContent)) {
|
|
671
|
+
message = {
|
|
672
|
+
...message,
|
|
673
|
+
content: strippedContent,
|
|
674
|
+
};
|
|
675
|
+
changed = true;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (changed) {
|
|
680
|
+
entry = { ...entry, message };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
sanitizedLines.push(changed ? JSON.stringify(entry) : line);
|
|
371
685
|
}
|
|
372
|
-
|
|
686
|
+
|
|
687
|
+
const sanitized = `${sanitizedLines.join("\n")}\n`;
|
|
688
|
+
return compressFlowToolResults(sanitized, flowResultCache);
|
|
373
689
|
}
|
|
374
690
|
|
|
375
691
|
function computeActiveTools(optimize: boolean): string[] {
|
|
376
692
|
return optimize
|
|
377
|
-
? ["
|
|
693
|
+
? ["batch_read", "bash", "flow"]
|
|
378
694
|
: ["read", "write", "edit", "batch", "bash", "flow", "web"];
|
|
379
695
|
}
|
|
380
696
|
|
|
@@ -391,6 +707,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
391
707
|
description: "Block delegating to flows already in the current delegation stack (default: true).",
|
|
392
708
|
type: "boolean",
|
|
393
709
|
});
|
|
710
|
+
pi.registerFlag("flow-model-config", {
|
|
711
|
+
description: "Named flow model strategy from settings.json flowModelConfigs.",
|
|
712
|
+
type: "string",
|
|
713
|
+
});
|
|
394
714
|
pi.registerFlag("flow-lite-model", {
|
|
395
715
|
description: "Model for lite-tier flows (scout, debug).",
|
|
396
716
|
type: "string",
|
|
@@ -414,6 +734,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
414
734
|
|
|
415
735
|
// toolOptimize: CLI flag > env var > settings.json > default (true)
|
|
416
736
|
let toolOptimize = true;
|
|
737
|
+
// structuredOutput: settings.json > default (true)
|
|
738
|
+
let structuredOutput = true;
|
|
417
739
|
const envToolOptimize = process.env[FLOW_TOOL_OPTIMIZE_ENV];
|
|
418
740
|
if (envToolOptimize !== undefined) {
|
|
419
741
|
const parsed = parseBoolean(envToolOptimize);
|
|
@@ -421,13 +743,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
743
|
}
|
|
422
744
|
|
|
423
745
|
let discoveredFlows: FlowConfig[] = [];
|
|
424
|
-
let
|
|
746
|
+
let loadedFlowModelConfigs: LoadedFlowModelConfigs = {
|
|
747
|
+
selectedName: "default",
|
|
748
|
+
configs: { default: {} },
|
|
749
|
+
strategy: {},
|
|
750
|
+
};
|
|
425
751
|
|
|
426
752
|
// Auto-discover flows on session start
|
|
427
753
|
pi.on("session_start", async (_event, ctx) => {
|
|
428
754
|
const discovery = discoverFlows(ctx.cwd, "all");
|
|
429
755
|
discoveredFlows = discovery.flows;
|
|
430
|
-
|
|
756
|
+
loadedFlowModelConfigs = loadFlowModelConfigs(ctx.cwd);
|
|
431
757
|
|
|
432
758
|
// Resolve toolOptimize: CLI flag > env var > settings.json > default
|
|
433
759
|
const cliFlag = pi.getFlag("tool-optimize");
|
|
@@ -441,6 +767,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
441
767
|
if (typeof flowSettings.toolOptimize === "boolean") {
|
|
442
768
|
toolOptimize = flowSettings.toolOptimize;
|
|
443
769
|
}
|
|
770
|
+
if (typeof flowSettings.structuredOutput === "boolean") {
|
|
771
|
+
structuredOutput = flowSettings.structuredOutput;
|
|
772
|
+
}
|
|
444
773
|
}
|
|
445
774
|
|
|
446
775
|
// Only restrict tools for the main orchestrator (depth 0).
|
|
@@ -450,8 +779,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
450
779
|
pi.setActiveTools(computeActiveTools(toolOptimize));
|
|
451
780
|
}
|
|
452
781
|
|
|
453
|
-
// Register batch so
|
|
782
|
+
// Register batch and batch_read so they are available for main agent and child flows.
|
|
454
783
|
if (toolOptimize) {
|
|
784
|
+
pi.registerTool(createBatchReadTool());
|
|
455
785
|
pi.registerTool(createBatchTool());
|
|
456
786
|
}
|
|
457
787
|
});
|
|
@@ -486,14 +816,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
486
816
|
}
|
|
487
817
|
|
|
488
818
|
let systemPrompt = event.systemPrompt;
|
|
489
|
-
if (webInstructions.length > 0) {
|
|
819
|
+
if (!toolOptimize && webInstructions.length > 0) {
|
|
490
820
|
systemPrompt +=
|
|
491
821
|
"\n\n## pi-web steering\n" +
|
|
492
822
|
webInstructions.map((line) => `- ${line}`).join("\n");
|
|
493
823
|
}
|
|
494
824
|
|
|
825
|
+
// Append sliding prompt to static system prompt unconditionally.
|
|
826
|
+
systemPrompt += "\n\n" + SLIDING_PROMPT;
|
|
827
|
+
|
|
495
828
|
if (!canDelegate || discoveredFlows.length === 0) {
|
|
496
|
-
return
|
|
829
|
+
return { systemPrompt };
|
|
497
830
|
}
|
|
498
831
|
|
|
499
832
|
return {
|
|
@@ -507,8 +840,8 @@ Before acting, reason about whether to dive into a flow:
|
|
|
507
840
|
- [debug] — when something is broken. Investigate logs, errors, stack traces to find root cause.
|
|
508
841
|
- [build] — when you are ready to build. Implement features, fix bugs, write tests.
|
|
509
842
|
- [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.
|
|
843
|
+
- [audit] — when you need to verify and remediate. Audit security, quality, and correctness; fix safe issues directly.
|
|
844
|
+
- [ideas] — when you need fresh ideas. Use inherited context as background while exploring alternatives.
|
|
512
845
|
|
|
513
846
|
Multiple independent flows? Batch them into one call:
|
|
514
847
|
|
|
@@ -518,16 +851,16 @@ Multiple independent flows? Batch them into one call:
|
|
|
518
851
|
Each call renders as:
|
|
519
852
|
|
|
520
853
|
• flow [scout] — Map the full directory structure...
|
|
521
|
-
• flow [audit] — Audit security and quality...
|
|
854
|
+
• flow [audit] — Audit security and quality, then fix safe issues...
|
|
522
855
|
|
|
523
856
|
Each flow returns:
|
|
524
857
|
|
|
525
858
|
flow [type] accomplished
|
|
526
859
|
|
|
527
|
-
[Summary] — what happened
|
|
528
|
-
[Done] — completed with file:line references
|
|
529
|
-
[Not Done] — incomplete items and reasons
|
|
530
|
-
[Next Steps] — recommended follow-up
|
|
860
|
+
[Summary] — what happened and current status
|
|
861
|
+
[Done] — completed work with file:line references and verification results
|
|
862
|
+
[Not Done] — incomplete items, skipped checks, blockers, and reasons
|
|
863
|
+
[Next Steps] — specific recommended follow-up or next flow
|
|
531
864
|
|
|
532
865
|
### Guards
|
|
533
866
|
- Depth: ${currentDepth}/${maxDepth} | Cycles: ${preventCycles ? "blocked" : "off"} | Stack: ${ancestorFlowStack.length > 0 ? ancestorFlowStack.join(" -> ") : "(root)"}
|
|
@@ -535,32 +868,50 @@ flow [type] accomplished
|
|
|
535
868
|
};
|
|
536
869
|
});
|
|
537
870
|
|
|
538
|
-
// Sliding
|
|
539
|
-
//
|
|
540
|
-
//
|
|
871
|
+
// Sliding system prompt: insert as a separate system message immediately
|
|
872
|
+
// before the latest user message each turn. Strips from the static
|
|
873
|
+
// systemPrompt to avoid duplication, then inserts separately.
|
|
874
|
+
// Skipped for child flows (depth > 0) — they have explicit <mission> directives.
|
|
541
875
|
pi.on("context", async (event) => {
|
|
542
876
|
if (currentDepth > 0) return undefined;
|
|
543
877
|
|
|
544
|
-
|
|
878
|
+
// Always strip old sliding prompt messages to prevent accumulation
|
|
879
|
+
const { messages, changed: messagesChanged } = stripSlidingPromptsFromMessages(event.messages);
|
|
880
|
+
|
|
881
|
+
// Find latest user message
|
|
545
882
|
const userIndices = messages
|
|
546
883
|
.map((m: any, i: number) => (m.role === "user" ? i : -1))
|
|
547
884
|
.filter((i: number) => i !== -1);
|
|
548
885
|
|
|
549
|
-
if (userIndices.length === 0)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const modified = messages.map((msg: any, idx: number) => {
|
|
554
|
-
if (msg.role !== "user") return msg;
|
|
886
|
+
if (userIndices.length === 0) {
|
|
887
|
+
// No user message yet: keep sliding prompt in the static system prompt only.
|
|
888
|
+
return messagesChanged ? { messages } : undefined;
|
|
889
|
+
}
|
|
555
890
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
891
|
+
// Strip sliding from the static systemPrompt so it only appears once,
|
|
892
|
+
// as a separate message right before the latest user message.
|
|
893
|
+
let systemPrompt = event.systemPrompt;
|
|
894
|
+
let systemPromptChanged = false;
|
|
895
|
+
if (typeof systemPrompt === "string") {
|
|
896
|
+
const stripped = stripSlidingPromptText(systemPrompt);
|
|
897
|
+
if (stripped !== systemPrompt) {
|
|
898
|
+
systemPrompt = stripped;
|
|
899
|
+
systemPromptChanged = true;
|
|
559
900
|
}
|
|
560
|
-
|
|
561
|
-
});
|
|
901
|
+
}
|
|
562
902
|
|
|
563
|
-
|
|
903
|
+
const lastUserIndex = userIndices[userIndices.length - 1];
|
|
904
|
+
const modified = [
|
|
905
|
+
...messages.slice(0, lastUserIndex),
|
|
906
|
+
makeSlidingPromptMessage(messages[lastUserIndex]),
|
|
907
|
+
...messages.slice(lastUserIndex),
|
|
908
|
+
];
|
|
909
|
+
|
|
910
|
+
const result: any = { messages: modified };
|
|
911
|
+
if (systemPromptChanged) {
|
|
912
|
+
result.systemPrompt = systemPrompt;
|
|
913
|
+
}
|
|
914
|
+
return result;
|
|
564
915
|
});
|
|
565
916
|
|
|
566
917
|
// Register the web tool
|
|
@@ -577,26 +928,53 @@ flow [type] accomplished
|
|
|
577
928
|
"",
|
|
578
929
|
"Flow states are isolated \u03c0 processes with forked session snapshots. They run in parallel.",
|
|
579
930
|
'Invoke: { "flow": [{ "type": "scout", "intent": "..." }, ...] }',
|
|
580
|
-
"States: scout
|
|
931
|
+
"States: scout, debug, build, craft, audit, ideas.",
|
|
581
932
|
"Custom states configs in (create if not exists): .md files in .pi/agents/ or ~/.pi/agent/agents/.",
|
|
582
933
|
].join("\n"),
|
|
583
934
|
parameters: FlowParams,
|
|
584
935
|
|
|
585
|
-
async execute(
|
|
936
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
586
937
|
const discovery = discoverFlows(ctx.cwd, "all");
|
|
938
|
+
flowResultCache.clear();
|
|
587
939
|
const { flows } = discovery;
|
|
588
940
|
const makeDetails = makeFlowDetailsFactory(discovery.projectFlowsDir);
|
|
589
941
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
942
|
+
const cliFlowModelConfig =
|
|
943
|
+
typeof pi.getFlag("flow-model-config") === "string"
|
|
944
|
+
? (pi.getFlag("flow-model-config") as string)
|
|
945
|
+
: inheritedCliArgs.flowModelConfig;
|
|
946
|
+
const selectedFlowModelConfig = selectFlowModelStrategy(
|
|
947
|
+
loadedFlowModelConfigs.configs,
|
|
948
|
+
cliFlowModelConfig ?? loadedFlowModelConfigs.selectedName,
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
const getTierOverride = (tier: "lite" | "flash" | "full"): string | undefined => {
|
|
952
|
+
const flagName =
|
|
953
|
+
tier === "lite"
|
|
954
|
+
? "flow-lite-model"
|
|
955
|
+
: tier === "flash"
|
|
956
|
+
? "flow-flash-model"
|
|
957
|
+
: "flow-full-model";
|
|
958
|
+
const runtimeValue = pi.getFlag(flagName);
|
|
959
|
+
if (typeof runtimeValue === "string" && runtimeValue.trim()) return runtimeValue.trim();
|
|
960
|
+
const inheritedValue = inheritedCliArgs.tieredModels?.[tier];
|
|
961
|
+
return typeof inheritedValue === "string" && inheritedValue.trim() ? inheritedValue.trim() : undefined;
|
|
595
962
|
};
|
|
596
963
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
964
|
+
const shouldFailover = (result: SingleResult): boolean => {
|
|
965
|
+
if (result.stopReason === "aborted") return false;
|
|
966
|
+
const text = `${result.errorMessage ?? ""}\n${result.stderr ?? ""}`.toLowerCase();
|
|
967
|
+
if (!text.trim()) return false;
|
|
968
|
+
if (text.includes("permission") || text.includes("invalid tool") || text.includes("bad settings")) {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
return result.exitCode > 0;
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// Build the full fork session snapshot and sanitize only non-inheritable
|
|
975
|
+
// artifacts before passing it to child flows.
|
|
976
|
+
const forkSessionSnapshotJsonl = sanitizeForkSnapshot(
|
|
977
|
+
buildForkSessionSnapshotJsonl(ctx.sessionManager),
|
|
600
978
|
);
|
|
601
979
|
|
|
602
980
|
// Collect all requested flow names
|
|
@@ -661,13 +1039,22 @@ flow [type] accomplished
|
|
|
661
1039
|
}
|
|
662
1040
|
|
|
663
1041
|
let lastStreamingText = "";
|
|
664
|
-
let
|
|
1042
|
+
let lastEmittedSignature: string | undefined;
|
|
665
1043
|
const emitProgress = (streamingText?: string) => {
|
|
666
1044
|
if (!onUpdate) return;
|
|
667
1045
|
if (streamingText !== undefined) lastStreamingText = streamingText;
|
|
668
1046
|
const text = lastStreamingText || "";
|
|
669
|
-
|
|
670
|
-
|
|
1047
|
+
const signature =
|
|
1048
|
+
text +
|
|
1049
|
+
"|" +
|
|
1050
|
+
allResults
|
|
1051
|
+
.map(
|
|
1052
|
+
(r) =>
|
|
1053
|
+
`${r.messages.length}:${r.usage.toolCalls}:${r.usage.input}:${r.usage.output}:${r.usage.contextTokens}:${r.usage.smoothedTps ?? 0}:${r.errorMessage ?? ""}`,
|
|
1054
|
+
)
|
|
1055
|
+
.join(";");
|
|
1056
|
+
if (signature === lastEmittedSignature) return;
|
|
1057
|
+
lastEmittedSignature = signature;
|
|
671
1058
|
onUpdate({
|
|
672
1059
|
content: [{ type: "text", text }],
|
|
673
1060
|
details: makeDetails([...allResults]),
|
|
@@ -683,34 +1070,66 @@ flow [type] accomplished
|
|
|
683
1070
|
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
684
1071
|
|
|
685
1072
|
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,
|
|
1073
|
+
const tier = getFlowTier(normalizedType);
|
|
1074
|
+
const { candidates } = resolveFlowModelCandidates({
|
|
1075
|
+
tier,
|
|
1076
|
+
flowModel: targetFlow?.model,
|
|
1077
|
+
cliTierOverride: getTierOverride(tier),
|
|
1078
|
+
strategy: selectedFlowModelConfig.strategy,
|
|
1079
|
+
fallbackModel: inheritedCliArgs.fallbackModel,
|
|
708
1080
|
});
|
|
709
|
-
|
|
710
|
-
|
|
1081
|
+
const attemptModels = candidates.length > 0 ? candidates : [undefined];
|
|
1082
|
+
const attemptedModels: string[] = [];
|
|
1083
|
+
let result = allResults[index];
|
|
1084
|
+
|
|
1085
|
+
for (let attempt = 0; attempt < attemptModels.length; attempt++) {
|
|
1086
|
+
const candidateModel = attemptModels[attempt];
|
|
1087
|
+
if (candidateModel) attemptedModels.push(candidateModel);
|
|
1088
|
+
result = await runFlow({
|
|
1089
|
+
cwd: ctx.cwd,
|
|
1090
|
+
flows,
|
|
1091
|
+
flowName: normalizedType,
|
|
1092
|
+
intent: item.intent,
|
|
1093
|
+
aim: item.aim,
|
|
1094
|
+
taskCwd: item.cwd,
|
|
1095
|
+
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
1096
|
+
parentDepth: currentDepth,
|
|
1097
|
+
parentFlowStack: ancestorFlowStack,
|
|
1098
|
+
maxDepth: effectiveMaxDepth,
|
|
1099
|
+
preventCycles,
|
|
1100
|
+
toolOptimize,
|
|
1101
|
+
structuredOutput,
|
|
1102
|
+
model: candidateModel,
|
|
1103
|
+
signal,
|
|
1104
|
+
onUpdate: (partial) => {
|
|
1105
|
+
if (partial.details?.results[0]) {
|
|
1106
|
+
allResults[index] = partial.details.results[0];
|
|
1107
|
+
emitProgress(partial.content?.[0]?.text);
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
makeDetails,
|
|
1111
|
+
});
|
|
1112
|
+
allResults[index] = result;
|
|
1113
|
+
emitProgress();
|
|
1114
|
+
if (isFlowSuccess(result) || signal?.aborted) break;
|
|
1115
|
+
if (attempt < attemptModels.length - 1 && shouldFailover(result)) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (result && !isFlowSuccess(result) && attemptedModels.length > 1) {
|
|
1122
|
+
const summary = `Model failover attempts: ${attemptedModels.join(" -> ")}`;
|
|
1123
|
+
const baseStderr = result.stderr.trim();
|
|
1124
|
+
result.stderr = baseStderr ? `${baseStderr}\n\n${summary}` : summary;
|
|
1125
|
+
allResults[index] = result;
|
|
1126
|
+
emitProgress();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
711
1129
|
return result;
|
|
712
1130
|
});
|
|
713
1131
|
|
|
1132
|
+
cacheFlowResults(toolCallId, results);
|
|
714
1133
|
// Build tool result with FULL flow output — no truncation
|
|
715
1134
|
const successCount = results.filter((r) => isFlowSuccess(r)).length;
|
|
716
1135
|
const flowReports = results.map((r) => {
|