pi-agent-flow 1.5.0 → 1.7.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 +112 -3
- package/src/index.ts +12 -6
- package/src/render-utils.ts +6 -3
- package/src/render.ts +15 -9
- package/src/session-mode.ts +4 -3
- package/src/timed-bash.ts +36 -0
package/package.json
CHANGED
package/src/flow.ts
CHANGED
|
@@ -25,10 +25,11 @@ import { DEFAULT_AGENT_SESSION_MODE, getAgentSessionTimeoutMs, type AgentSession
|
|
|
25
25
|
|
|
26
26
|
const isWindows = process.platform === "win32";
|
|
27
27
|
const SIGKILL_TIMEOUT_MS = 5000;
|
|
28
|
+
const FINISH_KILL_GRACE_MS = 5_000; // wait 5s after finish() before force-killing the child process
|
|
28
29
|
const AGENT_END_GRACE_MS = 2000;
|
|
29
30
|
const FLOW_TIME_BUDGET_WARNING_MS = 2 * 60 * 1000; // warn 2 min before kill
|
|
30
|
-
const FLOW_FINAL_URGE_MS =
|
|
31
|
-
const REPORTING_GRACE_MS =
|
|
31
|
+
const FLOW_FINAL_URGE_MS = 135 * 1000; // final urge 135 s (2m15s) before kill (increased from 30s for wider summary window)
|
|
32
|
+
const REPORTING_GRACE_MS = 90_000; // grace period after timeout for agent to report findings (increased from 10s to 90s)
|
|
32
33
|
const FLOW_TOOL_SUMMARY_GRACE_MS = FLOW_FINAL_URGE_MS; // bash/tool abort lead time so the agent can summarize
|
|
33
34
|
const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
34
35
|
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
@@ -38,6 +39,54 @@ const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
|
38
39
|
const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
|
|
39
40
|
export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
|
|
40
41
|
const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
42
|
+
const FLOW_REMINDER_FILE_ENV = "PI_FLOW_REMINDER_FILE";
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Global child process group tracking for signal propagation
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** Track child process groups so we can kill them on parent exit / signal. */
|
|
49
|
+
const runningChildGroups = new Map<number, { groupPid: number; name: string }>();
|
|
50
|
+
|
|
51
|
+
/** Register a child process group for cleanup on shutdown. */
|
|
52
|
+
export function registerChildGroup(pid: number, name: string): void {
|
|
53
|
+
if (pid > 0 && !isWindows) {
|
|
54
|
+
runningChildGroups.set(pid, { groupPid: pid, name });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Unregister a completed/stopped child process group. */
|
|
59
|
+
export function unregisterChildGroup(pid: number): void {
|
|
60
|
+
runningChildGroups.delete(pid);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Terminate all registered child process groups via SIGTERM (then SIGKILL after timeout).
|
|
65
|
+
* Called on parent exit, SIGINT, SIGTERM, or pi-agent-flow:shutdown.
|
|
66
|
+
*/
|
|
67
|
+
export function terminateAllChildGroups(): void {
|
|
68
|
+
if (runningChildGroups.size === 0) return;
|
|
69
|
+
const pids = Array.from(runningChildGroups.keys());
|
|
70
|
+
for (const pid of pids) {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(-pid, "SIGTERM");
|
|
73
|
+
} catch {
|
|
74
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* gone */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Hard kill after timeout
|
|
78
|
+
const sigkillTimer = setTimeout(() => {
|
|
79
|
+
for (const pid of pids) {
|
|
80
|
+
try {
|
|
81
|
+
process.kill(-pid, "SIGKILL");
|
|
82
|
+
} catch {
|
|
83
|
+
try { process.kill(pid, "SIGKILL"); } catch { /* gone */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
runningChildGroups.clear();
|
|
87
|
+
}, 5000);
|
|
88
|
+
sigkillTimer.unref();
|
|
89
|
+
}
|
|
41
90
|
|
|
42
91
|
type FlowUpdateCallback = (partial: AgentToolResult<FlowDetails>) => void;
|
|
43
92
|
|
|
@@ -109,6 +158,24 @@ function cleanupFlowTempDir(dir: string | null): void {
|
|
|
109
158
|
}
|
|
110
159
|
}
|
|
111
160
|
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Reminder file helpers
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Write a reminder message to the reminder file so the child agent can see it
|
|
167
|
+
* via the timed-bash wrapper before its next tool call.
|
|
168
|
+
* Creates the file if it doesn't exist; appends the message.
|
|
169
|
+
*/
|
|
170
|
+
function writeReminderFile(reminderFilePath: string | null, message: string): void {
|
|
171
|
+
if (!reminderFilePath) return;
|
|
172
|
+
try {
|
|
173
|
+
fs.writeFileSync(reminderFilePath, message + "\n", { encoding: "utf-8", flag: "a" });
|
|
174
|
+
} catch {
|
|
175
|
+
/* best-effort */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
// ---------------------------------------------------------------------------
|
|
113
180
|
// Build pi CLI arguments (fork-only)
|
|
114
181
|
// ---------------------------------------------------------------------------
|
|
@@ -428,6 +495,15 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
428
495
|
forkSessionTmpPath = forkTmp.filePath;
|
|
429
496
|
}
|
|
430
497
|
|
|
498
|
+
// Create a temp dir for the reminder file so the child agent can read timeout warnings
|
|
499
|
+
// via the timed-bash wrapper before its next tool call.
|
|
500
|
+
let reminderTmpDir: string | null = null;
|
|
501
|
+
let reminderFilePath: string | null = null;
|
|
502
|
+
if (effectiveTimeout > 0) {
|
|
503
|
+
reminderTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-flow-reminder-"));
|
|
504
|
+
reminderFilePath = path.join(reminderTmpDir, "reminder.txt");
|
|
505
|
+
}
|
|
506
|
+
|
|
431
507
|
try {
|
|
432
508
|
const piArgs = buildFlowArgs(
|
|
433
509
|
flow,
|
|
@@ -472,13 +548,20 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
472
548
|
...(effectiveTimeout > 0 ? {
|
|
473
549
|
[FLOW_DEADLINE_ENV]: String(deadlineAtMs),
|
|
474
550
|
[FLOW_TOOL_SUMMARY_GRACE_ENV]: String(toolSummaryGraceMs),
|
|
551
|
+
[FLOW_REMINDER_FILE_ENV]: reminderFilePath ?? "",
|
|
475
552
|
} : {
|
|
476
553
|
[FLOW_DEADLINE_ENV]: "",
|
|
477
554
|
[FLOW_TOOL_SUMMARY_GRACE_ENV]: "0",
|
|
555
|
+
[FLOW_REMINDER_FILE_ENV]: "",
|
|
478
556
|
}),
|
|
479
557
|
},
|
|
480
558
|
});
|
|
481
559
|
|
|
560
|
+
// Register the child process group for global cleanup on signal/exit
|
|
561
|
+
if (proc.pid !== undefined && !isWindows) {
|
|
562
|
+
registerChildGroup(proc.pid, normalizedFlowName);
|
|
563
|
+
}
|
|
564
|
+
|
|
482
565
|
let stdinEnded = false;
|
|
483
566
|
const endStdin = () => {
|
|
484
567
|
if (stdinEnded) return;
|
|
@@ -497,6 +580,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
497
580
|
let timeoutFired = false;
|
|
498
581
|
let semanticCompletionTimer: NodeJS.Timeout | undefined;
|
|
499
582
|
let countdownTimer: NodeJS.Timeout | undefined;
|
|
583
|
+
let finishKillTimer: NodeJS.Timeout | undefined;
|
|
500
584
|
|
|
501
585
|
const clearSemanticCompletionTimer = () => {
|
|
502
586
|
if (semanticCompletionTimer) {
|
|
@@ -534,6 +618,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
534
618
|
sigkillTimer.unref();
|
|
535
619
|
};
|
|
536
620
|
|
|
621
|
+
const clearFinishKillTimer = () => {
|
|
622
|
+
if (finishKillTimer) {
|
|
623
|
+
clearTimeout(finishKillTimer);
|
|
624
|
+
finishKillTimer = undefined;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
537
628
|
const finish = (code: number) => {
|
|
538
629
|
if (settled) return;
|
|
539
630
|
settled = true;
|
|
@@ -543,6 +634,15 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
543
634
|
if (signal && abortHandler) {
|
|
544
635
|
signal.removeEventListener("abort", abortHandler);
|
|
545
636
|
}
|
|
637
|
+
// Soft-kill: give the child a short grace to exit naturally after stdin close.
|
|
638
|
+
// If it hasn't closed by then, force-kill to prevent orphaned processes.
|
|
639
|
+
clearFinishKillTimer();
|
|
640
|
+
finishKillTimer = setTimeout(() => {
|
|
641
|
+
if (!didClose) {
|
|
642
|
+
terminateChild();
|
|
643
|
+
}
|
|
644
|
+
}, FINISH_KILL_GRACE_MS);
|
|
645
|
+
finishKillTimer.unref();
|
|
546
646
|
resolve(code);
|
|
547
647
|
};
|
|
548
648
|
|
|
@@ -596,6 +696,10 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
596
696
|
|
|
597
697
|
proc.on("close", (code) => {
|
|
598
698
|
didClose = true;
|
|
699
|
+
clearFinishKillTimer();
|
|
700
|
+
if (proc.pid !== undefined) {
|
|
701
|
+
unregisterChildGroup(proc.pid);
|
|
702
|
+
}
|
|
599
703
|
if (buffer.trim()) flushBufferedLines(buffer);
|
|
600
704
|
finish(code ?? 0);
|
|
601
705
|
});
|
|
@@ -630,13 +734,15 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
630
734
|
const remainingSec = Math.round(FLOW_TIME_BUDGET_WARNING_MS / 1000);
|
|
631
735
|
const warnMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. The agent should wrap up now.`;
|
|
632
736
|
result.stderr += warnMsg;
|
|
737
|
+
// Write to reminder file so the child agent sees it on its next bash call.
|
|
738
|
+
writeReminderFile(reminderFilePath, `[Flow warning] ${remainingSec}s remaining before hard timeout. Wrap up your work and output structured findings.`);
|
|
633
739
|
// Force an update so the parent UI shows the warning immediately.
|
|
634
740
|
emitUpdate();
|
|
635
741
|
}, warningMs);
|
|
636
742
|
warnTimer.unref();
|
|
637
743
|
}
|
|
638
744
|
|
|
639
|
-
// Final urge timer: stronger warning
|
|
745
|
+
// Final urge timer: stronger warning 45 s before hard timeout
|
|
640
746
|
const urgeMs = effectiveTimeout - FLOW_FINAL_URGE_MS;
|
|
641
747
|
if (urgeMs > 0) {
|
|
642
748
|
const urgeTimer = setTimeout(() => {
|
|
@@ -644,6 +750,8 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
644
750
|
const remainingSec = Math.round(FLOW_FINAL_URGE_MS / 1000);
|
|
645
751
|
const urgeMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. Stop all work and output your structured findings.`;
|
|
646
752
|
result.stderr += urgeMsg;
|
|
753
|
+
// Write to reminder file so the child agent sees it on its next bash call.
|
|
754
|
+
writeReminderFile(reminderFilePath, `[Flow urge] ${remainingSec}s remaining before hard timeout. STOP all tool use and output your structured findings NOW.`);
|
|
647
755
|
emitUpdate();
|
|
648
756
|
}, urgeMs);
|
|
649
757
|
urgeTimer.unref();
|
|
@@ -692,6 +800,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
692
800
|
return normalized;
|
|
693
801
|
} finally {
|
|
694
802
|
cleanupFlowTempDir(forkSessionTmpDir);
|
|
803
|
+
cleanupFlowTempDir(reminderTmpDir);
|
|
695
804
|
}
|
|
696
805
|
}
|
|
697
806
|
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { getInheritedCliArgs } from "./cli-args.js";
|
|
|
24
24
|
import { renderFlowCall, renderFlowResult } from "./render.js";
|
|
25
25
|
import { getFlowSummaryText } from "./runner-events.js";
|
|
26
26
|
import { runHooks, runHooksDetailed, getRegisteredHooks, registerHook, unregisterHook } from "./hooks.js";
|
|
27
|
-
import { mapFlowConcurrent, runFlow } from "./flow.js";
|
|
27
|
+
import { mapFlowConcurrent, runFlow, terminateAllChildGroups } from "./flow.js";
|
|
28
28
|
import { executeFlows } from "./executor.js";
|
|
29
29
|
import {
|
|
30
30
|
type SingleResult,
|
|
@@ -1095,14 +1095,20 @@ flow [type] accomplished
|
|
|
1095
1095
|
}
|
|
1096
1096
|
|
|
1097
1097
|
// Register cleanup on process exit (once).
|
|
1098
|
-
// We
|
|
1099
|
-
//
|
|
1100
|
-
// host
|
|
1101
|
-
// lines visible on Ctrl+C. The 'exit' event fires for normal exits,
|
|
1102
|
-
// including when the host handles the signal and calls process.exit().
|
|
1098
|
+
// We use prependListener on SIGINT/SIGTERM to propagate to child processes
|
|
1099
|
+
// before the host's own signal handler runs. This avoids orphaned sub-agents.
|
|
1100
|
+
// The host handler still runs afterward and handles terminal cleanup.
|
|
1103
1101
|
if (!(globalThis as any).__pi_agent_flow_shutdown_registered) {
|
|
1104
1102
|
(globalThis as any).__pi_agent_flow_shutdown_registered = true;
|
|
1103
|
+
|
|
1104
|
+
// Propagate signals to child process groups so sub-agents don't become orphans.
|
|
1105
|
+
// We use prependListener so our handler runs first, before the host's cleanup.
|
|
1106
|
+
process.prependListener("SIGINT", terminateAllChildGroups);
|
|
1107
|
+
process.prependListener("SIGTERM", terminateAllChildGroups);
|
|
1108
|
+
|
|
1109
|
+
// Also handle the 'exit' event, which fires when the host calls process.exit().
|
|
1105
1110
|
const emitShutdown = () => {
|
|
1111
|
+
terminateAllChildGroups();
|
|
1106
1112
|
if (typeof pi.emit === "function") {
|
|
1107
1113
|
pi.emit("pi-agent-flow:shutdown", { reason: "process-exit" });
|
|
1108
1114
|
}
|
package/src/render-utils.ts
CHANGED
|
@@ -108,12 +108,15 @@ export function getTruncationBudget(prefixLength: number): number {
|
|
|
108
108
|
/** Fixed content budget for collapsed-line text (dir/act/log). */
|
|
109
109
|
export const CONTENT_MAX = 60;
|
|
110
110
|
|
|
111
|
+
/** Longer budget for detail lines (act/msg) to show more content. */
|
|
112
|
+
export const DETAIL_CONTENT_MAX = 80;
|
|
113
|
+
|
|
111
114
|
/**
|
|
112
115
|
* Compute how many visible chars of content fit after a prefix,
|
|
113
|
-
* using the
|
|
116
|
+
* using the given budget (defaults to CONTENT_MAX). Floor of 8 to keep things readable.
|
|
114
117
|
*/
|
|
115
|
-
export function contentBudget(prefixVisibleLen: number): number {
|
|
116
|
-
return Math.max(
|
|
118
|
+
export function contentBudget(prefixVisibleLen: number, max = CONTENT_MAX): number {
|
|
119
|
+
return Math.max(max - prefixVisibleLen, 8);
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/**
|
package/src/render.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
isFlowError,
|
|
23
23
|
isFlowSuccess,
|
|
24
24
|
} from "./types.js";
|
|
25
|
-
import { formatCompactStats, formatCompactTokenPair, formatCountdown, formatFlowTypeName, truncateChars, tailText, contentBudget, visibleLength } from "./render-utils.js";
|
|
25
|
+
import { formatCompactStats, formatCompactTokenPair, formatCountdown, formatFlowTypeName, truncateChars, tailText, contentBudget, visibleLength, DETAIL_CONTENT_MAX } from "./render-utils.js";
|
|
26
26
|
|
|
27
27
|
function shortenPath(p: string): string {
|
|
28
28
|
const home = os.homedir();
|
|
@@ -33,6 +33,12 @@ type ThemeFg = (color: string, text: string) => string;
|
|
|
33
33
|
type ThemeBg = (color: string, text: string) => string;
|
|
34
34
|
type FlowTheme = { fg: ThemeFg; bold: (s: string) => string; bg: ThemeBg };
|
|
35
35
|
|
|
36
|
+
const COLLAPSED_FLOW_HEADER_TYPE_WIDTH = 8;
|
|
37
|
+
|
|
38
|
+
function formatCollapsedFlowHeaderTypeName(type: string): string {
|
|
39
|
+
return formatFlowTypeName(type).padEnd(COLLAPSED_FLOW_HEADER_TYPE_WIDTH, " ");
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
function formatFlowToolCall(toolName: string, args: Record<string, unknown>, fg: ThemeFg): string {
|
|
37
43
|
const pathArg = (args.file_path || args.path || "...") as string;
|
|
38
44
|
|
|
@@ -320,8 +326,8 @@ function renderFlowCollapsed(
|
|
|
320
326
|
const container = new Container();
|
|
321
327
|
const maxWidth = process.stdout.columns ?? 80;
|
|
322
328
|
const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true });
|
|
323
|
-
const typeName =
|
|
324
|
-
let header = `${theme.fg("accent", theme.bold(typeName))}
|
|
329
|
+
const typeName = formatCollapsedFlowHeaderTypeName(r.type);
|
|
330
|
+
let header = `${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", `· ${stats}`)}`;
|
|
325
331
|
if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
326
332
|
container.addChild(new TruncatedText(header, 0, 0));
|
|
327
333
|
|
|
@@ -337,13 +343,13 @@ function renderFlowCollapsed(
|
|
|
337
343
|
if (lastTool) {
|
|
338
344
|
const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
339
345
|
const actPrefix = `├─ act: [${r.usage.toolCalls}] - `;
|
|
340
|
-
const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix)));
|
|
346
|
+
const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix), DETAIL_CONTENT_MAX));
|
|
341
347
|
container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${actContent}`, 0, 0));
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
// msg: line (last assistant text or streaming)
|
|
345
351
|
const msgPrefix = formatMsgLinePrefix("└─", r);
|
|
346
|
-
const msgBudget = contentBudget(visibleLength(msgPrefix));
|
|
352
|
+
const msgBudget = contentBudget(visibleLength(msgPrefix), DETAIL_CONTENT_MAX);
|
|
347
353
|
if (r.exitCode === -1 && streamingText) {
|
|
348
354
|
const logContent = tailText(streamingText, msgBudget);
|
|
349
355
|
container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", logContent)}`, 0, 0));
|
|
@@ -453,11 +459,11 @@ function renderActivityPanel(
|
|
|
453
459
|
const isLast = i === results.length - 1;
|
|
454
460
|
const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true });
|
|
455
461
|
const error = isFlowError(r);
|
|
456
|
-
const typeName =
|
|
462
|
+
const typeName = formatCollapsedFlowHeaderTypeName(r.type);
|
|
457
463
|
|
|
458
464
|
// Header line
|
|
459
465
|
const headerPrefix = isLast ? "└─" : "├─";
|
|
460
|
-
let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.fg("accent", theme.bold(typeName))}
|
|
466
|
+
let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", `· ${stats}`)}`;
|
|
461
467
|
if (error && r.stopReason) {
|
|
462
468
|
headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
463
469
|
}
|
|
@@ -478,13 +484,13 @@ function renderActivityPanel(
|
|
|
478
484
|
if (lastTool) {
|
|
479
485
|
const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
480
486
|
const actPrefix = `${indent}├─ act: [${r.usage.toolCalls}] - `;
|
|
481
|
-
const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix)));
|
|
487
|
+
const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix), DETAIL_CONTENT_MAX));
|
|
482
488
|
container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${actContent}`, 0, 0));
|
|
483
489
|
}
|
|
484
490
|
|
|
485
491
|
// msg: line (live streaming text or last assistant text)
|
|
486
492
|
const msgPrefix = formatMsgLinePrefix(indent + "└─", r);
|
|
487
|
-
const msgBudget = contentBudget(visibleLength(msgPrefix));
|
|
493
|
+
const msgBudget = contentBudget(visibleLength(msgPrefix), DETAIL_CONTENT_MAX);
|
|
488
494
|
const liveText = r.exitCode === -1 ? r.streamingText : undefined;
|
|
489
495
|
const lastText = liveText || getLastAssistantText(r.messages);
|
|
490
496
|
if (lastText) {
|
package/src/session-mode.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
export const AGENT_SESSION_MODES = ["fast", "default", "long"] as const;
|
|
1
|
+
export const AGENT_SESSION_MODES = ["fast", "default", "long", "extreme_long"] as const;
|
|
2
2
|
|
|
3
3
|
export type AgentSessionMode = typeof AGENT_SESSION_MODES[number];
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_AGENT_SESSION_MODE: AgentSessionMode = "default";
|
|
6
|
-
export const MAX_AGENT_SESSION_TIMEOUT_MS =
|
|
6
|
+
export const MAX_AGENT_SESSION_TIMEOUT_MS = 1_200_000;
|
|
7
7
|
export const PI_FLOW_SESSION_MODE_ENV = "PI_FLOW_SESSION_MODE";
|
|
8
8
|
|
|
9
9
|
export const AGENT_SESSION_TIMEOUTS_MS: Record<AgentSessionMode, number> = {
|
|
10
10
|
fast: 300_000,
|
|
11
11
|
default: 600_000,
|
|
12
|
-
long:
|
|
12
|
+
long: 900_000,
|
|
13
|
+
extreme_long: MAX_AGENT_SESSION_TIMEOUT_MS,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export function parseAgentSessionMode(value: unknown): AgentSessionMode | undefined {
|
package/src/timed-bash.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* still active.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import * as fs from "node:fs";
|
|
16
17
|
import { createBashToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
17
18
|
|
|
18
19
|
export type TimingTier =
|
|
@@ -30,6 +31,7 @@ export interface TimingReport {
|
|
|
30
31
|
|
|
31
32
|
const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
32
33
|
const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
|
|
34
|
+
const FLOW_REMINDER_FILE_ENV = "PI_FLOW_REMINDER_FILE";
|
|
33
35
|
const DEFAULT_FLOW_TOOL_SUMMARY_GRACE_MS = 30_000;
|
|
34
36
|
|
|
35
37
|
/** Classify duration into user-defined tiers with actionable feedback. */
|
|
@@ -87,6 +89,30 @@ function getFlowToolSummaryGraceMs(): number {
|
|
|
87
89
|
return parseNonNegativeSafeInteger(process.env[FLOW_TOOL_SUMMARY_GRACE_ENV]) ?? DEFAULT_FLOW_TOOL_SUMMARY_GRACE_MS;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
function getFlowReminderFilePath(): string | null {
|
|
93
|
+
const raw = process.env[FLOW_REMINDER_FILE_ENV];
|
|
94
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read any pending reminder from the reminder file set by the parent runner.
|
|
99
|
+
* Returns the reminder text (without trailing newline), or null if no reminder exists.
|
|
100
|
+
* Clears the file after reading so the agent only sees each reminder once.
|
|
101
|
+
*/
|
|
102
|
+
export function readAndClearReminderFile(): string | null {
|
|
103
|
+
const filePath = getFlowReminderFilePath();
|
|
104
|
+
if (!filePath) return null;
|
|
105
|
+
try {
|
|
106
|
+
if (!fs.existsSync(filePath)) return null;
|
|
107
|
+
const content = fs.readFileSync(filePath, { encoding: "utf-8" }).trim();
|
|
108
|
+
// Clear the file after reading so each reminder is only injected once.
|
|
109
|
+
try { fs.writeFileSync(filePath, "", { encoding: "utf-8" }); } catch { /* best-effort */ }
|
|
110
|
+
return content || null;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
function formatDeadlineAppendix(): string {
|
|
91
117
|
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
118
|
}
|
|
@@ -189,6 +215,12 @@ export function createTimedBashToolDefinition(
|
|
|
189
215
|
) {
|
|
190
216
|
const start = Date.now();
|
|
191
217
|
const deadlineSignal = createDeadlineSignal(signal);
|
|
218
|
+
|
|
219
|
+
// Check for pending reminder from parent runner before executing bash.
|
|
220
|
+
// This allows the parent to inject timeout warnings into the agent's context
|
|
221
|
+
// via the tool result, since stdin is closed after spawn.
|
|
222
|
+
const reminderPreamble = readAndClearReminderFile();
|
|
223
|
+
|
|
192
224
|
try {
|
|
193
225
|
const result = await original.execute(
|
|
194
226
|
toolCallId,
|
|
@@ -201,6 +233,10 @@ export function createTimedBashToolDefinition(
|
|
|
201
233
|
const report = classifyDuration(duration);
|
|
202
234
|
const appendix = formatTimingAppendix(report);
|
|
203
235
|
|
|
236
|
+
// Inject reminder preamble before timing info so it's the first thing the agent sees.
|
|
237
|
+
if (reminderPreamble) {
|
|
238
|
+
appendTextToToolResult(result, `\n\n[REMINDER FROM PARENT] ${reminderPreamble}`);
|
|
239
|
+
}
|
|
204
240
|
appendTextToToolResult(result, appendix);
|
|
205
241
|
if (deadlineSignal.wasDeadlineAbort()) {
|
|
206
242
|
appendTextToToolResult(result, formatDeadlineAppendix());
|