pi-agent-flow 1.4.3 → 1.4.4
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 +62 -2
- package/package.json +1 -1
- package/src/cli-args.ts +21 -0
- package/src/config.ts +57 -6
- package/src/executor.ts +38 -11
- package/src/flow.ts +35 -16
- package/src/index.ts +103 -16
- package/src/render-utils.ts +11 -0
- package/src/render.ts +47 -27
- 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
|
|
@@ -90,6 +91,33 @@ 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
|
+
### Session modes
|
|
95
|
+
|
|
96
|
+
Each flow call may set `sessionMode` to choose the child-agent time budget:
|
|
97
|
+
|
|
98
|
+
| Mode | Budget | Recommended use |
|
|
99
|
+
|------|-------:|-----------------|
|
|
100
|
+
| `fast` | 300s | quick scouting, narrow checks, small design passes |
|
|
101
|
+
| `default` | 600s | normal flow work; this is the default |
|
|
102
|
+
| `long` | 900s | large builds, full test runs, broad refactors, complex debugging |
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"flow": [
|
|
109
|
+
{
|
|
110
|
+
"type": "build",
|
|
111
|
+
"intent": "Run the full test suite and fix failures",
|
|
112
|
+
"aim": "Fix failing tests",
|
|
113
|
+
"sessionMode": "long"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The public interface is mode-based; arbitrary per-flow numeric timeouts are not exposed.
|
|
120
|
+
|
|
93
121
|
---
|
|
94
122
|
|
|
95
123
|
## Flow Definitions
|
|
@@ -259,6 +287,36 @@ Use `flowModelConfigs` in your Pi settings to define tiered model strategies. Ea
|
|
|
259
287
|
|
|
260
288
|
Settings are merged: project `.pi/settings.json` overrides global `~/.pi/agent/settings.json`.
|
|
261
289
|
|
|
290
|
+
Switch the global active strategy quickly with `--flow-mode`:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
pi --flow-mode balance
|
|
294
|
+
pi --flow-mode quality
|
|
295
|
+
pi --flow-mode mimo
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`--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.
|
|
299
|
+
|
|
300
|
+
You can also set flow runtime defaults under `flowSettings`:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"flowSettings": {
|
|
305
|
+
"sessionMode": "default",
|
|
306
|
+
"maxConcurrency": 4,
|
|
307
|
+
"toolOptimize": true,
|
|
308
|
+
"structuredOutput": true,
|
|
309
|
+
"autoTransition": false
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`flowSettings.sessionMode` accepts `fast`, `default`, or `long`. Session mode precedence is:
|
|
315
|
+
|
|
316
|
+
```txt
|
|
317
|
+
per-flow sessionMode > --flow-session-mode > PI_FLOW_SESSION_MODE > flowSettings.sessionMode > default
|
|
318
|
+
```
|
|
319
|
+
|
|
262
320
|
### Flags
|
|
263
321
|
|
|
264
322
|
| Flag | Description | Default |
|
|
@@ -266,10 +324,12 @@ Settings are merged: project `.pi/settings.json` overrides global `~/.pi/agent/s
|
|
|
266
324
|
| `--flow-max-depth [n]` | Maximum delegation depth | `3` |
|
|
267
325
|
| `--flow-prevent-cycles` | Block cyclic delegation | `true` |
|
|
268
326
|
| `--no-flow-prevent-cycles` | Disable cycle prevention | — |
|
|
269
|
-
| `--flow-model-config [name]` | Select a named model strategy | `balance` |
|
|
327
|
+
| `--flow-model-config [name]` | Select a named model strategy for this invocation | `balance` |
|
|
328
|
+
| `--flow-mode [name]` | Persistently switch the global model strategy and apply it immediately | — |
|
|
270
329
|
| `--flow-lite-model [model]` | Override the lite-tier model | — |
|
|
271
330
|
| `--flow-flash-model [model]` | Override the flash-tier model | — |
|
|
272
331
|
| `--flow-full-model [model]` | Override the full-tier model | — |
|
|
332
|
+
| `--flow-session-mode [mode]` | Default child-flow session mode: `fast`, `default`, or `long` | `default` |
|
|
273
333
|
| `--tool-optimize` | Use unified `batch`/`batch_read` instead of separate read/write/edit | `true` |
|
|
274
334
|
| `--no-tool-optimize` | Disable tool optimization; use legacy read/write/edit tools | — |
|
|
275
335
|
|
|
@@ -282,7 +342,7 @@ Settings are merged: project `.pi/settings.json` overrides global `~/.pi/agent/s
|
|
|
282
342
|
| `PI_FLOW_STACK` | JSON array of ancestor flow names |
|
|
283
343
|
| `PI_FLOW_PREVENT_CYCLES` | `"1"` or `"0"` |
|
|
284
344
|
| `PI_FLOW_TOOL_OPTIMIZE` | `"1"` or `"0"` (overrides default tool optimization) |
|
|
285
|
-
| `
|
|
345
|
+
| `PI_FLOW_SESSION_MODE` | Default child-flow session mode: `fast`, `default`, or `long` |
|
|
286
346
|
|
|
287
347
|
---
|
|
288
348
|
|
package/package.json
CHANGED
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,6 +8,7 @@
|
|
|
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";
|
|
11
12
|
|
|
12
13
|
export type FlowTier = "lite" | "flash" | "full";
|
|
13
14
|
|
|
@@ -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 };
|
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,11 @@ 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,
|
|
17
20
|
type LoadedFlowModelConfigs,
|
|
18
21
|
} from "./config.js";
|
|
19
22
|
import { getInheritedCliArgs } from "./cli-args.js";
|
|
@@ -55,6 +58,12 @@ import {
|
|
|
55
58
|
} from "./sliding-prompt.js";
|
|
56
59
|
import { DEFAULT_TRANSITIONS, buildTransitionHooks } from "./transitions.js";
|
|
57
60
|
import { createTimedBashToolDefinition } from "./timed-bash.js";
|
|
61
|
+
import {
|
|
62
|
+
DEFAULT_AGENT_SESSION_MODE,
|
|
63
|
+
PI_FLOW_SESSION_MODE_ENV,
|
|
64
|
+
parseAgentSessionMode,
|
|
65
|
+
type AgentSessionMode,
|
|
66
|
+
} from "./session-mode.js";
|
|
58
67
|
|
|
59
68
|
// ---------------------------------------------------------------------------
|
|
60
69
|
// Limits
|
|
@@ -85,13 +94,23 @@ const FlowItem = Type.Object({
|
|
|
85
94
|
cwd: Type.Optional(
|
|
86
95
|
Type.String({ description: "Working directory override for this flow." }),
|
|
87
96
|
),
|
|
97
|
+
sessionMode: Type.Optional(
|
|
98
|
+
Type.Union([
|
|
99
|
+
Type.Literal("fast"),
|
|
100
|
+
Type.Literal("default"),
|
|
101
|
+
Type.Literal("long"),
|
|
102
|
+
], {
|
|
103
|
+
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.",
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
88
106
|
});
|
|
89
107
|
|
|
90
108
|
const FlowParams = Type.Object({
|
|
91
109
|
flow: Type.Array(FlowItem, {
|
|
92
110
|
description:
|
|
93
111
|
"Array of flow tasks to execute. Each runs in its own forked process. " +
|
|
94
|
-
|
|
112
|
+
"Optional sessionMode selects the child-agent budget: fast=300s, default=600s, long=900s. " +
|
|
113
|
+
'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
114
|
minItems: 1,
|
|
96
115
|
}),
|
|
97
116
|
confirmProjectFlows: Type.Optional(
|
|
@@ -634,6 +653,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
634
653
|
description: "Named flow model strategy from settings.json flowModelConfigs.",
|
|
635
654
|
type: "string",
|
|
636
655
|
});
|
|
656
|
+
pi.registerFlag("flow-mode", {
|
|
657
|
+
description: "Persistently switch the global flow model strategy in ~/.pi/agent/settings.json.",
|
|
658
|
+
type: "string",
|
|
659
|
+
});
|
|
637
660
|
pi.registerFlag("flow-lite-model", {
|
|
638
661
|
description: "Model for lite-tier flows (scout, debug).",
|
|
639
662
|
type: "string",
|
|
@@ -650,6 +673,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
650
673
|
description: "Maximum number of flows to execute in parallel (default: 4).",
|
|
651
674
|
type: "string",
|
|
652
675
|
});
|
|
676
|
+
pi.registerFlag("flow-session-mode", {
|
|
677
|
+
description: "Default child-flow session mode: fast (300s), default (600s), or long (900s).",
|
|
678
|
+
type: "string",
|
|
679
|
+
});
|
|
653
680
|
pi.registerFlag("auto-transition", {
|
|
654
681
|
description: "Automatically queue follow-up flows based on hook transitions (default: false).",
|
|
655
682
|
type: "boolean",
|
|
@@ -669,6 +696,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
669
696
|
let structuredOutput = true;
|
|
670
697
|
let maxConcurrency = 4;
|
|
671
698
|
let autoTransition = false;
|
|
699
|
+
let defaultSessionMode: AgentSessionMode = DEFAULT_AGENT_SESSION_MODE;
|
|
672
700
|
const envToolOptimize = process.env[FLOW_TOOL_OPTIMIZE_ENV];
|
|
673
701
|
if (envToolOptimize !== undefined) {
|
|
674
702
|
const parsed = parseBoolean(envToolOptimize);
|
|
@@ -681,39 +709,96 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
709
|
configs: { default: {} },
|
|
682
710
|
strategy: {},
|
|
683
711
|
};
|
|
712
|
+
let activeRuntimeFlowMode: string | undefined;
|
|
684
713
|
|
|
685
714
|
// Auto-discover flows on session start
|
|
686
715
|
pi.on("session_start", async (_event, ctx) => {
|
|
687
716
|
const discovery = discoverFlows(ctx.cwd, "all");
|
|
688
717
|
discoveredFlows = discovery.flows;
|
|
689
718
|
loadedFlowModelConfigs = loadFlowModelConfigs(ctx.cwd);
|
|
719
|
+
activeRuntimeFlowMode = undefined;
|
|
720
|
+
|
|
721
|
+
const requestedFlowMode = normalizeFlowModeName(pi.getFlag("flow-mode"));
|
|
722
|
+
if (requestedFlowMode !== undefined) {
|
|
723
|
+
if (!Object.prototype.hasOwnProperty.call(loadedFlowModelConfigs.configs, requestedFlowMode)) {
|
|
724
|
+
const availableModes = Object.keys(loadedFlowModelConfigs.configs).sort().join(", ") || "(none)";
|
|
725
|
+
console.warn(
|
|
726
|
+
`[pi-agent-flow] Cannot switch flow mode to "${requestedFlowMode}"; no flowModelConfigs.${requestedFlowMode} strategy was found. Available modes: ${availableModes}.`,
|
|
727
|
+
);
|
|
728
|
+
} else {
|
|
729
|
+
try {
|
|
730
|
+
const writeResult = writeGlobalFlowMode(requestedFlowMode);
|
|
731
|
+
console.warn(`[pi-agent-flow] Flow mode switched to "${requestedFlowMode}" in ${writeResult.path}.`);
|
|
732
|
+
} catch (error) {
|
|
733
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
734
|
+
console.warn(`[pi-agent-flow] ${message}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const projectFlowModelConfig = loadProjectFlowModelConfigName(ctx.cwd);
|
|
738
|
+
if (projectFlowModelConfig !== undefined && projectFlowModelConfig !== requestedFlowMode) {
|
|
739
|
+
console.warn(
|
|
740
|
+
`[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.`,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
activeRuntimeFlowMode = requestedFlowMode;
|
|
745
|
+
loadedFlowModelConfigs = selectFlowModelStrategy(loadedFlowModelConfigs.configs, requestedFlowMode);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
690
748
|
|
|
691
749
|
// Register declarative transition hooks from the transition matrix
|
|
692
750
|
for (const hook of buildTransitionHooks(DEFAULT_TRANSITIONS)) {
|
|
693
751
|
registerHook(hook);
|
|
694
752
|
}
|
|
695
753
|
|
|
754
|
+
const flowSettings = loadFlowSettings(ctx.cwd);
|
|
755
|
+
if (typeof flowSettings.structuredOutput === "boolean") {
|
|
756
|
+
structuredOutput = flowSettings.structuredOutput;
|
|
757
|
+
}
|
|
758
|
+
if (typeof flowSettings.maxConcurrency === "number") {
|
|
759
|
+
maxConcurrency = flowSettings.maxConcurrency;
|
|
760
|
+
}
|
|
761
|
+
if (typeof flowSettings.autoTransition === "boolean") {
|
|
762
|
+
autoTransition = flowSettings.autoTransition;
|
|
763
|
+
}
|
|
764
|
+
|
|
696
765
|
// Resolve toolOptimize: CLI flag > env var > settings.json > default
|
|
697
766
|
const cliFlag = pi.getFlag("tool-optimize");
|
|
767
|
+
if (typeof flowSettings.toolOptimize === "boolean") {
|
|
768
|
+
toolOptimize = flowSettings.toolOptimize;
|
|
769
|
+
}
|
|
770
|
+
if (envToolOptimize !== undefined) {
|
|
771
|
+
const parsed = parseBoolean(envToolOptimize);
|
|
772
|
+
if (parsed !== null) toolOptimize = parsed;
|
|
773
|
+
}
|
|
698
774
|
if (typeof cliFlag === "boolean") {
|
|
699
775
|
toolOptimize = cliFlag;
|
|
700
776
|
} else if (typeof cliFlag === "string") {
|
|
701
777
|
const parsed = parseBoolean(cliFlag);
|
|
702
778
|
if (parsed !== null) toolOptimize = parsed;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Resolve sessionMode: CLI flag > env var > settings.json > default
|
|
782
|
+
defaultSessionMode = flowSettings.sessionMode ?? DEFAULT_AGENT_SESSION_MODE;
|
|
783
|
+
const envSessionModeRaw = process.env[PI_FLOW_SESSION_MODE_ENV];
|
|
784
|
+
if (envSessionModeRaw !== undefined) {
|
|
785
|
+
const envSessionMode = parseAgentSessionMode(envSessionModeRaw);
|
|
786
|
+
if (envSessionMode !== undefined) {
|
|
787
|
+
defaultSessionMode = envSessionMode;
|
|
788
|
+
} else {
|
|
789
|
+
console.warn(`[pi-agent-flow] Ignoring invalid ${PI_FLOW_SESSION_MODE_ENV}="${envSessionModeRaw}". Expected fast, default, or long.`);
|
|
713
790
|
}
|
|
714
|
-
|
|
715
|
-
|
|
791
|
+
}
|
|
792
|
+
const cliSessionModeRaw = pi.getFlag("flow-session-mode");
|
|
793
|
+
if (typeof cliSessionModeRaw === "string") {
|
|
794
|
+
const cliSessionMode = parseAgentSessionMode(cliSessionModeRaw);
|
|
795
|
+
if (cliSessionMode !== undefined) {
|
|
796
|
+
defaultSessionMode = cliSessionMode;
|
|
797
|
+
} else {
|
|
798
|
+
console.warn(`[pi-agent-flow] Ignoring invalid --flow-session-mode value "${cliSessionModeRaw}". Expected fast, default, or long.`);
|
|
716
799
|
}
|
|
800
|
+
} else if (inheritedCliArgs.flowSessionMode !== undefined) {
|
|
801
|
+
defaultSessionMode = inheritedCliArgs.flowSessionMode;
|
|
717
802
|
}
|
|
718
803
|
|
|
719
804
|
// Resolve maxConcurrency: CLI flag > env var > settings.json > default
|
|
@@ -908,7 +993,8 @@ flow [type] accomplished
|
|
|
908
993
|
"You MUST enter to the following flow states, with tool call method.",
|
|
909
994
|
"",
|
|
910
995
|
"Flow states are isolated \u03c0 processes with forked session snapshots. They run in parallel.",
|
|
911
|
-
'Invoke: { "flow": [{ "type": "scout", "intent": "..." }, ...] }',
|
|
996
|
+
'Invoke: { "flow": [{ "type": "scout", "intent": "...", "aim": "...", "sessionMode": "default" }, ...] }',
|
|
997
|
+
"Session modes: fast=300s, default=600s, long=900s. Use long only when the work genuinely needs the larger budget.",
|
|
912
998
|
"States: scout, debug, build, craft, audit, ideas.",
|
|
913
999
|
"Custom states configs in (create if not exists): .md files in .pi/agents/ or ~/.pi/agent/agents/.",
|
|
914
1000
|
].join("\n"),
|
|
@@ -953,10 +1039,11 @@ flow [type] accomplished
|
|
|
953
1039
|
loadedFlowModelConfigs,
|
|
954
1040
|
maxConcurrency,
|
|
955
1041
|
autoTransition,
|
|
1042
|
+
defaultSessionMode,
|
|
956
1043
|
signal,
|
|
957
1044
|
onUpdate,
|
|
958
1045
|
makeDetails,
|
|
959
|
-
getFlag: (name: string) => pi.getFlag(name),
|
|
1046
|
+
getFlag: (name: string) => name === "flow-mode" ? activeRuntimeFlowMode : pi.getFlag(name),
|
|
960
1047
|
tierOverrideResolver: getTierOverride,
|
|
961
1048
|
fallbackModel: inheritedCliArgs.fallbackModel,
|
|
962
1049
|
forkSessionSnapshotJsonl,
|
|
@@ -968,7 +1055,7 @@ flow [type] accomplished
|
|
|
968
1055
|
onFlowMetrics: (metrics) => { if (typeof pi.emit === "function") pi.emit("pi-agent-flow:complete", metrics); },
|
|
969
1056
|
confirmProjectFlows: params.confirmProjectFlows,
|
|
970
1057
|
},
|
|
971
|
-
params.flow.map((f: any) => ({ type: f.type, intent: f.intent, aim: f.aim, cwd: f.cwd })),
|
|
1058
|
+
params.flow.map((f: any) => ({ type: f.type, intent: f.intent, aim: f.aim, cwd: f.cwd, sessionMode: f.sessionMode })),
|
|
972
1059
|
toolCallId,
|
|
973
1060
|
);
|
|
974
1061
|
|
package/src/render-utils.ts
CHANGED
|
@@ -43,6 +43,10 @@ function formatTps(value: number | undefined): string {
|
|
|
43
43
|
return value.toFixed(1).padStart(5);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function formatCompactTokenPair(usage: Partial<UsageStats>): string {
|
|
47
|
+
return `↑ ${formatFixedTokens(usage.input || 0)} · ↓ ${formatFixedTokens(usage.output || 0)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
export function formatCompactStats(usage: Partial<UsageStats>, model?: string, maxWidth?: number): string {
|
|
47
51
|
const parts: string[] = [];
|
|
48
52
|
parts.push(`↑ ${formatFixedTokens(usage.input || 0)}`);
|
|
@@ -72,6 +76,13 @@ export function formatCompactStats(usage: Partial<UsageStats>, model?: string, m
|
|
|
72
76
|
return result;
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
export function formatCountdown(ms: number): string {
|
|
80
|
+
const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
|
|
81
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
82
|
+
const seconds = totalSeconds % 60;
|
|
83
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
/** Regex matching ANSI escape sequences. */
|
|
76
87
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
77
88
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -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;
|
|
@@ -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. */
|