pi-agent-flow 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/flow.ts +36 -8
package/package.json
CHANGED
package/src/flow.ts
CHANGED
|
@@ -26,11 +26,13 @@ const isWindows = process.platform === "win32";
|
|
|
26
26
|
const SIGKILL_TIMEOUT_MS = 5000;
|
|
27
27
|
const AGENT_END_GRACE_MS = 2000;
|
|
28
28
|
const DEFAULT_FLOW_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
29
|
+
const FLOW_TIME_BUDGET_WARNING_MS = 2 * 60 * 1000; // warn 2 min before kill
|
|
29
30
|
const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
30
31
|
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
31
32
|
const FLOW_STACK_ENV = "PI_FLOW_STACK";
|
|
32
33
|
const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
33
34
|
const FLOW_TIMEOUT_ENV = "PI_FLOW_TIMEOUT_MS";
|
|
35
|
+
const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
34
36
|
export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
|
|
35
37
|
const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
36
38
|
|
|
@@ -140,6 +142,7 @@ function buildFlowArgs(
|
|
|
140
142
|
maxDepth: number = 0,
|
|
141
143
|
toolOptimize: boolean = false,
|
|
142
144
|
structuredOutput: boolean = true,
|
|
145
|
+
timeoutMs?: number,
|
|
143
146
|
): string[] {
|
|
144
147
|
const args: string[] = [
|
|
145
148
|
"--mode",
|
|
@@ -210,11 +213,17 @@ function buildFlowArgs(
|
|
|
210
213
|
? `You may delegate to sub-flows (depth ${currentDepth}/${effectiveMaxDepth}).`
|
|
211
214
|
: `You may NOT delegate to sub-flows (depth limit reached).`;
|
|
212
215
|
|
|
216
|
+
const timeBudgetHint =
|
|
217
|
+
timeoutMs && timeoutMs > 0
|
|
218
|
+
? `Time budget: ${Math.round(timeoutMs / 1000)}s total. You will be hard-killed when time expires — plan and prioritize accordingly.\n`
|
|
219
|
+
: "";
|
|
220
|
+
|
|
213
221
|
const activation =
|
|
214
222
|
`\n\n<activation flow="${flow.name}" depth="${currentDepth}" tools="${availableTools}">\n` +
|
|
215
223
|
`You are a [${flow.name}] agent operating at depth ${currentDepth}.\n` +
|
|
216
224
|
`Available tools: ${availableTools}.\n` +
|
|
217
225
|
`${delegationRule}\n` +
|
|
226
|
+
`${timeBudgetHint}` +
|
|
218
227
|
`Do not attempt to use any tool outside the available set — it will fail.\n` +
|
|
219
228
|
`</activation>`;
|
|
220
229
|
|
|
@@ -406,6 +415,14 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
406
415
|
forkSessionTmpPath = forkTmp.filePath;
|
|
407
416
|
}
|
|
408
417
|
|
|
418
|
+
// Resolve timeout: explicit option > env var > default (10 min)
|
|
419
|
+
const envTimeoutRaw = process.env[FLOW_TIMEOUT_ENV];
|
|
420
|
+
const envTimeout = envTimeoutRaw !== undefined ? (() => {
|
|
421
|
+
const n = Number(envTimeoutRaw);
|
|
422
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
423
|
+
})() : null;
|
|
424
|
+
const effectiveTimeout = opts.timeoutMs ?? envTimeout ?? DEFAULT_FLOW_TIMEOUT_MS;
|
|
425
|
+
|
|
409
426
|
try {
|
|
410
427
|
const piArgs = buildFlowArgs(
|
|
411
428
|
flow,
|
|
@@ -416,17 +433,10 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
416
433
|
maxDepth,
|
|
417
434
|
toolOptimize,
|
|
418
435
|
structuredOutput,
|
|
436
|
+
effectiveTimeout,
|
|
419
437
|
);
|
|
420
438
|
let wasAborted = false;
|
|
421
439
|
|
|
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
440
|
const exitCode = await new Promise<number>((resolve) => {
|
|
431
441
|
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
432
442
|
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
@@ -446,6 +456,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
446
456
|
[FLOW_PREVENT_CYCLES_ENV]: preventCycles ? "1" : "0",
|
|
447
457
|
[FLOW_TOOL_OPTIMIZE_ENV]: toolOptimize ? "1" : "0",
|
|
448
458
|
[PI_OFFLINE_ENV]: "1",
|
|
459
|
+
[FLOW_DEADLINE_ENV]: String(Date.now() + effectiveTimeout),
|
|
449
460
|
},
|
|
450
461
|
});
|
|
451
462
|
|
|
@@ -562,6 +573,23 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
562
573
|
|
|
563
574
|
// Execution timeout — kill child if it runs too long
|
|
564
575
|
if (effectiveTimeout > 0) {
|
|
576
|
+
// Warning timer: notify the parent UI that the child is about to be killed.
|
|
577
|
+
// NOTE: True mid-flight injection into the child's context requires pi-core
|
|
578
|
+
// support for out-of-band messages. Until then, the agent only knows its
|
|
579
|
+
// budget from the initial prompt; this warning is parent-side only.
|
|
580
|
+
const warningMs = effectiveTimeout - FLOW_TIME_BUDGET_WARNING_MS;
|
|
581
|
+
if (warningMs > 0) {
|
|
582
|
+
const warnTimer = setTimeout(() => {
|
|
583
|
+
if (didClose || settled) return;
|
|
584
|
+
const remainingSec = Math.round(FLOW_TIME_BUDGET_WARNING_MS / 1000);
|
|
585
|
+
const warnMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. The agent should wrap up now.`;
|
|
586
|
+
result.stderr += warnMsg;
|
|
587
|
+
// Force an update so the parent UI shows the warning immediately.
|
|
588
|
+
emitUpdate();
|
|
589
|
+
}, warningMs);
|
|
590
|
+
warnTimer.unref();
|
|
591
|
+
}
|
|
592
|
+
|
|
565
593
|
const timeoutTimer = setTimeout(() => {
|
|
566
594
|
if (didClose || settled) return;
|
|
567
595
|
result.stderr += `\nFlow timed out after ${Math.round(effectiveTimeout / 1000)}s.`;
|