pi-agent-flow 1.2.7 → 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/batch/constants.ts +1 -0
- package/src/batch/execute.ts +26 -1
- package/src/flow.ts +41 -11
- package/src/index.ts +1 -5
- package/src/structured-output.ts +62 -0
- package/src/types.ts +1 -9
package/package.json
CHANGED
package/src/batch/constants.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const MAX_BYTES = 50 * 1024; // 50KB
|
|
|
11
11
|
export const SAFE_FULL_READ_LIMIT = 300;
|
|
12
12
|
export const TARGETED_READ_LINE_LIMIT = 1000;
|
|
13
13
|
export const MAX_CONTEXT_MAP_ENTRIES = 100;
|
|
14
|
+
export const MAX_TOTAL_RESULT_LINES = 1500;
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Types
|
package/src/batch/execute.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
MAX_BYTES,
|
|
17
17
|
SAFE_FULL_READ_LIMIT,
|
|
18
18
|
TARGETED_READ_LINE_LIMIT,
|
|
19
|
+
MAX_TOTAL_RESULT_LINES,
|
|
19
20
|
} from "./constants.js";
|
|
20
21
|
import {
|
|
21
22
|
normalizeToLF,
|
|
@@ -212,6 +213,8 @@ export async function executeOperations(
|
|
|
212
213
|
const counts = { read: 0, write: 0, edit: 0, delete: 0, error: 0, skipped: 0 };
|
|
213
214
|
const errors: { path: string; op: string; message: string; hint?: string }[] = [];
|
|
214
215
|
const truncatedFiles: { path: string; shown: number; total: number; nextOffset?: number }[] = [];
|
|
216
|
+
const aggregateLimitSkipped: { path: string }[] = [];
|
|
217
|
+
let aggregateLinesRead = 0;
|
|
215
218
|
const includeLimitWarnings = options.includeLimitWarnings ?? true;
|
|
216
219
|
|
|
217
220
|
for (const op of operations) {
|
|
@@ -232,6 +235,18 @@ export async function executeOperations(
|
|
|
232
235
|
|
|
233
236
|
switch (op.o) {
|
|
234
237
|
case "read": {
|
|
238
|
+
if (aggregateLinesRead >= MAX_TOTAL_RESULT_LINES) {
|
|
239
|
+
results.push({
|
|
240
|
+
op: "read",
|
|
241
|
+
path: op.p,
|
|
242
|
+
status: "skipped",
|
|
243
|
+
error: `Skipped: aggregate line limit of ${MAX_TOTAL_RESULT_LINES} already reached. Use separate batch/batch_read calls.`,
|
|
244
|
+
});
|
|
245
|
+
counts.skipped++;
|
|
246
|
+
aggregateLimitSkipped.push({ path: op.p });
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
// Access check before reading
|
|
236
251
|
try {
|
|
237
252
|
await fs.access(resolvedPath);
|
|
@@ -293,6 +308,8 @@ export async function executeOperations(
|
|
|
293
308
|
});
|
|
294
309
|
}
|
|
295
310
|
|
|
311
|
+
aggregateLinesRead += linesRead;
|
|
312
|
+
|
|
296
313
|
results.push({
|
|
297
314
|
op: "read",
|
|
298
315
|
path: op.p,
|
|
@@ -404,7 +421,7 @@ export async function executeOperations(
|
|
|
404
421
|
}
|
|
405
422
|
}
|
|
406
423
|
// Build the enhanced summary and content text
|
|
407
|
-
const summary = buildSummary(counts, errors, truncatedFiles);
|
|
424
|
+
const summary = buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped);
|
|
408
425
|
const contentText = buildContentText(summary, results);
|
|
409
426
|
|
|
410
427
|
return { summary, contentText, results };
|
|
@@ -418,6 +435,7 @@ function buildSummary(
|
|
|
418
435
|
counts: { read: number; write: number; edit: number; delete: number; error: number; skipped: number },
|
|
419
436
|
errors: { path: string; op: string; message: string; hint?: string }[],
|
|
420
437
|
truncatedFiles: { path: string; shown: number; total: number; nextOffset?: number }[],
|
|
438
|
+
aggregateLimitSkipped: { path: string }[] = [],
|
|
421
439
|
): string {
|
|
422
440
|
const totalSuccess =
|
|
423
441
|
counts.read + counts.write + counts.edit + counts.delete;
|
|
@@ -471,6 +489,13 @@ function buildSummary(
|
|
|
471
489
|
}
|
|
472
490
|
}
|
|
473
491
|
|
|
492
|
+
// Aggregate line limit warnings
|
|
493
|
+
if (aggregateLimitSkipped.length > 0) {
|
|
494
|
+
parts.push(
|
|
495
|
+
` ⚠ Aggregate line limit (${MAX_TOTAL_RESULT_LINES}) reached — skipped ${aggregateLimitSkipped.length} read${aggregateLimitSkipped.length > 1 ? "s" : ""}: ${aggregateLimitSkipped.map((s) => s.path).join(", ")}`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
474
499
|
return parts.join("\n");
|
|
475
500
|
}
|
|
476
501
|
|
package/src/flow.ts
CHANGED
|
@@ -20,17 +20,19 @@ import {
|
|
|
20
20
|
getFlowOutput,
|
|
21
21
|
normalizeFlowResult,
|
|
22
22
|
} from "./types.js";
|
|
23
|
-
import { extractStructuredOutput } from "./structured-output.js";
|
|
23
|
+
import { extractStructuredOutput, enrichStructuredOutputCommands } from "./structured-output.js";
|
|
24
24
|
|
|
25
25
|
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
|
|
|
@@ -227,6 +236,8 @@ function buildFlowArgs(
|
|
|
227
236
|
`\n\n## Structured Output\n\n` +
|
|
228
237
|
`End your response with a JSON code block containing:\n` +
|
|
229
238
|
`\n` +
|
|
239
|
+
`For every command you ran, include the exact verbatim command string in the \\"command\\" field (not a paraphrase or summary).\n` +
|
|
240
|
+
`\n` +
|
|
230
241
|
`\`\`\`json\n` +
|
|
231
242
|
`{\n` +
|
|
232
243
|
` "version": "1.0",\n` +
|
|
@@ -239,7 +250,7 @@ function buildFlowArgs(
|
|
|
239
250
|
` { "type": "read", "description": "what was done", "target": "file.ts", "result": "success", "evidence": "output or proof" }\n` +
|
|
240
251
|
` ],\n` +
|
|
241
252
|
` "commands": [\n` +
|
|
242
|
-
` { "command": "
|
|
253
|
+
` { "command": "curl -s -X POST https://api.example.com/v1/data -H 'Authorization: Bearer token'", "tool": "bash" }\n` +
|
|
243
254
|
` ],\n` +
|
|
244
255
|
` "notDone": [\n` +
|
|
245
256
|
` { "item": "unfinished work", "reason": "why it was not completed", "blocker": "blocking issue if any", "nextStep": "specific follow-up" }\n` +
|
|
@@ -404,6 +415,14 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
404
415
|
forkSessionTmpPath = forkTmp.filePath;
|
|
405
416
|
}
|
|
406
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
|
+
|
|
407
426
|
try {
|
|
408
427
|
const piArgs = buildFlowArgs(
|
|
409
428
|
flow,
|
|
@@ -414,17 +433,10 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
414
433
|
maxDepth,
|
|
415
434
|
toolOptimize,
|
|
416
435
|
structuredOutput,
|
|
436
|
+
effectiveTimeout,
|
|
417
437
|
);
|
|
418
438
|
let wasAborted = false;
|
|
419
439
|
|
|
420
|
-
// Resolve timeout: explicit option > env var > default (10 min)
|
|
421
|
-
const envTimeoutRaw = process.env[FLOW_TIMEOUT_ENV];
|
|
422
|
-
const envTimeout = envTimeoutRaw !== undefined ? (() => {
|
|
423
|
-
const n = Number(envTimeoutRaw);
|
|
424
|
-
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
425
|
-
})() : null;
|
|
426
|
-
const effectiveTimeout = opts.timeoutMs ?? envTimeout ?? DEFAULT_FLOW_TIMEOUT_MS;
|
|
427
|
-
|
|
428
440
|
const exitCode = await new Promise<number>((resolve) => {
|
|
429
441
|
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
430
442
|
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
@@ -444,6 +456,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
444
456
|
[FLOW_PREVENT_CYCLES_ENV]: preventCycles ? "1" : "0",
|
|
445
457
|
[FLOW_TOOL_OPTIMIZE_ENV]: toolOptimize ? "1" : "0",
|
|
446
458
|
[PI_OFFLINE_ENV]: "1",
|
|
459
|
+
[FLOW_DEADLINE_ENV]: String(Date.now() + effectiveTimeout),
|
|
447
460
|
},
|
|
448
461
|
});
|
|
449
462
|
|
|
@@ -560,6 +573,23 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
560
573
|
|
|
561
574
|
// Execution timeout — kill child if it runs too long
|
|
562
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
|
+
|
|
563
593
|
const timeoutTimer = setTimeout(() => {
|
|
564
594
|
if (didClose || settled) return;
|
|
565
595
|
result.stderr += `\nFlow timed out after ${Math.round(effectiveTimeout / 1000)}s.`;
|
|
@@ -586,7 +616,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
586
616
|
const flowText = getFlowOutput(normalized.messages);
|
|
587
617
|
const extracted = extractStructuredOutput(flowText);
|
|
588
618
|
if (extracted) {
|
|
589
|
-
normalized.structuredOutput = extracted;
|
|
619
|
+
normalized.structuredOutput = enrichStructuredOutputCommands(extracted, normalized.messages);
|
|
590
620
|
}
|
|
591
621
|
}
|
|
592
622
|
|
package/src/index.ts
CHANGED
|
@@ -448,11 +448,7 @@ function renderCompressedFlowResult(r: CompressedFlowResult): string {
|
|
|
448
448
|
parts.push(`Files:\n${fileLines.join("\n")}`);
|
|
449
449
|
}
|
|
450
450
|
if (r.commands?.length) {
|
|
451
|
-
const cmdLines = r.commands.map((c) => {
|
|
452
|
-
const result = c.result ? ` (${c.result})` : "";
|
|
453
|
-
const purpose = c.purpose ? ` — ${c.purpose}` : "";
|
|
454
|
-
return ` ${c.tool ?? "cmd"}: ${c.command}${result}${purpose}`;
|
|
455
|
-
});
|
|
451
|
+
const cmdLines = r.commands.map((c) => ` ${c.tool ?? "cmd"}: ${c.command}`);
|
|
456
452
|
parts.push(`Commands:\n${cmdLines.join("\n")}`);
|
|
457
453
|
}
|
|
458
454
|
if (r.error) parts.push(`Error: ${r.error}`);
|
package/src/structured-output.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and validates it against the FlowStructuredOutput schema.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
8
9
|
import type { Action, CommandEntry, FileEntry, FlowStructuredOutput, NotDoneItem } from "./types.js";
|
|
9
10
|
|
|
10
11
|
type FlowStatus = FlowStructuredOutput["status"];
|
|
@@ -95,3 +96,64 @@ export function extractStructuredOutput(text: string): FlowStructuredOutput | un
|
|
|
95
96
|
...(parsed.extensions !== undefined ? { extensions: parsed.extensions } : {}),
|
|
96
97
|
};
|
|
97
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Walk the message history and mechanically replace paraphrased bash commands
|
|
102
|
+
* in a structured-output `commands` array with the exact verbatim strings from
|
|
103
|
+
* the actual tool calls. This fixes the common LLM behaviour of summarising
|
|
104
|
+
* `curl -s -X POST …` as `"curl GAWA baseline"`.
|
|
105
|
+
*
|
|
106
|
+
* Batch operations are flattened so that bash commands nested inside a batch
|
|
107
|
+
* call are included in the same chronological order they were executed.
|
|
108
|
+
*/
|
|
109
|
+
export function enrichStructuredOutputCommands(
|
|
110
|
+
structuredOutput: FlowStructuredOutput,
|
|
111
|
+
messages: Message[],
|
|
112
|
+
): FlowStructuredOutput {
|
|
113
|
+
// Collect actual verbatim bash commands (including those inside batch ops)
|
|
114
|
+
const actualBashCommands: string[] = [];
|
|
115
|
+
for (const msg of messages) {
|
|
116
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
117
|
+
for (const part of msg.content) {
|
|
118
|
+
if (part.type !== "toolCall") continue;
|
|
119
|
+
const name =
|
|
120
|
+
part.name ||
|
|
121
|
+
(part as unknown as { toolName?: string }).toolName ||
|
|
122
|
+
"";
|
|
123
|
+
const args = part.arguments || part.input || {};
|
|
124
|
+
|
|
125
|
+
if (name === "bash") {
|
|
126
|
+
const cmd = (args.command as string) || "";
|
|
127
|
+
if (cmd) actualBashCommands.push(cmd);
|
|
128
|
+
} else if (name === "batch") {
|
|
129
|
+
const ops = Array.isArray(args.o)
|
|
130
|
+
? args.o
|
|
131
|
+
: Array.isArray(args.op)
|
|
132
|
+
? args.op
|
|
133
|
+
: Array.isArray(args.operations)
|
|
134
|
+
? args.operations
|
|
135
|
+
: [];
|
|
136
|
+
for (const op of ops) {
|
|
137
|
+
if (!op) continue;
|
|
138
|
+
const opType = (op.o ?? op.op) as string;
|
|
139
|
+
if (opType === "bash" && op.command) {
|
|
140
|
+
actualBashCommands.push(op.command as string);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (actualBashCommands.length === 0) return structuredOutput;
|
|
148
|
+
|
|
149
|
+
// Replace paraphrased bash commands with actual verbatim ones, in order.
|
|
150
|
+
let bashIdx = 0;
|
|
151
|
+
const enrichedCommands = structuredOutput.commands.map((cmd) => {
|
|
152
|
+
if (cmd.tool === "bash" && bashIdx < actualBashCommands.length) {
|
|
153
|
+
return { command: actualBashCommands[bashIdx++], tool: "bash" };
|
|
154
|
+
}
|
|
155
|
+
return { command: cmd.command, tool: cmd.tool };
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return { ...structuredOutput, commands: enrichedCommands };
|
|
159
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -39,18 +39,10 @@ export interface FileEntry {
|
|
|
39
39
|
|
|
40
40
|
/** Structured command/tool invocation entry in a flow's output. */
|
|
41
41
|
export interface CommandEntry {
|
|
42
|
-
/** The command string or tool call
|
|
42
|
+
/** The exact verbatim command string or tool call that was executed. */
|
|
43
43
|
command: string;
|
|
44
44
|
/** Tool used: bash, grep, find, ls, batch, read, write, edit, flow, web. */
|
|
45
45
|
tool?: string;
|
|
46
|
-
/** Working directory or target scope (file path, URL, etc.). */
|
|
47
|
-
target?: string;
|
|
48
|
-
/** Whether it succeeded, failed, or was aborted. */
|
|
49
|
-
result?: "success" | "failure" | "partial" | "aborted";
|
|
50
|
-
/** High-level output or error excerpt (not full stdout). */
|
|
51
|
-
output?: string;
|
|
52
|
-
/** Why this command was run (1 sentence). */
|
|
53
|
-
purpose?: string;
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
/** Compressed representation of a flow result for child context inheritance. */
|