pi-agent-flow 1.4.2 → 1.4.3
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/ambient.d.ts +4 -0
- package/src/flow.ts +18 -3
- package/src/timed-bash.ts +113 -8
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/flow.ts
CHANGED
|
@@ -29,12 +29,14 @@ 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
37
|
const FLOW_TIMEOUT_ENV = "PI_FLOW_TIMEOUT_MS";
|
|
37
38
|
const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
|
|
39
|
+
const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
|
|
38
40
|
export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
|
|
39
41
|
const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
40
42
|
|
|
@@ -217,7 +219,7 @@ function buildFlowArgs(
|
|
|
217
219
|
|
|
218
220
|
const timeBudgetHint =
|
|
219
221
|
timeoutMs && timeoutMs > 0
|
|
220
|
-
? `Time budget: ${Math.round(timeoutMs / 1000)}s total.
|
|
222
|
+
? `Time budget: ${Math.round(timeoutMs / 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
223
|
: "";
|
|
222
224
|
|
|
223
225
|
const activation =
|
|
@@ -443,6 +445,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
443
445
|
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
444
446
|
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
445
447
|
const propagatedStack = [...parentFlowStack, normalizedFlowName];
|
|
448
|
+
const proportionalGraceMs = Math.floor(effectiveTimeout * 0.1);
|
|
449
|
+
const minimumGraceMs = effectiveTimeout >= 10_000 ? 1_000 : Math.floor(effectiveTimeout / 2);
|
|
450
|
+
const toolSummaryGraceMs = Math.min(
|
|
451
|
+
FLOW_TOOL_SUMMARY_GRACE_MS,
|
|
452
|
+
Math.max(0, effectiveTimeout),
|
|
453
|
+
Math.max(minimumGraceMs, proportionalGraceMs),
|
|
454
|
+
);
|
|
446
455
|
const { command, prefixArgs } = resolveFlowSpawn();
|
|
447
456
|
const proc = spawn(command, [...prefixArgs, ...piArgs], {
|
|
448
457
|
cwd: taskCwd ?? cwd,
|
|
@@ -458,7 +467,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
458
467
|
[FLOW_PREVENT_CYCLES_ENV]: preventCycles ? "1" : "0",
|
|
459
468
|
[FLOW_TOOL_OPTIMIZE_ENV]: toolOptimize ? "1" : "0",
|
|
460
469
|
[PI_OFFLINE_ENV]: "1",
|
|
461
|
-
|
|
470
|
+
...(effectiveTimeout > 0 ? {
|
|
471
|
+
[FLOW_DEADLINE_ENV]: String(Date.now() + effectiveTimeout),
|
|
472
|
+
[FLOW_TOOL_SUMMARY_GRACE_ENV]: String(toolSummaryGraceMs),
|
|
473
|
+
} : {
|
|
474
|
+
[FLOW_DEADLINE_ENV]: "",
|
|
475
|
+
[FLOW_TOOL_SUMMARY_GRACE_ENV]: "0",
|
|
476
|
+
}),
|
|
462
477
|
},
|
|
463
478
|
});
|
|
464
479
|
|
|
@@ -583,7 +598,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
583
598
|
else signal.addEventListener("abort", abortHandler, { once: true });
|
|
584
599
|
}
|
|
585
600
|
|
|
586
|
-
// Execution timeout — two-stage:
|
|
601
|
+
// Execution timeout — two-stage: parent-side warnings, tool-level deadline abort, then grace, then hard kill
|
|
587
602
|
if (effectiveTimeout > 0) {
|
|
588
603
|
// Warning timer: notify the parent UI that the child is about to be killed.
|
|
589
604
|
// NOTE: True mid-flight injection into the child's context requires pi-core
|
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
|
};
|