pi-agent-flow 1.2.7 → 1.3.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 +5 -3
- 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,7 +20,7 @@ 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;
|
|
@@ -227,6 +227,8 @@ function buildFlowArgs(
|
|
|
227
227
|
`\n\n## Structured Output\n\n` +
|
|
228
228
|
`End your response with a JSON code block containing:\n` +
|
|
229
229
|
`\n` +
|
|
230
|
+
`For every command you ran, include the exact verbatim command string in the \\"command\\" field (not a paraphrase or summary).\n` +
|
|
231
|
+
`\n` +
|
|
230
232
|
`\`\`\`json\n` +
|
|
231
233
|
`{\n` +
|
|
232
234
|
` "version": "1.0",\n` +
|
|
@@ -239,7 +241,7 @@ function buildFlowArgs(
|
|
|
239
241
|
` { "type": "read", "description": "what was done", "target": "file.ts", "result": "success", "evidence": "output or proof" }\n` +
|
|
240
242
|
` ],\n` +
|
|
241
243
|
` "commands": [\n` +
|
|
242
|
-
` { "command": "
|
|
244
|
+
` { "command": "curl -s -X POST https://api.example.com/v1/data -H 'Authorization: Bearer token'", "tool": "bash" }\n` +
|
|
243
245
|
` ],\n` +
|
|
244
246
|
` "notDone": [\n` +
|
|
245
247
|
` { "item": "unfinished work", "reason": "why it was not completed", "blocker": "blocking issue if any", "nextStep": "specific follow-up" }\n` +
|
|
@@ -586,7 +588,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
586
588
|
const flowText = getFlowOutput(normalized.messages);
|
|
587
589
|
const extracted = extractStructuredOutput(flowText);
|
|
588
590
|
if (extracted) {
|
|
589
|
-
normalized.structuredOutput = extracted;
|
|
591
|
+
normalized.structuredOutput = enrichStructuredOutputCommands(extracted, normalized.messages);
|
|
590
592
|
}
|
|
591
593
|
}
|
|
592
594
|
|
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. */
|