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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/flow.ts +36 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
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.`;