pi-agent-flow 1.4.3 → 1.5.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 +66 -4
- package/agents/build.md +4 -2
- package/agents/debug.md +4 -1
- package/package.json +1 -1
- package/src/agents.ts +12 -0
- package/src/cli-args.ts +21 -0
- package/src/config.ts +78 -7
- package/src/executor.ts +38 -11
- package/src/flow.ts +35 -16
- package/src/index.ts +112 -19
- package/src/render-utils.ts +26 -12
- package/src/render.ts +49 -29
- package/src/session-mode.ts +32 -0
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ pi install .
|
|
|
47
47
|
- **Parallel execution** — batch independent flows into one call with bounded concurrency
|
|
48
48
|
- **Structured reports** — every flow returns `[Summary]`, `[Done]`, `[Not Done]`, `[Next Steps]`
|
|
49
49
|
- **Depth guards** — configurable max delegation depth (default: `3`)
|
|
50
|
+
- **Session timeout modes** — child flows use controlled budgets: `fast` (300s), `default` (600s), or `long` (900s)
|
|
50
51
|
- **Cycle prevention** — blocks re-entering flows already in the ancestor stack
|
|
51
52
|
- **Model tiering & failover** — flows map to `lite` / `flash` / `full` tiers with primary + failover model chains
|
|
52
53
|
- **Unified batch tools** — `batch` (read/write/edit/delete) and `batch_read` replace separate file tools for cross-cutting work
|
|
@@ -80,8 +81,8 @@ The result is faster, cheaper, and cleaner delegation: the main agent remains un
|
|
|
80
81
|
| Flow | Purpose | Tools | Tier |
|
|
81
82
|
|------|---------|-------|------|
|
|
82
83
|
| `[scout]` | Discover files, trace code paths, map architecture | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
|
|
83
|
-
| `[debug]` | Investigate logs, errors, stack traces, root causes | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
|
|
84
|
-
| `[build]` | Implement features, fix bugs, write tests, ship | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
|
|
84
|
+
| `[debug]` | Investigate logs, errors, stack traces, root causes, and update relevant troubleshooting docs | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
|
|
85
|
+
| `[build]` | Implement features, fix bugs, write tests, update relevant docs, ship | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
|
|
85
86
|
| `[craft]` | Plan structure, break down requirements, design solutions | `batch`, `bash`, `find`, `grep`, `ls` | `full` |
|
|
86
87
|
| `[audit]` | Audit security, quality, correctness; fix issues autonomously | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
|
|
87
88
|
| `[ideas]` | Generate ideas and explore possibilities with inherited context | `batch`, `bash` | `full` |
|
|
@@ -90,6 +91,35 @@ The result is faster, cheaper, and cleaner delegation: the main agent remains un
|
|
|
90
91
|
|
|
91
92
|
> **Clean slate:** Set `inheritContext: false` in a custom flow's front-matter so it receives only the intent, ideal for unbiased creative work.
|
|
92
93
|
|
|
94
|
+
> **Docs hygiene:** Bundled `build` and `debug` flows are instructed to update relevant documentation after their work when the findings or implementation change developer or operational knowledge. If no docs apply, they should state why in the final report.
|
|
95
|
+
|
|
96
|
+
### Session modes
|
|
97
|
+
|
|
98
|
+
Each flow call may set `sessionMode` to choose the child-agent time budget:
|
|
99
|
+
|
|
100
|
+
| Mode | Budget | Recommended use |
|
|
101
|
+
|------|-------:|-----------------|
|
|
102
|
+
| `fast` | 300s | quick scouting, narrow checks, small design passes |
|
|
103
|
+
| `default` | 600s | normal flow work; this is the default |
|
|
104
|
+
| `long` | 900s | large builds, full test runs, broad refactors, complex debugging |
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"flow": [
|
|
111
|
+
{
|
|
112
|
+
"type": "build",
|
|
113
|
+
"intent": "Run the full test suite and fix failures",
|
|
114
|
+
"aim": "Fix failing tests",
|
|
115
|
+
"sessionMode": "long"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The public interface is mode-based; arbitrary per-flow numeric timeouts are not exposed.
|
|
122
|
+
|
|
93
123
|
---
|
|
94
124
|
|
|
95
125
|
## Flow Definitions
|
|
@@ -259,6 +289,36 @@ Use `flowModelConfigs` in your Pi settings to define tiered model strategies. Ea
|
|
|
259
289
|
|
|
260
290
|
Settings are merged: project `.pi/settings.json` overrides global `~/.pi/agent/settings.json`.
|
|
261
291
|
|
|
292
|
+
Switch the global active strategy quickly with `--flow-mode`:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
pi --flow-mode balance
|
|
296
|
+
pi --flow-mode quality
|
|
297
|
+
pi --flow-mode mimo
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`--flow-mode` updates `flowModelConfig` in global `~/.pi/agent/settings.json` (or `$PI_CODING_AGENT_DIR/settings.json`) and applies the mode immediately for the current invocation. The mode must already exist in the merged `flowModelConfigs`; project `.pi/settings.json` can still override global settings on later no-flag runs.
|
|
301
|
+
|
|
302
|
+
You can also set flow runtime defaults under `flowSettings`:
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{
|
|
306
|
+
"flowSettings": {
|
|
307
|
+
"sessionMode": "default",
|
|
308
|
+
"maxConcurrency": 4,
|
|
309
|
+
"toolOptimize": true,
|
|
310
|
+
"structuredOutput": true,
|
|
311
|
+
"autoTransition": false
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`flowSettings.sessionMode` accepts `fast`, `default`, or `long`. Session mode precedence is:
|
|
317
|
+
|
|
318
|
+
```txt
|
|
319
|
+
per-flow sessionMode > --flow-session-mode > PI_FLOW_SESSION_MODE > flowSettings.sessionMode > default
|
|
320
|
+
```
|
|
321
|
+
|
|
262
322
|
### Flags
|
|
263
323
|
|
|
264
324
|
| Flag | Description | Default |
|
|
@@ -266,10 +326,12 @@ Settings are merged: project `.pi/settings.json` overrides global `~/.pi/agent/s
|
|
|
266
326
|
| `--flow-max-depth [n]` | Maximum delegation depth | `3` |
|
|
267
327
|
| `--flow-prevent-cycles` | Block cyclic delegation | `true` |
|
|
268
328
|
| `--no-flow-prevent-cycles` | Disable cycle prevention | — |
|
|
269
|
-
| `--flow-model-config [name]` | Select a named model strategy | `balance` |
|
|
329
|
+
| `--flow-model-config [name]` | Select a named model strategy for this invocation | `balance` |
|
|
330
|
+
| `--flow-mode [name]` | Persistently switch the global model strategy and apply it immediately | — |
|
|
270
331
|
| `--flow-lite-model [model]` | Override the lite-tier model | — |
|
|
271
332
|
| `--flow-flash-model [model]` | Override the flash-tier model | — |
|
|
272
333
|
| `--flow-full-model [model]` | Override the full-tier model | — |
|
|
334
|
+
| `--flow-session-mode [mode]` | Default child-flow session mode: `fast`, `default`, or `long` | `default` |
|
|
273
335
|
| `--tool-optimize` | Use unified `batch`/`batch_read` instead of separate read/write/edit | `true` |
|
|
274
336
|
| `--no-tool-optimize` | Disable tool optimization; use legacy read/write/edit tools | — |
|
|
275
337
|
|
|
@@ -282,7 +344,7 @@ Settings are merged: project `.pi/settings.json` overrides global `~/.pi/agent/s
|
|
|
282
344
|
| `PI_FLOW_STACK` | JSON array of ancestor flow names |
|
|
283
345
|
| `PI_FLOW_PREVENT_CYCLES` | `"1"` or `"0"` |
|
|
284
346
|
| `PI_FLOW_TOOL_OPTIMIZE` | `"1"` or `"0"` (overrides default tool optimization) |
|
|
285
|
-
| `
|
|
347
|
+
| `PI_FLOW_SESSION_MODE` | Default child-flow session mode: `fast`, `default`, or `long` |
|
|
286
348
|
|
|
287
349
|
---
|
|
288
350
|
|
package/agents/build.md
CHANGED
|
@@ -17,8 +17,9 @@ During this build flow — your mission is to implement and verify changes. Be a
|
|
|
17
17
|
4. Execute — implement changes following core principles.
|
|
18
18
|
5. Refactor — clean up only if the change is working.
|
|
19
19
|
6. Verify — run tests and relevant checks before considering the work done.
|
|
20
|
-
7.
|
|
21
|
-
8.
|
|
20
|
+
7. Document — update relevant docs after the implementation and verification are settled; if no docs apply, state why.
|
|
21
|
+
8. Ship — commit, push, monitor CI/CD, and fix failures until green when shipping is in scope.
|
|
22
|
+
9. Finalize — confirm implementation, docs, tests, and CI/CD status.
|
|
22
23
|
|
|
23
24
|
## Rules
|
|
24
25
|
|
|
@@ -28,6 +29,7 @@ During this build flow — your mission is to implement and verify changes. Be a
|
|
|
28
29
|
- If already on a feature/fix branch, continue on it.
|
|
29
30
|
- Commit with a clear conventional message such as `feat:`, `fix:`, or `refactor:` when committing is in scope.
|
|
30
31
|
- Push only after local verification passes when shipping is in scope.
|
|
32
|
+
- Update relevant documentation after finishing the work; if no docs changed, explain why in the final report.
|
|
31
33
|
- If CI/CD fails, diagnose, fix, commit, push, and repeat until green.
|
|
32
34
|
- If an unexpected error or trace is needed, recommend [debug] rather than guessing.
|
|
33
35
|
|
package/agents/debug.md
CHANGED
|
@@ -16,6 +16,8 @@ During this debug flow — your mission is to investigate root cause. Be forensi
|
|
|
16
16
|
3. Check changes — inspect recent diffs, configuration, dependencies, and environment differences.
|
|
17
17
|
4. Identify root cause — state exactly what is broken and why.
|
|
18
18
|
5. Recommend fix — propose the smallest safe correction only after evidence confirms the cause.
|
|
19
|
+
6. Document — update relevant docs, runbooks, or troubleshooting notes after finishing the investigation; if no docs apply, state why.
|
|
20
|
+
7. Finalize — confirm root cause, evidence, documentation updates, and recommended next steps.
|
|
19
21
|
|
|
20
22
|
## Rules
|
|
21
23
|
|
|
@@ -23,7 +25,8 @@ During this debug flow — your mission is to investigate root cause. Be forensi
|
|
|
23
25
|
- Read logs and symptoms before reading broad code areas.
|
|
24
26
|
- Use `batch` with `o: "read"`, `s: <offset>`, and `l: <limit>` for targeted file reading instead of bash `sed`/`head`/`tail`.
|
|
25
27
|
- Do not suggest fixes until root cause is confirmed.
|
|
26
|
-
-
|
|
28
|
+
- Documentation-only updates are required after finishing the work when relevant and safe; if no docs changed, explain why in the final report.
|
|
29
|
+
- Do not implement product-code changes from this flow unless explicitly requested.
|
|
27
30
|
|
|
28
31
|
## Note
|
|
29
32
|
Treat this as a clean-slate system rewrite, unless explicitly mentioned in the requirements. Perform a comprehensive migration with zero requirements for backwards compatibility. You must ensure that all residual code, variable names, test suites, and documentation are fully refactored and perfectly aligned with the new architecture.
|
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -56,6 +56,18 @@ export function getFlowTier(flowName: string): FlowTier {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** Return the bundled flow names associated with a given tier. */
|
|
60
|
+
export function getTierFlowNames(tier: FlowTier): string[] {
|
|
61
|
+
switch (tier) {
|
|
62
|
+
case "lite":
|
|
63
|
+
return ["scout", "debug"];
|
|
64
|
+
case "flash":
|
|
65
|
+
return ["build", "audit"];
|
|
66
|
+
case "full":
|
|
67
|
+
return ["ideas", "craft"];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
// ---------------------------------------------------------------------------
|
|
60
72
|
// Internal helpers
|
|
61
73
|
// ---------------------------------------------------------------------------
|
package/src/cli-args.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import { parseAgentSessionMode, type AgentSessionMode } from "./session-mode.js";
|
|
8
9
|
|
|
9
10
|
function looksLikeExplicitRelativePath(value: string): boolean {
|
|
10
11
|
return (
|
|
@@ -47,6 +48,8 @@ export interface ParsedFlowCliArgs {
|
|
|
47
48
|
fallbackTools?: string;
|
|
48
49
|
fallbackNoTools: boolean;
|
|
49
50
|
flowModelConfig?: string;
|
|
51
|
+
flowMode?: string;
|
|
52
|
+
flowSessionMode?: AgentSessionMode;
|
|
50
53
|
tieredModels: {
|
|
51
54
|
lite?: string;
|
|
52
55
|
flash?: string;
|
|
@@ -80,6 +83,8 @@ export function parseFlowCliArgs(argv: string[]): ParsedFlowCliArgs {
|
|
|
80
83
|
let fallbackTools: string | undefined;
|
|
81
84
|
let fallbackNoTools = false;
|
|
82
85
|
let flowModelConfig: string | undefined;
|
|
86
|
+
let flowMode: string | undefined;
|
|
87
|
+
let flowSessionMode: AgentSessionMode | undefined;
|
|
83
88
|
let tieredLiteModel: string | undefined;
|
|
84
89
|
let tieredFlashModel: string | undefined;
|
|
85
90
|
let tieredFullModel: string | undefined;
|
|
@@ -112,6 +117,20 @@ export function parseFlowCliArgs(argv: string[]): ParsedFlowCliArgs {
|
|
|
112
117
|
continue;
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
if (flagName === "--flow-mode") {
|
|
121
|
+
const [value, skip] = getValue();
|
|
122
|
+
if (value !== undefined) flowMode = value;
|
|
123
|
+
i += skip;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (flagName === "--flow-session-mode") {
|
|
128
|
+
const [value, skip] = getValue();
|
|
129
|
+
flowSessionMode = parseAgentSessionMode(value);
|
|
130
|
+
i += skip;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
if (
|
|
116
135
|
[
|
|
117
136
|
"--mode",
|
|
@@ -285,6 +304,8 @@ export function parseFlowCliArgs(argv: string[]): ParsedFlowCliArgs {
|
|
|
285
304
|
fallbackTools,
|
|
286
305
|
fallbackNoTools,
|
|
287
306
|
flowModelConfig,
|
|
307
|
+
flowMode,
|
|
308
|
+
flowSessionMode,
|
|
288
309
|
tieredModels: {
|
|
289
310
|
lite: tieredLiteModel,
|
|
290
311
|
flash: tieredFlashModel,
|
package/src/config.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
+
import { parseAgentSessionMode, type AgentSessionMode } from "./session-mode.js";
|
|
12
|
+
import { type FlowTier } from "./agents.js";
|
|
11
13
|
|
|
12
|
-
export type FlowTier = "lite" | "flash" | "full";
|
|
13
14
|
|
|
14
15
|
export interface FlowModelTierConfig {
|
|
15
16
|
primary?: string;
|
|
@@ -33,6 +34,8 @@ export interface FlowSettings {
|
|
|
33
34
|
maxConcurrency?: number;
|
|
34
35
|
/** Whether to automatically queue follow-up flows based on hook transitions. Default: false. */
|
|
35
36
|
autoTransition?: boolean;
|
|
37
|
+
/** Default child-flow session mode. Default: "default" (600s). */
|
|
38
|
+
sessionMode?: AgentSessionMode;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const BUILTIN_FLOW_MODEL_CONFIGS: FlowModelConfigs = {
|
|
@@ -54,7 +57,7 @@ function readSettingsJson(filePath: string): Record<string, unknown> | null {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
function getGlobalSettingsPath(): string {
|
|
60
|
+
export function getGlobalSettingsPath(): string {
|
|
58
61
|
const agentDir = process.env["PI_CODING_AGENT_DIR"]?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
59
62
|
return path.join(agentDir, "settings.json");
|
|
60
63
|
}
|
|
@@ -63,12 +66,56 @@ function getProjectSettingsPath(cwd: string): string {
|
|
|
63
66
|
return path.join(cwd, ".pi", "settings.json");
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
export function normalizeFlowModeName(value: unknown): string | undefined {
|
|
70
|
+
if (typeof value !== "string") return undefined;
|
|
71
|
+
const normalized = value.trim();
|
|
72
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
function extractSelectedFlowModelConfigName(settings: Record<string, unknown> | null): string | undefined {
|
|
67
76
|
if (!isPlainObject(settings)) return undefined;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
return normalizeFlowModeName(settings.flowModelConfig);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function loadProjectFlowModelConfigName(cwd: string): string | undefined {
|
|
81
|
+
return extractSelectedFlowModelConfigName(readSettingsJson(getProjectSettingsPath(cwd)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function writeGlobalFlowMode(mode: string): { path: string; previous?: string } {
|
|
85
|
+
const normalized = normalizeFlowModeName(mode);
|
|
86
|
+
if (!normalized) {
|
|
87
|
+
throw new Error("Cannot update flow mode. Expected a non-empty mode name.");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const filePath = getGlobalSettingsPath();
|
|
91
|
+
let settings: Record<string, unknown> = {};
|
|
92
|
+
|
|
93
|
+
if (fs.existsSync(filePath)) {
|
|
94
|
+
let parsed: unknown;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new Error(`Cannot update flow mode because ${filePath} contains invalid JSON.`);
|
|
99
|
+
}
|
|
100
|
+
if (!isPlainObject(parsed)) {
|
|
101
|
+
throw new Error(`Cannot update flow mode because ${filePath} must contain a JSON object.`);
|
|
102
|
+
}
|
|
103
|
+
settings = { ...parsed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const previous = extractSelectedFlowModelConfigName(settings);
|
|
107
|
+
settings.flowModelConfig = normalized;
|
|
108
|
+
|
|
109
|
+
const dir = path.dirname(filePath);
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
const tmpPath = path.join(dir, `.settings.json.${process.pid}.${Date.now()}.tmp`);
|
|
112
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
113
|
+
fs.renameSync(tmpPath, filePath);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
path: filePath,
|
|
117
|
+
...(previous !== undefined ? { previous } : {}),
|
|
118
|
+
};
|
|
72
119
|
}
|
|
73
120
|
|
|
74
121
|
function normalizeFailoverList(
|
|
@@ -186,6 +233,10 @@ function extractFlowSettings(settings: Record<string, unknown> | null): FlowSett
|
|
|
186
233
|
if (typeof obj.autoTransition === "boolean") {
|
|
187
234
|
result.autoTransition = obj.autoTransition;
|
|
188
235
|
}
|
|
236
|
+
const sessionMode = parseAgentSessionMode(obj.sessionMode);
|
|
237
|
+
if (sessionMode !== undefined) {
|
|
238
|
+
result.sessionMode = sessionMode;
|
|
239
|
+
}
|
|
189
240
|
return result;
|
|
190
241
|
}
|
|
191
242
|
|
|
@@ -245,7 +296,7 @@ export function selectFlowModelStrategy(
|
|
|
245
296
|
configs: FlowModelConfigs,
|
|
246
297
|
requestedName?: string,
|
|
247
298
|
): LoadedFlowModelConfigs {
|
|
248
|
-
const normalizedRequested = requestedName
|
|
299
|
+
const normalizedRequested = normalizeFlowModeName(requestedName) ?? "default";
|
|
249
300
|
const strategy = configs[normalizedRequested];
|
|
250
301
|
if (strategy) {
|
|
251
302
|
return { selectedName: normalizedRequested, configs, strategy };
|
|
@@ -318,3 +369,23 @@ export function resolveFlowModelCandidates(opts: {
|
|
|
318
369
|
|
|
319
370
|
return { primary: candidates[0], candidates };
|
|
320
371
|
}
|
|
372
|
+
|
|
373
|
+
export function formatFlowModelStrategy(modeName: string, strategy: FlowModelStrategy): string {
|
|
374
|
+
const tiers: FlowTier[] = ["lite", "flash", "full"];
|
|
375
|
+
const parts: string[] = [];
|
|
376
|
+
for (const tier of tiers) {
|
|
377
|
+
const config = strategy[tier];
|
|
378
|
+
const hasPrimary = Boolean(config?.primary);
|
|
379
|
+
const hasFailover = config?.failover && config.failover.length > 0;
|
|
380
|
+
let value: string;
|
|
381
|
+
if (hasPrimary) {
|
|
382
|
+
value = config!.primary!;
|
|
383
|
+
} else if (hasFailover) {
|
|
384
|
+
value = `failover: ${config!.failover!.join(", ")}`;
|
|
385
|
+
} else {
|
|
386
|
+
value = "(default)";
|
|
387
|
+
}
|
|
388
|
+
parts.push(`${tier}: ${value}`);
|
|
389
|
+
}
|
|
390
|
+
return `mode: ${modeName} | ${parts.join(" · ")}`;
|
|
391
|
+
}
|
package/src/executor.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { extractStructuredOutput } from "./structured-output.js";
|
|
|
18
18
|
import { runHooksDetailed, type RunHooksResult } from "./hooks.js";
|
|
19
19
|
import { mapFlowConcurrent, runFlow } from "./flow.js";
|
|
20
20
|
import { getFlowSummaryText } from "./runner-events.js";
|
|
21
|
-
import { resolveFlowModelCandidates, selectFlowModelStrategy, type LoadedFlowModelConfigs, type FlowModelStrategy } from "./config.js";
|
|
21
|
+
import { normalizeFlowModeName, resolveFlowModelCandidates, selectFlowModelStrategy, type LoadedFlowModelConfigs, type FlowModelStrategy } from "./config.js";
|
|
22
|
+
import { getAgentSessionTimeoutMs, resolveAgentSessionMode, type AgentSessionMode } from "./session-mode.js";
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Types
|
|
@@ -47,6 +48,8 @@ export interface FlowExecutorDeps {
|
|
|
47
48
|
maxConcurrency: number;
|
|
48
49
|
/** Whether auto-transition is enabled. */
|
|
49
50
|
autoTransition: boolean;
|
|
51
|
+
/** Default child-flow session mode. */
|
|
52
|
+
defaultSessionMode: AgentSessionMode;
|
|
50
53
|
/** Abort signal. */
|
|
51
54
|
signal?: AbortSignal;
|
|
52
55
|
/** Streaming update callback. */
|
|
@@ -82,6 +85,7 @@ export interface ExecuteFlowParams {
|
|
|
82
85
|
intent: string;
|
|
83
86
|
aim: string;
|
|
84
87
|
cwd?: string;
|
|
88
|
+
sessionMode?: AgentSessionMode;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
export interface ExecuteFlowResult {
|
|
@@ -184,7 +188,7 @@ export async function executeFlows(
|
|
|
184
188
|
const {
|
|
185
189
|
flows, currentDepth, maxDepth, ancestorFlowStack, preventCycles,
|
|
186
190
|
toolOptimize, structuredOutput, cwd, loadedFlowModelConfigs,
|
|
187
|
-
maxConcurrency, autoTransition, signal, onUpdate, makeDetails,
|
|
191
|
+
maxConcurrency, autoTransition, defaultSessionMode, signal, onUpdate, makeDetails,
|
|
188
192
|
getFlag, tierOverrideResolver, fallbackModel, forkSessionSnapshotJsonl,
|
|
189
193
|
flowResultCache, projectFlowsDir, hasUI, uiConfirm, onFlowMetrics,
|
|
190
194
|
confirmProjectFlows,
|
|
@@ -222,13 +226,16 @@ export async function executeFlows(
|
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
// Resolve model strategy
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
const cliFlowMode = normalizeFlowModeName(getFlag("flow-mode"));
|
|
230
|
+
const cliFlowModelConfig = normalizeFlowModeName(getFlag("flow-model-config"));
|
|
231
|
+
if (cliFlowMode !== undefined && cliFlowModelConfig !== undefined && cliFlowMode !== cliFlowModelConfig) {
|
|
232
|
+
console.warn(
|
|
233
|
+
`[pi-agent-flow] Both --flow-mode "${cliFlowMode}" and --flow-model-config "${cliFlowModelConfig}" were provided. Using --flow-mode.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
229
236
|
const selectedFlowModelConfig = selectFlowModelStrategy(
|
|
230
237
|
loadedFlowModelConfigs.configs,
|
|
231
|
-
cliFlowModelConfig ?? loadedFlowModelConfigs.selectedName,
|
|
238
|
+
cliFlowMode ?? cliFlowModelConfig ?? loadedFlowModelConfigs.selectedName,
|
|
232
239
|
);
|
|
233
240
|
|
|
234
241
|
// Pre-allocate results array
|
|
@@ -257,10 +264,12 @@ export async function executeFlows(
|
|
|
257
264
|
text +
|
|
258
265
|
"|" +
|
|
259
266
|
allResults
|
|
260
|
-
.map(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
.map((r) => {
|
|
268
|
+
const remainingSeconds = r.exitCode === -1 && typeof r.deadlineAtMs === "number"
|
|
269
|
+
? Math.max(0, Math.ceil((r.deadlineAtMs - Date.now()) / 1000))
|
|
270
|
+
: "";
|
|
271
|
+
return `${r.messages.length}:${r.usage.toolCalls}:${r.usage.input}:${r.usage.output}:${r.usage.contextTokens}:${r.usage.smoothedTps ?? 0}:${r.startedAtMs ?? ""}:${r.deadlineAtMs ?? ""}:${remainingSeconds}:${r.errorMessage ?? ""}`;
|
|
272
|
+
})
|
|
264
273
|
.join(";");
|
|
265
274
|
if (signature === lastEmittedSignature) return;
|
|
266
275
|
lastEmittedSignature = signature;
|
|
@@ -276,6 +285,7 @@ export async function executeFlows(
|
|
|
276
285
|
const executionStart = Date.now();
|
|
277
286
|
const results = await mapFlowConcurrent(params, maxConcurrency, async (item, index) => {
|
|
278
287
|
const normalizedType = item.type.toLowerCase();
|
|
288
|
+
const sessionMode = resolveAgentSessionMode(item.sessionMode, defaultSessionMode);
|
|
279
289
|
const targetFlow = flows.find((f) => f.name === normalizedType);
|
|
280
290
|
const effectiveMaxDepth =
|
|
281
291
|
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
@@ -297,6 +307,22 @@ export async function executeFlows(
|
|
|
297
307
|
for (let attempt = 0; attempt < attemptModels.length; attempt++) {
|
|
298
308
|
const candidateModel = attemptModels[attempt];
|
|
299
309
|
if (candidateModel) attemptedModels.push(candidateModel);
|
|
310
|
+
const attemptStartMs = Date.now();
|
|
311
|
+
const attemptTimeoutMs = getAgentSessionTimeoutMs(sessionMode);
|
|
312
|
+
allResults[index] = {
|
|
313
|
+
type: normalizedType,
|
|
314
|
+
agentSource: targetFlow?.source ?? "unknown",
|
|
315
|
+
intent: item.intent,
|
|
316
|
+
aim: item.aim,
|
|
317
|
+
exitCode: -1,
|
|
318
|
+
messages: [],
|
|
319
|
+
stderr: "",
|
|
320
|
+
usage: emptyFlowUsage(),
|
|
321
|
+
model: candidateModel,
|
|
322
|
+
startedAtMs: attemptStartMs,
|
|
323
|
+
deadlineAtMs: attemptStartMs + attemptTimeoutMs,
|
|
324
|
+
};
|
|
325
|
+
emitProgress();
|
|
300
326
|
result = await runFlow({
|
|
301
327
|
cwd,
|
|
302
328
|
flows,
|
|
@@ -311,6 +337,7 @@ export async function executeFlows(
|
|
|
311
337
|
preventCycles,
|
|
312
338
|
toolOptimize,
|
|
313
339
|
structuredOutput,
|
|
340
|
+
sessionMode,
|
|
314
341
|
model: candidateModel,
|
|
315
342
|
signal,
|
|
316
343
|
onUpdate: (partial) => {
|
package/src/flow.ts
CHANGED
|
@@ -21,11 +21,11 @@ import {
|
|
|
21
21
|
normalizeFlowResult,
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
import { extractStructuredOutput, enrichStructuredOutputCommands } from "./structured-output.js";
|
|
24
|
+
import { DEFAULT_AGENT_SESSION_MODE, getAgentSessionTimeoutMs, type AgentSessionMode } from "./session-mode.js";
|
|
24
25
|
|
|
25
26
|
const isWindows = process.platform === "win32";
|
|
26
27
|
const SIGKILL_TIMEOUT_MS = 5000;
|
|
27
28
|
const AGENT_END_GRACE_MS = 2000;
|
|
28
|
-
const DEFAULT_FLOW_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
29
29
|
const FLOW_TIME_BUDGET_WARNING_MS = 2 * 60 * 1000; // warn 2 min before kill
|
|
30
30
|
const FLOW_FINAL_URGE_MS = 30 * 1000; // final urge 30 s before kill
|
|
31
31
|
const REPORTING_GRACE_MS = 10_000; // grace period after timeout for agent to report findings
|
|
@@ -34,7 +34,6 @@ const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
|
34
34
|
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
35
35
|
const FLOW_STACK_ENV = "PI_FLOW_STACK";
|
|
36
36
|
const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
37
|
-
const FLOW_TIMEOUT_ENV = "PI_FLOW_TIMEOUT_MS";
|
|
38
37
|
const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
39
38
|
const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
|
|
40
39
|
export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
|
|
@@ -146,7 +145,8 @@ function buildFlowArgs(
|
|
|
146
145
|
maxDepth: number = 0,
|
|
147
146
|
toolOptimize: boolean = false,
|
|
148
147
|
structuredOutput: boolean = true,
|
|
149
|
-
|
|
148
|
+
sessionMode: AgentSessionMode = DEFAULT_AGENT_SESSION_MODE,
|
|
149
|
+
sessionTimeoutMs: number = getAgentSessionTimeoutMs(sessionMode),
|
|
150
150
|
): string[] {
|
|
151
151
|
const args: string[] = [
|
|
152
152
|
"--mode",
|
|
@@ -163,6 +163,9 @@ function buildFlowArgs(
|
|
|
163
163
|
if (inheritedCliArgs.flowModelConfig) {
|
|
164
164
|
args.push("--flow-model-config", inheritedCliArgs.flowModelConfig);
|
|
165
165
|
}
|
|
166
|
+
if (inheritedCliArgs.flowSessionMode) {
|
|
167
|
+
args.push("--flow-session-mode", inheritedCliArgs.flowSessionMode);
|
|
168
|
+
}
|
|
166
169
|
if (inheritedCliArgs.tieredModels?.lite) {
|
|
167
170
|
args.push("--flow-lite-model", inheritedCliArgs.tieredModels.lite);
|
|
168
171
|
}
|
|
@@ -218,8 +221,8 @@ function buildFlowArgs(
|
|
|
218
221
|
: `You may NOT delegate to sub-flows (depth limit reached).`;
|
|
219
222
|
|
|
220
223
|
const timeBudgetHint =
|
|
221
|
-
|
|
222
|
-
? `Time budget: ${Math.round(
|
|
224
|
+
sessionTimeoutMs > 0
|
|
225
|
+
? `Session mode: ${sessionMode}. Time budget: ${Math.round(sessionTimeoutMs / 1000)}s total. Long-running tools may be interrupted near the deadline to preserve final-summary time; if a tool reports [Flow timeout], stop tool use and output structured findings immediately.\n`
|
|
223
226
|
: "";
|
|
224
227
|
|
|
225
228
|
const activation =
|
|
@@ -324,8 +327,8 @@ export interface RunFlowOptions {
|
|
|
324
327
|
onUpdate?: FlowUpdateCallback;
|
|
325
328
|
/** Factory to wrap results into FlowDetails. */
|
|
326
329
|
makeDetails: (results: SingleResult[]) => FlowDetails;
|
|
327
|
-
/**
|
|
328
|
-
|
|
330
|
+
/** Child-flow session mode. Default: "default" (600s). */
|
|
331
|
+
sessionMode?: AgentSessionMode;
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
/**
|
|
@@ -370,6 +373,10 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
370
373
|
};
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
const effectiveSessionMode = opts.sessionMode ?? DEFAULT_AGENT_SESSION_MODE;
|
|
377
|
+
const effectiveTimeout = getAgentSessionTimeoutMs(effectiveSessionMode);
|
|
378
|
+
const startedAtMs = Date.now();
|
|
379
|
+
const deadlineAtMs = effectiveTimeout > 0 ? startedAtMs + effectiveTimeout : undefined;
|
|
373
380
|
const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
|
|
374
381
|
const result: SingleResult = {
|
|
375
382
|
type: normalizedFlowName,
|
|
@@ -381,6 +388,8 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
381
388
|
stderr: "",
|
|
382
389
|
usage: emptyFlowUsage(),
|
|
383
390
|
model: resolvedModel,
|
|
391
|
+
startedAtMs,
|
|
392
|
+
...(deadlineAtMs !== undefined ? { deadlineAtMs } : {}),
|
|
384
393
|
};
|
|
385
394
|
|
|
386
395
|
let liveStreamingText = "";
|
|
@@ -419,14 +428,6 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
419
428
|
forkSessionTmpPath = forkTmp.filePath;
|
|
420
429
|
}
|
|
421
430
|
|
|
422
|
-
// Resolve timeout: explicit option > env var > default (10 min)
|
|
423
|
-
const envTimeoutRaw = process.env[FLOW_TIMEOUT_ENV];
|
|
424
|
-
const envTimeout = envTimeoutRaw !== undefined ? (() => {
|
|
425
|
-
const n = Number(envTimeoutRaw);
|
|
426
|
-
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
427
|
-
})() : null;
|
|
428
|
-
const effectiveTimeout = opts.timeoutMs ?? envTimeout ?? DEFAULT_FLOW_TIMEOUT_MS;
|
|
429
|
-
|
|
430
431
|
try {
|
|
431
432
|
const piArgs = buildFlowArgs(
|
|
432
433
|
flow,
|
|
@@ -437,6 +438,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
437
438
|
maxDepth,
|
|
438
439
|
toolOptimize,
|
|
439
440
|
structuredOutput,
|
|
441
|
+
effectiveSessionMode,
|
|
440
442
|
effectiveTimeout,
|
|
441
443
|
);
|
|
442
444
|
let wasAborted = false;
|
|
@@ -468,7 +470,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
468
470
|
[FLOW_TOOL_OPTIMIZE_ENV]: toolOptimize ? "1" : "0",
|
|
469
471
|
[PI_OFFLINE_ENV]: "1",
|
|
470
472
|
...(effectiveTimeout > 0 ? {
|
|
471
|
-
[FLOW_DEADLINE_ENV]: String(
|
|
473
|
+
[FLOW_DEADLINE_ENV]: String(deadlineAtMs),
|
|
472
474
|
[FLOW_TOOL_SUMMARY_GRACE_ENV]: String(toolSummaryGraceMs),
|
|
473
475
|
} : {
|
|
474
476
|
[FLOW_DEADLINE_ENV]: "",
|
|
@@ -494,6 +496,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
494
496
|
let settled = false;
|
|
495
497
|
let timeoutFired = false;
|
|
496
498
|
let semanticCompletionTimer: NodeJS.Timeout | undefined;
|
|
499
|
+
let countdownTimer: NodeJS.Timeout | undefined;
|
|
497
500
|
|
|
498
501
|
const clearSemanticCompletionTimer = () => {
|
|
499
502
|
if (semanticCompletionTimer) {
|
|
@@ -502,6 +505,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
502
505
|
}
|
|
503
506
|
};
|
|
504
507
|
|
|
508
|
+
const clearCountdownTimer = () => {
|
|
509
|
+
if (countdownTimer) {
|
|
510
|
+
clearInterval(countdownTimer);
|
|
511
|
+
countdownTimer = undefined;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
505
515
|
const terminateChild = () => {
|
|
506
516
|
endStdin();
|
|
507
517
|
if (isWindows) {
|
|
@@ -529,6 +539,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
529
539
|
settled = true;
|
|
530
540
|
endStdin();
|
|
531
541
|
clearSemanticCompletionTimer();
|
|
542
|
+
clearCountdownTimer();
|
|
532
543
|
if (signal && abortHandler) {
|
|
533
544
|
signal.removeEventListener("abort", abortHandler);
|
|
534
545
|
}
|
|
@@ -575,6 +586,14 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
575
586
|
proc.stdout.on("data", onStdoutData);
|
|
576
587
|
proc.stderr.on("data", onStderrData);
|
|
577
588
|
|
|
589
|
+
if (onUpdate && effectiveTimeout > 0) {
|
|
590
|
+
countdownTimer = setInterval(() => {
|
|
591
|
+
if (didClose || settled) return;
|
|
592
|
+
emitUpdate();
|
|
593
|
+
}, 1000);
|
|
594
|
+
countdownTimer.unref();
|
|
595
|
+
}
|
|
596
|
+
|
|
578
597
|
proc.on("close", (code) => {
|
|
579
598
|
didClose = true;
|
|
580
599
|
if (buffer.trim()) flushBufferedLines(buffer);
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,12 @@ import { type FlowConfig, discoverFlows, getFlowTier } from "./agents.js";
|
|
|
12
12
|
import {
|
|
13
13
|
loadFlowModelConfigs,
|
|
14
14
|
loadFlowSettings,
|
|
15
|
+
loadProjectFlowModelConfigName,
|
|
16
|
+
normalizeFlowModeName,
|
|
15
17
|
resolveFlowModelCandidates,
|
|
16
18
|
selectFlowModelStrategy,
|
|
19
|
+
writeGlobalFlowMode,
|
|
20
|
+
formatFlowModelStrategy,
|
|
17
21
|
type LoadedFlowModelConfigs,
|
|
18
22
|
} from "./config.js";
|
|
19
23
|
import { getInheritedCliArgs } from "./cli-args.js";
|
|
@@ -55,6 +59,12 @@ import {
|
|
|
55
59
|
} from "./sliding-prompt.js";
|
|
56
60
|
import { DEFAULT_TRANSITIONS, buildTransitionHooks } from "./transitions.js";
|
|
57
61
|
import { createTimedBashToolDefinition } from "./timed-bash.js";
|
|
62
|
+
import {
|
|
63
|
+
DEFAULT_AGENT_SESSION_MODE,
|
|
64
|
+
PI_FLOW_SESSION_MODE_ENV,
|
|
65
|
+
parseAgentSessionMode,
|
|
66
|
+
type AgentSessionMode,
|
|
67
|
+
} from "./session-mode.js";
|
|
58
68
|
|
|
59
69
|
// ---------------------------------------------------------------------------
|
|
60
70
|
// Limits
|
|
@@ -85,13 +95,23 @@ const FlowItem = Type.Object({
|
|
|
85
95
|
cwd: Type.Optional(
|
|
86
96
|
Type.String({ description: "Working directory override for this flow." }),
|
|
87
97
|
),
|
|
98
|
+
sessionMode: Type.Optional(
|
|
99
|
+
Type.Union([
|
|
100
|
+
Type.Literal("fast"),
|
|
101
|
+
Type.Literal("default"),
|
|
102
|
+
Type.Literal("long"),
|
|
103
|
+
], {
|
|
104
|
+
description: "Agent session budget for this flow: fast=300s, default=600s, long=900s. Use long only for large builds, broad refactors, full test runs, or complex debugging.",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
88
107
|
});
|
|
89
108
|
|
|
90
109
|
const FlowParams = Type.Object({
|
|
91
110
|
flow: Type.Array(FlowItem, {
|
|
92
111
|
description:
|
|
93
112
|
"Array of flow tasks to execute. Each runs in its own forked process. " +
|
|
94
|
-
|
|
113
|
+
"Optional sessionMode selects the child-agent budget: fast=300s, default=600s, long=900s. " +
|
|
114
|
+
'Example: { flow: [{ type: "scout", "intent": "Find all authentication-related code and trace JWT validation", "aim": "Find auth code and trace JWT", "sessionMode": "fast" }, { type: "build", "intent": "Fix the bug in user registration", "aim": "Fix registration bug", "sessionMode": "long" }] }',
|
|
95
115
|
minItems: 1,
|
|
96
116
|
}),
|
|
97
117
|
confirmProjectFlows: Type.Optional(
|
|
@@ -634,6 +654,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
634
654
|
description: "Named flow model strategy from settings.json flowModelConfigs.",
|
|
635
655
|
type: "string",
|
|
636
656
|
});
|
|
657
|
+
pi.registerFlag("flow-mode", {
|
|
658
|
+
description: "Persistently switch the global flow model strategy in ~/.pi/agent/settings.json.",
|
|
659
|
+
type: "string",
|
|
660
|
+
});
|
|
637
661
|
pi.registerFlag("flow-lite-model", {
|
|
638
662
|
description: "Model for lite-tier flows (scout, debug).",
|
|
639
663
|
type: "string",
|
|
@@ -650,6 +674,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
650
674
|
description: "Maximum number of flows to execute in parallel (default: 4).",
|
|
651
675
|
type: "string",
|
|
652
676
|
});
|
|
677
|
+
pi.registerFlag("flow-session-mode", {
|
|
678
|
+
description: "Default child-flow session mode: fast (300s), default (600s), or long (900s).",
|
|
679
|
+
type: "string",
|
|
680
|
+
});
|
|
653
681
|
pi.registerFlag("auto-transition", {
|
|
654
682
|
description: "Automatically queue follow-up flows based on hook transitions (default: false).",
|
|
655
683
|
type: "boolean",
|
|
@@ -669,6 +697,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
669
697
|
let structuredOutput = true;
|
|
670
698
|
let maxConcurrency = 4;
|
|
671
699
|
let autoTransition = false;
|
|
700
|
+
let defaultSessionMode: AgentSessionMode = DEFAULT_AGENT_SESSION_MODE;
|
|
672
701
|
const envToolOptimize = process.env[FLOW_TOOL_OPTIMIZE_ENV];
|
|
673
702
|
if (envToolOptimize !== undefined) {
|
|
674
703
|
const parsed = parseBoolean(envToolOptimize);
|
|
@@ -681,39 +710,98 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
710
|
configs: { default: {} },
|
|
682
711
|
strategy: {},
|
|
683
712
|
};
|
|
713
|
+
let activeRuntimeFlowMode: string | undefined;
|
|
684
714
|
|
|
685
715
|
// Auto-discover flows on session start
|
|
686
716
|
pi.on("session_start", async (_event, ctx) => {
|
|
687
717
|
const discovery = discoverFlows(ctx.cwd, "all");
|
|
688
718
|
discoveredFlows = discovery.flows;
|
|
689
719
|
loadedFlowModelConfigs = loadFlowModelConfigs(ctx.cwd);
|
|
720
|
+
activeRuntimeFlowMode = undefined;
|
|
721
|
+
|
|
722
|
+
const requestedFlowMode = normalizeFlowModeName(pi.getFlag("flow-mode"));
|
|
723
|
+
if (requestedFlowMode !== undefined) {
|
|
724
|
+
if (!Object.prototype.hasOwnProperty.call(loadedFlowModelConfigs.configs, requestedFlowMode)) {
|
|
725
|
+
const availableModes = Object.keys(loadedFlowModelConfigs.configs).sort().join(", ") || "(none)";
|
|
726
|
+
console.warn(
|
|
727
|
+
`[pi-agent-flow] Cannot switch flow mode to "${requestedFlowMode}"; no flowModelConfigs.${requestedFlowMode} strategy was found. Available modes: ${availableModes}.`,
|
|
728
|
+
);
|
|
729
|
+
} else {
|
|
730
|
+
try {
|
|
731
|
+
writeGlobalFlowMode(requestedFlowMode);
|
|
732
|
+
const strategy = loadedFlowModelConfigs.configs[requestedFlowMode] ?? {};
|
|
733
|
+
const strategyDescription = formatFlowModelStrategy(requestedFlowMode, strategy);
|
|
734
|
+
console.warn(strategyDescription);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
737
|
+
console.warn(`[pi-agent-flow] ${message}`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const projectFlowModelConfig = loadProjectFlowModelConfigName(ctx.cwd);
|
|
741
|
+
if (projectFlowModelConfig !== undefined && projectFlowModelConfig !== requestedFlowMode) {
|
|
742
|
+
console.warn(
|
|
743
|
+
`[pi-agent-flow] Switched global flow mode to "${requestedFlowMode}"; this project selects "${projectFlowModelConfig}" in .pi/settings.json, so future runs in this project may still use "${projectFlowModelConfig}" unless project settings are changed.`,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
activeRuntimeFlowMode = requestedFlowMode;
|
|
748
|
+
loadedFlowModelConfigs = selectFlowModelStrategy(loadedFlowModelConfigs.configs, requestedFlowMode);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
690
751
|
|
|
691
752
|
// Register declarative transition hooks from the transition matrix
|
|
692
753
|
for (const hook of buildTransitionHooks(DEFAULT_TRANSITIONS)) {
|
|
693
754
|
registerHook(hook);
|
|
694
755
|
}
|
|
695
756
|
|
|
757
|
+
const flowSettings = loadFlowSettings(ctx.cwd);
|
|
758
|
+
if (typeof flowSettings.structuredOutput === "boolean") {
|
|
759
|
+
structuredOutput = flowSettings.structuredOutput;
|
|
760
|
+
}
|
|
761
|
+
if (typeof flowSettings.maxConcurrency === "number") {
|
|
762
|
+
maxConcurrency = flowSettings.maxConcurrency;
|
|
763
|
+
}
|
|
764
|
+
if (typeof flowSettings.autoTransition === "boolean") {
|
|
765
|
+
autoTransition = flowSettings.autoTransition;
|
|
766
|
+
}
|
|
767
|
+
|
|
696
768
|
// Resolve toolOptimize: CLI flag > env var > settings.json > default
|
|
697
769
|
const cliFlag = pi.getFlag("tool-optimize");
|
|
770
|
+
if (typeof flowSettings.toolOptimize === "boolean") {
|
|
771
|
+
toolOptimize = flowSettings.toolOptimize;
|
|
772
|
+
}
|
|
773
|
+
if (envToolOptimize !== undefined) {
|
|
774
|
+
const parsed = parseBoolean(envToolOptimize);
|
|
775
|
+
if (parsed !== null) toolOptimize = parsed;
|
|
776
|
+
}
|
|
698
777
|
if (typeof cliFlag === "boolean") {
|
|
699
778
|
toolOptimize = cliFlag;
|
|
700
779
|
} else if (typeof cliFlag === "string") {
|
|
701
780
|
const parsed = parseBoolean(cliFlag);
|
|
702
781
|
if (parsed !== null) toolOptimize = parsed;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Resolve sessionMode: CLI flag > env var > settings.json > default
|
|
785
|
+
defaultSessionMode = flowSettings.sessionMode ?? DEFAULT_AGENT_SESSION_MODE;
|
|
786
|
+
const envSessionModeRaw = process.env[PI_FLOW_SESSION_MODE_ENV];
|
|
787
|
+
if (envSessionModeRaw !== undefined) {
|
|
788
|
+
const envSessionMode = parseAgentSessionMode(envSessionModeRaw);
|
|
789
|
+
if (envSessionMode !== undefined) {
|
|
790
|
+
defaultSessionMode = envSessionMode;
|
|
791
|
+
} else {
|
|
792
|
+
console.warn(`[pi-agent-flow] Ignoring invalid ${PI_FLOW_SESSION_MODE_ENV}="${envSessionModeRaw}". Expected fast, default, or long.`);
|
|
713
793
|
}
|
|
714
|
-
|
|
715
|
-
|
|
794
|
+
}
|
|
795
|
+
const cliSessionModeRaw = pi.getFlag("flow-session-mode");
|
|
796
|
+
if (typeof cliSessionModeRaw === "string") {
|
|
797
|
+
const cliSessionMode = parseAgentSessionMode(cliSessionModeRaw);
|
|
798
|
+
if (cliSessionMode !== undefined) {
|
|
799
|
+
defaultSessionMode = cliSessionMode;
|
|
800
|
+
} else {
|
|
801
|
+
console.warn(`[pi-agent-flow] Ignoring invalid --flow-session-mode value "${cliSessionModeRaw}". Expected fast, default, or long.`);
|
|
716
802
|
}
|
|
803
|
+
} else if (inheritedCliArgs.flowSessionMode !== undefined) {
|
|
804
|
+
defaultSessionMode = inheritedCliArgs.flowSessionMode;
|
|
717
805
|
}
|
|
718
806
|
|
|
719
807
|
// Resolve maxConcurrency: CLI flag > env var > settings.json > default
|
|
@@ -908,7 +996,8 @@ flow [type] accomplished
|
|
|
908
996
|
"You MUST enter to the following flow states, with tool call method.",
|
|
909
997
|
"",
|
|
910
998
|
"Flow states are isolated \u03c0 processes with forked session snapshots. They run in parallel.",
|
|
911
|
-
'Invoke: { "flow": [{ "type": "scout", "intent": "..." }, ...] }',
|
|
999
|
+
'Invoke: { "flow": [{ "type": "scout", "intent": "...", "aim": "...", "sessionMode": "default" }, ...] }',
|
|
1000
|
+
"Session modes: fast=300s, default=600s, long=900s. Use long only when the work genuinely needs the larger budget.",
|
|
912
1001
|
"States: scout, debug, build, craft, audit, ideas.",
|
|
913
1002
|
"Custom states configs in (create if not exists): .md files in .pi/agents/ or ~/.pi/agent/agents/.",
|
|
914
1003
|
].join("\n"),
|
|
@@ -953,10 +1042,11 @@ flow [type] accomplished
|
|
|
953
1042
|
loadedFlowModelConfigs,
|
|
954
1043
|
maxConcurrency,
|
|
955
1044
|
autoTransition,
|
|
1045
|
+
defaultSessionMode,
|
|
956
1046
|
signal,
|
|
957
1047
|
onUpdate,
|
|
958
1048
|
makeDetails,
|
|
959
|
-
getFlag: (name: string) => pi.getFlag(name),
|
|
1049
|
+
getFlag: (name: string) => name === "flow-mode" ? activeRuntimeFlowMode : pi.getFlag(name),
|
|
960
1050
|
tierOverrideResolver: getTierOverride,
|
|
961
1051
|
fallbackModel: inheritedCliArgs.fallbackModel,
|
|
962
1052
|
forkSessionSnapshotJsonl,
|
|
@@ -968,7 +1058,7 @@ flow [type] accomplished
|
|
|
968
1058
|
onFlowMetrics: (metrics) => { if (typeof pi.emit === "function") pi.emit("pi-agent-flow:complete", metrics); },
|
|
969
1059
|
confirmProjectFlows: params.confirmProjectFlows,
|
|
970
1060
|
},
|
|
971
|
-
params.flow.map((f: any) => ({ type: f.type, intent: f.intent, aim: f.aim, cwd: f.cwd })),
|
|
1061
|
+
params.flow.map((f: any) => ({ type: f.type, intent: f.intent, aim: f.aim, cwd: f.cwd, sessionMode: f.sessionMode })),
|
|
972
1062
|
toolCallId,
|
|
973
1063
|
);
|
|
974
1064
|
|
|
@@ -1004,7 +1094,12 @@ flow [type] accomplished
|
|
|
1004
1094
|
pi.emit("pi-agent-flow:ready", pluginApi);
|
|
1005
1095
|
}
|
|
1006
1096
|
|
|
1007
|
-
// Register cleanup on process exit (once)
|
|
1097
|
+
// Register cleanup on process exit (once).
|
|
1098
|
+
// We intentionally do NOT hook SIGINT/SIGTERM here. A plugin must not
|
|
1099
|
+
// override the host process signal handling; doing so can cut off the
|
|
1100
|
+
// host's terminal-cleanup logic and leave escape sequences or status
|
|
1101
|
+
// lines visible on Ctrl+C. The 'exit' event fires for normal exits,
|
|
1102
|
+
// including when the host handles the signal and calls process.exit().
|
|
1008
1103
|
if (!(globalThis as any).__pi_agent_flow_shutdown_registered) {
|
|
1009
1104
|
(globalThis as any).__pi_agent_flow_shutdown_registered = true;
|
|
1010
1105
|
const emitShutdown = () => {
|
|
@@ -1013,8 +1108,6 @@ flow [type] accomplished
|
|
|
1013
1108
|
}
|
|
1014
1109
|
};
|
|
1015
1110
|
process.on("exit", emitShutdown);
|
|
1016
|
-
process.on("SIGINT", () => { emitShutdown(); process.exit(130); });
|
|
1017
|
-
process.on("SIGTERM", () => { emitShutdown(); process.exit(143); });
|
|
1018
1111
|
}
|
|
1019
1112
|
|
|
1020
1113
|
}
|
package/src/render-utils.ts
CHANGED
|
@@ -43,27 +43,34 @@ function formatTps(value: number | undefined): string {
|
|
|
43
43
|
return value.toFixed(1).padStart(5);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
export function formatCompactTokenPair(usage: Partial<UsageStats>): string {
|
|
47
|
+
return `↑ ${formatFixedTokens(usage.input || 0)} · ↓ ${formatFixedTokens(usage.output || 0)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatCompactStats(
|
|
51
|
+
usage: Partial<UsageStats>,
|
|
52
|
+
model?: string,
|
|
53
|
+
maxWidth?: number,
|
|
54
|
+
options: { skipTokens?: boolean } = {},
|
|
55
|
+
): string {
|
|
56
|
+
const tokenParts = [`↑ ${formatFixedTokens(usage.input || 0)}`, `↓ ${formatFixedTokens(usage.output || 0)}`];
|
|
57
|
+
const runtimeParts = [`tps: ${formatTps(usage.smoothedTps)}`, `ctx: ${formatFixedTokens(usage.contextTokens || 0)}`];
|
|
58
|
+
const parts = options.skipTokens ? runtimeParts : [...tokenParts, ...runtimeParts];
|
|
52
59
|
|
|
53
60
|
const displayModel = model ? model.replace(/^[^/]+\//, "") : undefined;
|
|
54
61
|
let result = parts.join(" · ") + (displayModel ? ` · ${displayModel}` : "");
|
|
55
62
|
if (maxWidth && visibleLength(result) > maxWidth) {
|
|
56
|
-
// Drop model first
|
|
63
|
+
// Drop model first.
|
|
57
64
|
let narrow = parts.join(" · ");
|
|
58
65
|
if (visibleLength(narrow) <= maxWidth) return narrow;
|
|
59
66
|
|
|
60
|
-
// Drop context tokens
|
|
61
|
-
const
|
|
62
|
-
narrow =
|
|
67
|
+
// Drop context tokens next.
|
|
68
|
+
const withoutContext = parts.filter((part) => !part.startsWith("ctx:"));
|
|
69
|
+
narrow = withoutContext.join(" · ");
|
|
63
70
|
if (visibleLength(narrow) <= maxWidth) return narrow;
|
|
64
71
|
|
|
65
|
-
// Bare minimum
|
|
66
|
-
narrow =
|
|
72
|
+
// Bare minimum: token pair for normal stats, tps for token-free headers.
|
|
73
|
+
narrow = options.skipTokens ? runtimeParts[0] : tokenParts.join(" · ");
|
|
67
74
|
if (visibleLength(narrow) <= maxWidth) return narrow;
|
|
68
75
|
|
|
69
76
|
return truncateChars(result, maxWidth);
|
|
@@ -72,6 +79,13 @@ export function formatCompactStats(usage: Partial<UsageStats>, model?: string, m
|
|
|
72
79
|
return result;
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
export function formatCountdown(ms: number): string {
|
|
83
|
+
const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
|
|
84
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
85
|
+
const seconds = totalSeconds % 60;
|
|
86
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
/** Regex matching ANSI escape sequences. */
|
|
76
90
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
77
91
|
|
package/src/render.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
isFlowError,
|
|
23
23
|
isFlowSuccess,
|
|
24
24
|
} from "./types.js";
|
|
25
|
-
import { formatCompactStats, formatFlowTypeName, truncateChars, tailText, contentBudget } from "./render-utils.js";
|
|
25
|
+
import { formatCompactStats, formatCompactTokenPair, formatCountdown, formatFlowTypeName, truncateChars, tailText, contentBudget, visibleLength } from "./render-utils.js";
|
|
26
26
|
|
|
27
27
|
function shortenPath(p: string): string {
|
|
28
28
|
const home = os.homedir();
|
|
@@ -138,6 +138,20 @@ function sectionHeader(label: string): string {
|
|
|
138
138
|
return `${left} ${label} ${right}`;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function getLiveCountdown(r: SingleResult): string | undefined {
|
|
142
|
+
if (r.exitCode !== -1 || typeof r.deadlineAtMs !== "number") return undefined;
|
|
143
|
+
return formatCountdown(r.deadlineAtMs - Date.now());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatAimLinePrefix(treePrefix: string, r: SingleResult): string {
|
|
147
|
+
const countdown = getLiveCountdown(r);
|
|
148
|
+
return countdown ? `${treePrefix} aim: [${countdown}] - ` : `${treePrefix} aim: `;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatMsgLinePrefix(treePrefix: string, r: SingleResult): string {
|
|
152
|
+
return `${treePrefix} msg: [${formatCompactTokenPair(r.usage)}] - `;
|
|
153
|
+
}
|
|
154
|
+
|
|
141
155
|
// ---------------------------------------------------------------------------
|
|
142
156
|
// renderFlowCall — shown while the flow is being invoked
|
|
143
157
|
// ---------------------------------------------------------------------------
|
|
@@ -305,7 +319,7 @@ function renderFlowCollapsed(
|
|
|
305
319
|
): Container {
|
|
306
320
|
const container = new Container();
|
|
307
321
|
const maxWidth = process.stdout.columns ?? 80;
|
|
308
|
-
const stats = formatCompactStats(r.usage, r.model, maxWidth);
|
|
322
|
+
const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true });
|
|
309
323
|
const typeName = formatFlowTypeName(r.type);
|
|
310
324
|
let header = `${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", stats)}`;
|
|
311
325
|
if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
@@ -313,37 +327,40 @@ function renderFlowCollapsed(
|
|
|
313
327
|
|
|
314
328
|
// aim: line (short headline)
|
|
315
329
|
if (r.aim) {
|
|
316
|
-
const
|
|
317
|
-
|
|
330
|
+
const aimPrefix = formatAimLinePrefix("├─", r);
|
|
331
|
+
const dirContent = truncateChars(r.aim, contentBudget(visibleLength(aimPrefix)));
|
|
332
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", aimPrefix)}${theme.fg("dim", dirContent)}`, 0, 0));
|
|
318
333
|
}
|
|
319
334
|
|
|
320
335
|
// act: line (last tool call with count)
|
|
321
336
|
const lastTool = getLastToolCall(r.messages);
|
|
322
337
|
if (lastTool) {
|
|
323
338
|
const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
324
|
-
const actPrefix =
|
|
325
|
-
const actContent = truncateChars(actStr, contentBudget(
|
|
326
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
339
|
+
const actPrefix = `├─ act: [${r.usage.toolCalls}] - `;
|
|
340
|
+
const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix)));
|
|
341
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${actContent}`, 0, 0));
|
|
327
342
|
}
|
|
328
343
|
|
|
329
344
|
// msg: line (last assistant text or streaming)
|
|
345
|
+
const msgPrefix = formatMsgLinePrefix("└─", r);
|
|
346
|
+
const msgBudget = contentBudget(visibleLength(msgPrefix));
|
|
330
347
|
if (r.exitCode === -1 && streamingText) {
|
|
331
|
-
const logContent = tailText(streamingText,
|
|
332
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
348
|
+
const logContent = tailText(streamingText, msgBudget);
|
|
349
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", logContent)}`, 0, 0));
|
|
333
350
|
} else if (r.structuredOutput?.summary) {
|
|
334
|
-
const logContent = truncateChars(r.structuredOutput.summary,
|
|
335
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
351
|
+
const logContent = truncateChars(r.structuredOutput.summary, msgBudget);
|
|
352
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", logContent)}`, 0, 0));
|
|
336
353
|
} else if (flowOutput) {
|
|
337
|
-
const logContent = tailText(flowOutput,
|
|
338
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
354
|
+
const logContent = tailText(flowOutput, msgBudget);
|
|
355
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", logContent)}`, 0, 0));
|
|
339
356
|
} else if (streamingText) {
|
|
340
|
-
const logContent = tailText(streamingText,
|
|
341
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
357
|
+
const logContent = tailText(streamingText, msgBudget);
|
|
358
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", logContent)}`, 0, 0));
|
|
342
359
|
} else if (error && r.errorMessage) {
|
|
343
|
-
const logContent = truncateChars(r.errorMessage,
|
|
344
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
360
|
+
const logContent = truncateChars(r.errorMessage, msgBudget);
|
|
361
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("error", logContent)}`, 0, 0));
|
|
345
362
|
} else {
|
|
346
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
363
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", "[n/a]")}`, 0, 0));
|
|
347
364
|
}
|
|
348
365
|
|
|
349
366
|
return container;
|
|
@@ -434,7 +451,7 @@ function renderActivityPanel(
|
|
|
434
451
|
for (let i = 0; i < results.length; i++) {
|
|
435
452
|
const r = results[i];
|
|
436
453
|
const isLast = i === results.length - 1;
|
|
437
|
-
const stats = formatCompactStats(r.usage, r.model, maxWidth);
|
|
454
|
+
const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true });
|
|
438
455
|
const error = isFlowError(r);
|
|
439
456
|
const typeName = formatFlowTypeName(r.type);
|
|
440
457
|
|
|
@@ -451,30 +468,33 @@ function renderActivityPanel(
|
|
|
451
468
|
|
|
452
469
|
// aim: line (short headline)
|
|
453
470
|
if (r.aim) {
|
|
454
|
-
const
|
|
455
|
-
|
|
471
|
+
const aimPrefix = formatAimLinePrefix(indent + "├─", r);
|
|
472
|
+
const dirContent = truncateChars(r.aim, contentBudget(visibleLength(aimPrefix)));
|
|
473
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", aimPrefix)}${theme.fg("dim", dirContent)}`, 0, 0));
|
|
456
474
|
}
|
|
457
475
|
|
|
458
476
|
// act: line (last tool call with count)
|
|
459
477
|
const lastTool = getLastToolCall(r.messages);
|
|
460
478
|
if (lastTool) {
|
|
461
479
|
const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
462
|
-
const actPrefix =
|
|
463
|
-
const actContent = truncateChars(actStr, contentBudget(
|
|
464
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
480
|
+
const actPrefix = `${indent}├─ act: [${r.usage.toolCalls}] - `;
|
|
481
|
+
const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix)));
|
|
482
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${actContent}`, 0, 0));
|
|
465
483
|
}
|
|
466
484
|
|
|
467
485
|
// msg: line (live streaming text or last assistant text)
|
|
486
|
+
const msgPrefix = formatMsgLinePrefix(indent + "└─", r);
|
|
487
|
+
const msgBudget = contentBudget(visibleLength(msgPrefix));
|
|
468
488
|
const liveText = r.exitCode === -1 ? r.streamingText : undefined;
|
|
469
489
|
const lastText = liveText || getLastAssistantText(r.messages);
|
|
470
490
|
if (lastText) {
|
|
471
|
-
const logContent = tailText(lastText,
|
|
472
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
491
|
+
const logContent = tailText(lastText, msgBudget);
|
|
492
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", logContent)}`, 0, 0));
|
|
473
493
|
} else if (error && r.errorMessage) {
|
|
474
|
-
const logContent = truncateChars(r.errorMessage,
|
|
475
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
494
|
+
const logContent = truncateChars(r.errorMessage, msgBudget);
|
|
495
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("error", logContent)}`, 0, 0));
|
|
476
496
|
} else {
|
|
477
|
-
container.addChild(new TruncatedText(`${theme.fg("dim",
|
|
497
|
+
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", "[n/a]")}`, 0, 0));
|
|
478
498
|
}
|
|
479
499
|
|
|
480
500
|
// Add blank line separator between flows (with continuation pipe)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const AGENT_SESSION_MODES = ["fast", "default", "long"] as const;
|
|
2
|
+
|
|
3
|
+
export type AgentSessionMode = typeof AGENT_SESSION_MODES[number];
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_AGENT_SESSION_MODE: AgentSessionMode = "default";
|
|
6
|
+
export const MAX_AGENT_SESSION_TIMEOUT_MS = 900_000;
|
|
7
|
+
export const PI_FLOW_SESSION_MODE_ENV = "PI_FLOW_SESSION_MODE";
|
|
8
|
+
|
|
9
|
+
export const AGENT_SESSION_TIMEOUTS_MS: Record<AgentSessionMode, number> = {
|
|
10
|
+
fast: 300_000,
|
|
11
|
+
default: 600_000,
|
|
12
|
+
long: MAX_AGENT_SESSION_TIMEOUT_MS,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function parseAgentSessionMode(value: unknown): AgentSessionMode | undefined {
|
|
16
|
+
if (typeof value !== "string") return undefined;
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
return (AGENT_SESSION_MODES as readonly string[]).includes(normalized)
|
|
19
|
+
? normalized as AgentSessionMode
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveAgentSessionMode(
|
|
24
|
+
value: unknown,
|
|
25
|
+
fallback: AgentSessionMode = DEFAULT_AGENT_SESSION_MODE,
|
|
26
|
+
): AgentSessionMode {
|
|
27
|
+
return parseAgentSessionMode(value) ?? fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getAgentSessionTimeoutMs(mode: AgentSessionMode): number {
|
|
31
|
+
return AGENT_SESSION_TIMEOUTS_MS[mode];
|
|
32
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -122,6 +122,10 @@ export interface SingleResult {
|
|
|
122
122
|
stopReason?: string;
|
|
123
123
|
errorMessage?: string;
|
|
124
124
|
sawAgentEnd?: boolean;
|
|
125
|
+
/** Epoch ms when flow execution started; used for live countdown rendering. */
|
|
126
|
+
startedAtMs?: number;
|
|
127
|
+
/** Epoch ms when the flow hard timeout occurs; used for live countdown rendering. */
|
|
128
|
+
deadlineAtMs?: number;
|
|
125
129
|
/** Live in-progress text for status rendering; not part of the final flow report. */
|
|
126
130
|
streamingText?: string;
|
|
127
131
|
/** Structured JSON output parsed from the flow's final response. */
|