pi-agent-flow 1.4.2 → 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/ambient.d.ts +4 -0
- package/src/cli-args.ts +21 -0
- package/src/config.ts +57 -6
- package/src/executor.ts +38 -11
- package/src/flow.ts +51 -17
- 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/timed-bash.ts +113 -8
- 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/ambient.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ declare module "@mariozechner/pi-coding-agent" {
|
|
|
46
46
|
renderCall?: (...args: any[]) => any;
|
|
47
47
|
renderResult?: (...args: any[]) => any;
|
|
48
48
|
};
|
|
49
|
+
/** Test-only exports provided by tests/__mocks__/pi-coding-agent.ts. */
|
|
50
|
+
export const bashToolExecuteCalls: any[][];
|
|
51
|
+
export function __setBashToolExecuteImpl(fn: (...args: any[]) => Promise<any>): void;
|
|
52
|
+
export function __resetBashToolMock(): void;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
declare module "@mariozechner/pi-tui" {
|
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,20 +21,21 @@ 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
|
|
32
|
+
const FLOW_TOOL_SUMMARY_GRACE_MS = FLOW_FINAL_URGE_MS; // bash/tool abort lead time so the agent can summarize
|
|
32
33
|
const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
33
34
|
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
34
35
|
const FLOW_STACK_ENV = "PI_FLOW_STACK";
|
|
35
36
|
const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
36
|
-
const FLOW_TIMEOUT_ENV = "PI_FLOW_TIMEOUT_MS";
|
|
37
37
|
const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
38
|
+
const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
|
|
38
39
|
export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
|
|
39
40
|
const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
40
41
|
|
|
@@ -144,7 +145,8 @@ function buildFlowArgs(
|
|
|
144
145
|
maxDepth: number = 0,
|
|
145
146
|
toolOptimize: boolean = false,
|
|
146
147
|
structuredOutput: boolean = true,
|
|
147
|
-
|
|
148
|
+
sessionMode: AgentSessionMode = DEFAULT_AGENT_SESSION_MODE,
|
|
149
|
+
sessionTimeoutMs: number = getAgentSessionTimeoutMs(sessionMode),
|
|
148
150
|
): string[] {
|
|
149
151
|
const args: string[] = [
|
|
150
152
|
"--mode",
|
|
@@ -161,6 +163,9 @@ function buildFlowArgs(
|
|
|
161
163
|
if (inheritedCliArgs.flowModelConfig) {
|
|
162
164
|
args.push("--flow-model-config", inheritedCliArgs.flowModelConfig);
|
|
163
165
|
}
|
|
166
|
+
if (inheritedCliArgs.flowSessionMode) {
|
|
167
|
+
args.push("--flow-session-mode", inheritedCliArgs.flowSessionMode);
|
|
168
|
+
}
|
|
164
169
|
if (inheritedCliArgs.tieredModels?.lite) {
|
|
165
170
|
args.push("--flow-lite-model", inheritedCliArgs.tieredModels.lite);
|
|
166
171
|
}
|
|
@@ -216,8 +221,8 @@ function buildFlowArgs(
|
|
|
216
221
|
: `You may NOT delegate to sub-flows (depth limit reached).`;
|
|
217
222
|
|
|
218
223
|
const timeBudgetHint =
|
|
219
|
-
|
|
220
|
-
? `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`
|
|
221
226
|
: "";
|
|
222
227
|
|
|
223
228
|
const activation =
|
|
@@ -322,8 +327,8 @@ export interface RunFlowOptions {
|
|
|
322
327
|
onUpdate?: FlowUpdateCallback;
|
|
323
328
|
/** Factory to wrap results into FlowDetails. */
|
|
324
329
|
makeDetails: (results: SingleResult[]) => FlowDetails;
|
|
325
|
-
/**
|
|
326
|
-
|
|
330
|
+
/** Child-flow session mode. Default: "default" (600s). */
|
|
331
|
+
sessionMode?: AgentSessionMode;
|
|
327
332
|
}
|
|
328
333
|
|
|
329
334
|
/**
|
|
@@ -368,6 +373,10 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
368
373
|
};
|
|
369
374
|
}
|
|
370
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;
|
|
371
380
|
const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
|
|
372
381
|
const result: SingleResult = {
|
|
373
382
|
type: normalizedFlowName,
|
|
@@ -379,6 +388,8 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
379
388
|
stderr: "",
|
|
380
389
|
usage: emptyFlowUsage(),
|
|
381
390
|
model: resolvedModel,
|
|
391
|
+
startedAtMs,
|
|
392
|
+
...(deadlineAtMs !== undefined ? { deadlineAtMs } : {}),
|
|
382
393
|
};
|
|
383
394
|
|
|
384
395
|
let liveStreamingText = "";
|
|
@@ -417,14 +428,6 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
417
428
|
forkSessionTmpPath = forkTmp.filePath;
|
|
418
429
|
}
|
|
419
430
|
|
|
420
|
-
// Resolve timeout: explicit option > env var > default (10 min)
|
|
421
|
-
const envTimeoutRaw = process.env[FLOW_TIMEOUT_ENV];
|
|
422
|
-
const envTimeout = envTimeoutRaw !== undefined ? (() => {
|
|
423
|
-
const n = Number(envTimeoutRaw);
|
|
424
|
-
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
425
|
-
})() : null;
|
|
426
|
-
const effectiveTimeout = opts.timeoutMs ?? envTimeout ?? DEFAULT_FLOW_TIMEOUT_MS;
|
|
427
|
-
|
|
428
431
|
try {
|
|
429
432
|
const piArgs = buildFlowArgs(
|
|
430
433
|
flow,
|
|
@@ -435,6 +438,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
435
438
|
maxDepth,
|
|
436
439
|
toolOptimize,
|
|
437
440
|
structuredOutput,
|
|
441
|
+
effectiveSessionMode,
|
|
438
442
|
effectiveTimeout,
|
|
439
443
|
);
|
|
440
444
|
let wasAborted = false;
|
|
@@ -443,6 +447,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
443
447
|
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
444
448
|
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
445
449
|
const propagatedStack = [...parentFlowStack, normalizedFlowName];
|
|
450
|
+
const proportionalGraceMs = Math.floor(effectiveTimeout * 0.1);
|
|
451
|
+
const minimumGraceMs = effectiveTimeout >= 10_000 ? 1_000 : Math.floor(effectiveTimeout / 2);
|
|
452
|
+
const toolSummaryGraceMs = Math.min(
|
|
453
|
+
FLOW_TOOL_SUMMARY_GRACE_MS,
|
|
454
|
+
Math.max(0, effectiveTimeout),
|
|
455
|
+
Math.max(minimumGraceMs, proportionalGraceMs),
|
|
456
|
+
);
|
|
446
457
|
const { command, prefixArgs } = resolveFlowSpawn();
|
|
447
458
|
const proc = spawn(command, [...prefixArgs, ...piArgs], {
|
|
448
459
|
cwd: taskCwd ?? cwd,
|
|
@@ -458,7 +469,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
458
469
|
[FLOW_PREVENT_CYCLES_ENV]: preventCycles ? "1" : "0",
|
|
459
470
|
[FLOW_TOOL_OPTIMIZE_ENV]: toolOptimize ? "1" : "0",
|
|
460
471
|
[PI_OFFLINE_ENV]: "1",
|
|
461
|
-
|
|
472
|
+
...(effectiveTimeout > 0 ? {
|
|
473
|
+
[FLOW_DEADLINE_ENV]: String(deadlineAtMs),
|
|
474
|
+
[FLOW_TOOL_SUMMARY_GRACE_ENV]: String(toolSummaryGraceMs),
|
|
475
|
+
} : {
|
|
476
|
+
[FLOW_DEADLINE_ENV]: "",
|
|
477
|
+
[FLOW_TOOL_SUMMARY_GRACE_ENV]: "0",
|
|
478
|
+
}),
|
|
462
479
|
},
|
|
463
480
|
});
|
|
464
481
|
|
|
@@ -479,6 +496,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
479
496
|
let settled = false;
|
|
480
497
|
let timeoutFired = false;
|
|
481
498
|
let semanticCompletionTimer: NodeJS.Timeout | undefined;
|
|
499
|
+
let countdownTimer: NodeJS.Timeout | undefined;
|
|
482
500
|
|
|
483
501
|
const clearSemanticCompletionTimer = () => {
|
|
484
502
|
if (semanticCompletionTimer) {
|
|
@@ -487,6 +505,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
487
505
|
}
|
|
488
506
|
};
|
|
489
507
|
|
|
508
|
+
const clearCountdownTimer = () => {
|
|
509
|
+
if (countdownTimer) {
|
|
510
|
+
clearInterval(countdownTimer);
|
|
511
|
+
countdownTimer = undefined;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
490
515
|
const terminateChild = () => {
|
|
491
516
|
endStdin();
|
|
492
517
|
if (isWindows) {
|
|
@@ -514,6 +539,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
514
539
|
settled = true;
|
|
515
540
|
endStdin();
|
|
516
541
|
clearSemanticCompletionTimer();
|
|
542
|
+
clearCountdownTimer();
|
|
517
543
|
if (signal && abortHandler) {
|
|
518
544
|
signal.removeEventListener("abort", abortHandler);
|
|
519
545
|
}
|
|
@@ -560,6 +586,14 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
560
586
|
proc.stdout.on("data", onStdoutData);
|
|
561
587
|
proc.stderr.on("data", onStderrData);
|
|
562
588
|
|
|
589
|
+
if (onUpdate && effectiveTimeout > 0) {
|
|
590
|
+
countdownTimer = setInterval(() => {
|
|
591
|
+
if (didClose || settled) return;
|
|
592
|
+
emitUpdate();
|
|
593
|
+
}, 1000);
|
|
594
|
+
countdownTimer.unref();
|
|
595
|
+
}
|
|
596
|
+
|
|
563
597
|
proc.on("close", (code) => {
|
|
564
598
|
didClose = true;
|
|
565
599
|
if (buffer.trim()) flushBufferedLines(buffer);
|
|
@@ -583,7 +617,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
583
617
|
else signal.addEventListener("abort", abortHandler, { once: true });
|
|
584
618
|
}
|
|
585
619
|
|
|
586
|
-
// Execution timeout — two-stage:
|
|
620
|
+
// Execution timeout — two-stage: parent-side warnings, tool-level deadline abort, then grace, then hard kill
|
|
587
621
|
if (effectiveTimeout > 0) {
|
|
588
622
|
// Warning timer: notify the parent UI that the child is about to be killed.
|
|
589
623
|
// NOTE: True mid-flight injection into the child's context requires pi-core
|
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/timed-bash.ts
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* Wraps the built-in bash tool to append execution-time classification
|
|
5
5
|
* to every result. This gives the LLM concrete feedback to self-correct
|
|
6
6
|
* strategy (e.g. switch from bash grep to grep tool, batch commands, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Child flows also receive a hard deadline from the parent runner. When a
|
|
9
|
+
* bash command is still running near that deadline, this wrapper aborts just
|
|
10
|
+
* the bash tool and returns an explicit instruction to stop using tools and
|
|
11
|
+
* summarize. That preserves the child agent process long enough to produce
|
|
12
|
+
* its final structured report instead of being killed while a shell command is
|
|
13
|
+
* still active.
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
16
|
import { createBashToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
@@ -21,6 +28,10 @@ export interface TimingReport {
|
|
|
21
28
|
label: string;
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
32
|
+
const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
|
|
33
|
+
const DEFAULT_FLOW_TOOL_SUMMARY_GRACE_MS = 30_000;
|
|
34
|
+
|
|
24
35
|
/** Classify duration into user-defined tiers with actionable feedback. */
|
|
25
36
|
export function classifyDuration(ms: number): TimingReport {
|
|
26
37
|
const s = ms / 1000;
|
|
@@ -56,6 +67,90 @@ export function formatTimingAppendix(report: TimingReport): string {
|
|
|
56
67
|
return `\n\n[Execution time: ${report.label}]`;
|
|
57
68
|
}
|
|
58
69
|
|
|
70
|
+
function parsePositiveSafeInteger(raw: unknown): number | null {
|
|
71
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
72
|
+
const parsed = Number(raw);
|
|
73
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseNonNegativeSafeInteger(raw: unknown): number | null {
|
|
77
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
78
|
+
const parsed = Number(raw);
|
|
79
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getFlowDeadlineMs(): number | null {
|
|
83
|
+
return parsePositiveSafeInteger(process.env[FLOW_DEADLINE_ENV]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getFlowToolSummaryGraceMs(): number {
|
|
87
|
+
return parseNonNegativeSafeInteger(process.env[FLOW_TOOL_SUMMARY_GRACE_ENV]) ?? DEFAULT_FLOW_TOOL_SUMMARY_GRACE_MS;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatDeadlineAppendix(): string {
|
|
91
|
+
return "\n\n[Flow timeout] Bash command was interrupted to preserve time for the final flow summary. Stop running tools and return structured findings now.";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function appendTextToToolResult(result: any, text: string): void {
|
|
95
|
+
const textItem = result?.content?.find?.((c: any) => c.type === "text");
|
|
96
|
+
if (textItem && typeof textItem.text === "string") {
|
|
97
|
+
textItem.text += text;
|
|
98
|
+
} else if (Array.isArray(result?.content)) {
|
|
99
|
+
result.content.push({ type: "text", text: text.trim() });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createDeadlineSignal(parentSignal: AbortSignal | undefined): {
|
|
104
|
+
signal: AbortSignal | undefined;
|
|
105
|
+
cleanup: () => void;
|
|
106
|
+
wasDeadlineAbort: () => boolean;
|
|
107
|
+
} {
|
|
108
|
+
const deadlineMs = getFlowDeadlineMs();
|
|
109
|
+
if (!deadlineMs) {
|
|
110
|
+
return { signal: parentSignal, cleanup: () => undefined, wasDeadlineAbort: () => false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const summaryGraceMs = getFlowToolSummaryGraceMs();
|
|
114
|
+
const abortAtMs = deadlineMs - summaryGraceMs;
|
|
115
|
+
const delayMs = abortAtMs - Date.now();
|
|
116
|
+
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
let deadlineAbort = false;
|
|
119
|
+
let timer: NodeJS.Timeout | undefined;
|
|
120
|
+
let relayParentAbort: (() => void) | undefined;
|
|
121
|
+
|
|
122
|
+
const abortForDeadline = () => {
|
|
123
|
+
if (controller.signal.aborted) return;
|
|
124
|
+
deadlineAbort = true;
|
|
125
|
+
controller.abort(new Error("Flow deadline reached while bash command was running."));
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (parentSignal?.aborted) {
|
|
129
|
+
controller.abort(parentSignal.reason);
|
|
130
|
+
} else if (parentSignal) {
|
|
131
|
+
relayParentAbort = () => controller.abort(parentSignal.reason);
|
|
132
|
+
parentSignal.addEventListener("abort", relayParentAbort, { once: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (delayMs <= 0) {
|
|
136
|
+
abortForDeadline();
|
|
137
|
+
} else {
|
|
138
|
+
timer = setTimeout(abortForDeadline, delayMs);
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
signal: controller.signal,
|
|
144
|
+
cleanup: () => {
|
|
145
|
+
if (timer) clearTimeout(timer);
|
|
146
|
+
if (parentSignal && relayParentAbort) {
|
|
147
|
+
parentSignal.removeEventListener("abort", relayParentAbort);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
wasDeadlineAbort: () => deadlineAbort,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
59
154
|
/**
|
|
60
155
|
* Create a timed bash tool definition that wraps the built-in one.
|
|
61
156
|
* Extensions override built-in tools by name, so this replaces the
|
|
@@ -93,11 +188,12 @@ export function createTimedBashToolDefinition(
|
|
|
93
188
|
ctx: any,
|
|
94
189
|
) {
|
|
95
190
|
const start = Date.now();
|
|
191
|
+
const deadlineSignal = createDeadlineSignal(signal);
|
|
96
192
|
try {
|
|
97
193
|
const result = await original.execute(
|
|
98
194
|
toolCallId,
|
|
99
195
|
params,
|
|
100
|
-
signal,
|
|
196
|
+
deadlineSignal.signal,
|
|
101
197
|
onUpdate,
|
|
102
198
|
ctx,
|
|
103
199
|
);
|
|
@@ -105,13 +201,10 @@ export function createTimedBashToolDefinition(
|
|
|
105
201
|
const report = classifyDuration(duration);
|
|
106
202
|
const appendix = formatTimingAppendix(report);
|
|
107
203
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
textItem.text += appendix;
|
|
113
|
-
} else if (result.content) {
|
|
114
|
-
result.content.push({ type: "text", text: appendix.trim() });
|
|
204
|
+
appendTextToToolResult(result, appendix);
|
|
205
|
+
if (deadlineSignal.wasDeadlineAbort()) {
|
|
206
|
+
appendTextToToolResult(result, formatDeadlineAppendix());
|
|
207
|
+
result.isError = true;
|
|
115
208
|
}
|
|
116
209
|
return result;
|
|
117
210
|
} catch (err: any) {
|
|
@@ -119,10 +212,22 @@ export function createTimedBashToolDefinition(
|
|
|
119
212
|
const report = classifyDuration(duration);
|
|
120
213
|
const appendix = formatTimingAppendix(report);
|
|
121
214
|
|
|
215
|
+
if (deadlineSignal.wasDeadlineAbort()) {
|
|
216
|
+
const message = typeof err?.message === "string" && err.message.trim()
|
|
217
|
+
? `${err.message}${appendix}${formatDeadlineAppendix()}`
|
|
218
|
+
: `${appendix.trim()}${formatDeadlineAppendix()}`;
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: message }],
|
|
221
|
+
isError: true,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
122
225
|
if (err?.message && typeof err.message === "string") {
|
|
123
226
|
err.message += appendix;
|
|
124
227
|
}
|
|
125
228
|
throw err;
|
|
229
|
+
} finally {
|
|
230
|
+
deadlineSignal.cleanup();
|
|
126
231
|
}
|
|
127
232
|
},
|
|
128
233
|
};
|
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. */
|