pi-agent-flow 1.4.0 → 1.4.2
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 +39 -4
- package/src/structured-output.ts +42 -6
- package/src/types.ts +3 -1
package/package.json
CHANGED
package/src/flow.ts
CHANGED
|
@@ -27,6 +27,8 @@ 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
29
|
const FLOW_TIME_BUDGET_WARNING_MS = 2 * 60 * 1000; // warn 2 min before kill
|
|
30
|
+
const FLOW_FINAL_URGE_MS = 30 * 1000; // final urge 30 s before kill
|
|
31
|
+
const REPORTING_GRACE_MS = 10_000; // grace period after timeout for agent to report findings
|
|
30
32
|
const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
|
|
31
33
|
const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
|
|
32
34
|
const FLOW_STACK_ENV = "PI_FLOW_STACK";
|
|
@@ -250,7 +252,7 @@ function buildFlowArgs(
|
|
|
250
252
|
` { "type": "read", "description": "what was done", "target": "file.ts", "result": "success", "evidence": "output or proof" }\n` +
|
|
251
253
|
` ],\n` +
|
|
252
254
|
` "commands": [\n` +
|
|
253
|
-
` { "command": "curl -s -X POST https://api.example.com/v1/data -H 'Authorization: Bearer token'", "tool": "bash" }\n` +
|
|
255
|
+
` { "command": "curl -s -X POST https://api.example.com/v1/data -H 'Authorization: Bearer token'", "tool": "bash", "executionTime": "1.2s (normal)" }\n` +
|
|
254
256
|
` ],\n` +
|
|
255
257
|
` "notDone": [\n` +
|
|
256
258
|
` { "item": "unfinished work", "reason": "why it was not completed", "blocker": "blocking issue if any", "nextStep": "specific follow-up" }\n` +
|
|
@@ -460,15 +462,22 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
460
462
|
},
|
|
461
463
|
});
|
|
462
464
|
|
|
465
|
+
let stdinEnded = false;
|
|
466
|
+
const endStdin = () => {
|
|
467
|
+
if (stdinEnded) return;
|
|
468
|
+
stdinEnded = true;
|
|
469
|
+
try { proc.stdin.end(); } catch { /* ignore */ }
|
|
470
|
+
};
|
|
463
471
|
proc.stdin.on("error", () => {
|
|
464
472
|
/* ignore broken pipe on fast exits */
|
|
465
473
|
});
|
|
466
474
|
proc.stdin.end();
|
|
467
475
|
|
|
476
|
+
let abortHandler: (() => void) | undefined;
|
|
468
477
|
let buffer = "";
|
|
469
478
|
let didClose = false;
|
|
470
479
|
let settled = false;
|
|
471
|
-
let
|
|
480
|
+
let timeoutFired = false;
|
|
472
481
|
let semanticCompletionTimer: NodeJS.Timeout | undefined;
|
|
473
482
|
|
|
474
483
|
const clearSemanticCompletionTimer = () => {
|
|
@@ -479,6 +488,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
479
488
|
};
|
|
480
489
|
|
|
481
490
|
const terminateChild = () => {
|
|
491
|
+
endStdin();
|
|
482
492
|
if (isWindows) {
|
|
483
493
|
if (proc.pid !== undefined) {
|
|
484
494
|
const killer = spawn("taskkill", ["/T", "/F", "/PID", String(proc.pid)], {
|
|
@@ -502,6 +512,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
502
512
|
const finish = (code: number) => {
|
|
503
513
|
if (settled) return;
|
|
504
514
|
settled = true;
|
|
515
|
+
endStdin();
|
|
505
516
|
clearSemanticCompletionTimer();
|
|
506
517
|
if (signal && abortHandler) {
|
|
507
518
|
signal.removeEventListener("abort", abortHandler);
|
|
@@ -565,13 +576,14 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
565
576
|
abortHandler = () => {
|
|
566
577
|
if (didClose || settled) return;
|
|
567
578
|
wasAborted = true;
|
|
579
|
+
endStdin();
|
|
568
580
|
terminateChild();
|
|
569
581
|
};
|
|
570
582
|
if (signal.aborted) abortHandler();
|
|
571
583
|
else signal.addEventListener("abort", abortHandler, { once: true });
|
|
572
584
|
}
|
|
573
585
|
|
|
574
|
-
// Execution timeout —
|
|
586
|
+
// Execution timeout — two-stage: urge message, then stdin injection, then grace, then hard kill
|
|
575
587
|
if (effectiveTimeout > 0) {
|
|
576
588
|
// Warning timer: notify the parent UI that the child is about to be killed.
|
|
577
589
|
// NOTE: True mid-flight injection into the child's context requires pi-core
|
|
@@ -590,10 +602,33 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
590
602
|
warnTimer.unref();
|
|
591
603
|
}
|
|
592
604
|
|
|
605
|
+
// Final urge timer: stronger warning 30 s before hard timeout
|
|
606
|
+
const urgeMs = effectiveTimeout - FLOW_FINAL_URGE_MS;
|
|
607
|
+
if (urgeMs > 0) {
|
|
608
|
+
const urgeTimer = setTimeout(() => {
|
|
609
|
+
if (didClose || settled) return;
|
|
610
|
+
const remainingSec = Math.round(FLOW_FINAL_URGE_MS / 1000);
|
|
611
|
+
const urgeMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. Stop all work and output your structured findings.`;
|
|
612
|
+
result.stderr += urgeMsg;
|
|
613
|
+
emitUpdate();
|
|
614
|
+
}, urgeMs);
|
|
615
|
+
urgeTimer.unref();
|
|
616
|
+
}
|
|
617
|
+
|
|
593
618
|
const timeoutTimer = setTimeout(() => {
|
|
594
619
|
if (didClose || settled) return;
|
|
620
|
+
timeoutFired = true;
|
|
595
621
|
result.stderr += `\nFlow timed out after ${Math.round(effectiveTimeout / 1000)}s.`;
|
|
596
|
-
|
|
622
|
+
emitUpdate();
|
|
623
|
+
|
|
624
|
+
// Grace period before hard kill
|
|
625
|
+
const graceTimer = setTimeout(() => {
|
|
626
|
+
if (didClose || settled) return;
|
|
627
|
+
result.stopReason = "timeout";
|
|
628
|
+
result.errorMessage = `Flow timed out after ${Math.round(effectiveTimeout / 1000)}s.`;
|
|
629
|
+
terminateChild();
|
|
630
|
+
}, REPORTING_GRACE_MS);
|
|
631
|
+
graceTimer.unref();
|
|
597
632
|
}, effectiveTimeout);
|
|
598
633
|
timeoutTimer.unref();
|
|
599
634
|
}
|
package/src/structured-output.ts
CHANGED
|
@@ -110,8 +110,30 @@ export function enrichStructuredOutputCommands(
|
|
|
110
110
|
structuredOutput: FlowStructuredOutput,
|
|
111
111
|
messages: Message[],
|
|
112
112
|
): FlowStructuredOutput {
|
|
113
|
+
// Build a map of toolCallId -> execution time string from tool result messages.
|
|
114
|
+
const timingMap = new Map<string, string>();
|
|
115
|
+
for (const msg of messages) {
|
|
116
|
+
if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
|
|
117
|
+
const id =
|
|
118
|
+
(msg as unknown as { toolCallId?: string }).toolCallId ||
|
|
119
|
+
(msg as unknown as { tool_call_id?: string }).tool_call_id ||
|
|
120
|
+
"";
|
|
121
|
+
if (!id) continue;
|
|
122
|
+
const text = msg.content
|
|
123
|
+
.filter((c: { type: string; text?: string }) => c.type === "text" && typeof c.text === "string")
|
|
124
|
+
.map((c: { text: string }) => c.text)
|
|
125
|
+
.join("");
|
|
126
|
+
const match = text.match(/\[Execution time: ([^\]]+)\]/);
|
|
127
|
+
if (match) timingMap.set(id, match[1]);
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
// Collect actual verbatim bash commands (including those inside batch ops)
|
|
114
|
-
|
|
131
|
+
// alongside their toolCallId so we can look up execution time.
|
|
132
|
+
interface BashEntry {
|
|
133
|
+
command: string;
|
|
134
|
+
toolCallId?: string;
|
|
135
|
+
}
|
|
136
|
+
const actualBashEntries: BashEntry[] = [];
|
|
115
137
|
for (const msg of messages) {
|
|
116
138
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
117
139
|
for (const part of msg.content) {
|
|
@@ -121,10 +143,14 @@ export function enrichStructuredOutputCommands(
|
|
|
121
143
|
(part as unknown as { toolName?: string }).toolName ||
|
|
122
144
|
"";
|
|
123
145
|
const args = part.arguments || part.input || {};
|
|
146
|
+
const toolCallId =
|
|
147
|
+
(part as unknown as { toolCallId?: string }).toolCallId ||
|
|
148
|
+
(part as unknown as { tool_call_id?: string }).tool_call_id ||
|
|
149
|
+
"";
|
|
124
150
|
|
|
125
151
|
if (name === "bash") {
|
|
126
152
|
const cmd = (args.command as string) || "";
|
|
127
|
-
if (cmd)
|
|
153
|
+
if (cmd) actualBashEntries.push({ command: cmd, toolCallId });
|
|
128
154
|
} else if (name === "batch") {
|
|
129
155
|
const ops = Array.isArray(args.o)
|
|
130
156
|
? args.o
|
|
@@ -137,20 +163,30 @@ export function enrichStructuredOutputCommands(
|
|
|
137
163
|
if (!op) continue;
|
|
138
164
|
const opType = (op.o ?? op.op) as string;
|
|
139
165
|
if (opType === "bash" && op.command) {
|
|
140
|
-
|
|
166
|
+
// Batch-nested bash ops don't have individual toolCallIds,
|
|
167
|
+
// so execution time can't be correlated.
|
|
168
|
+
actualBashEntries.push({ command: op.command as string });
|
|
141
169
|
}
|
|
142
170
|
}
|
|
143
171
|
}
|
|
144
172
|
}
|
|
145
173
|
}
|
|
146
174
|
|
|
147
|
-
if (
|
|
175
|
+
if (actualBashEntries.length === 0) return structuredOutput;
|
|
148
176
|
|
|
149
177
|
// Replace paraphrased bash commands with actual verbatim ones, in order.
|
|
150
178
|
let bashIdx = 0;
|
|
151
179
|
const enrichedCommands = structuredOutput.commands.map((cmd) => {
|
|
152
|
-
if (cmd.tool === "bash" && bashIdx <
|
|
153
|
-
|
|
180
|
+
if (cmd.tool === "bash" && bashIdx < actualBashEntries.length) {
|
|
181
|
+
const entry = actualBashEntries[bashIdx++];
|
|
182
|
+
const executionTime = entry.toolCallId
|
|
183
|
+
? timingMap.get(entry.toolCallId)
|
|
184
|
+
: undefined;
|
|
185
|
+
return {
|
|
186
|
+
command: entry.command,
|
|
187
|
+
tool: "bash",
|
|
188
|
+
...(executionTime !== undefined ? { executionTime } : {}),
|
|
189
|
+
};
|
|
154
190
|
}
|
|
155
191
|
return { command: cmd.command, tool: cmd.tool };
|
|
156
192
|
});
|
package/src/types.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface CommandEntry {
|
|
|
43
43
|
command: string;
|
|
44
44
|
/** Tool used: bash, grep, find, ls, batch, read, write, edit, flow, web. */
|
|
45
45
|
tool?: string;
|
|
46
|
+
/** Execution time classification from the timed bash wrapper (e.g. "3.5s (normal)"). */
|
|
47
|
+
executionTime?: string;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/** Compressed representation of a flow result for child context inheritance. */
|
|
@@ -176,7 +178,7 @@ export function isFlowComplete(r: Pick<SingleResult, "messages" | "sawAgentEnd">
|
|
|
176
178
|
export function isFlowSuccess(r: SingleResult): boolean {
|
|
177
179
|
if (r.exitCode === -1) return false;
|
|
178
180
|
if (isFlowComplete(r)) return true;
|
|
179
|
-
return r.exitCode === 0 && r.stopReason !== "error" && r.stopReason !== "aborted";
|
|
181
|
+
return r.exitCode === 0 && r.stopReason !== "error" && r.stopReason !== "aborted" && r.stopReason !== "timeout";
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
/** Whether a result represents an error. */
|