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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "1.2.7",
3
+ "version": "1.3.0",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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
@@ -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": "npm test", "tool": "bash", "target": ".", "result": "success", "output": "12 passing, 2 failing", "purpose": "Run test suite to verify fix" }\n` +
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}`);
@@ -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 representation. */
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. */