pi-agent-flow 1.1.0 → 1.2.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.
@@ -10,8 +10,8 @@ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
13
- import { type FlowConfig, getFlowTier } from "./agents.js";
14
- import { parseFlowCliArgs } from "./runner-cli.js";
13
+ import { type FlowConfig } from "./agents.js";
14
+ import { parseFlowCliArgs } from "./cli-args.js";
15
15
  import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate, drainCtxEstimate, updateSmoothedTps, drainSmoothedTps } from "./runner-events.js";
16
16
  import {
17
17
  type SingleResult,
@@ -20,6 +20,7 @@ import {
20
20
  getFlowOutput,
21
21
  normalizeFlowResult,
22
22
  } from "./types.js";
23
+ import { extractStructuredOutput } from "./structured-output.js";
23
24
 
24
25
  const isWindows = process.platform === "win32";
25
26
  const SIGKILL_TIMEOUT_MS = 5000;
@@ -52,8 +53,8 @@ function mergeStreamingUsage(
52
53
  ...actual,
53
54
  ...(estimatedOutputTokens > 0 ? { output: Math.max(actual.output, estimatedOutputTokens) } : {}),
54
55
  ...(ctxEstimate > 0 ? { contextTokens: Math.max(actual.contextTokens, ctxEstimate) } : {}),
55
- // Preserve the peak smoothedTPS seen during the stream rather than the live EMA value.
56
- ...(smoothedTps > 0 ? { smoothedTps: Math.max(actual.smoothedTps || 0, smoothedTps) } : {}),
56
+ // Show the live EMA value so the dashboard can rise and fall smoothly.
57
+ ...(smoothedTps > 0 ? { smoothedTps } : {}),
57
58
  };
58
59
  }
59
60
 
@@ -117,7 +118,7 @@ export function getOptimizedTools(
117
118
  );
118
119
  if (!hasLegacyTools) return flowTools;
119
120
  const filtered = flowTools.filter(
120
- (t) => t !== "read" && t !== "write" && t !== "edit" && t !== "batch",
121
+ (t) => t !== "read" && t !== "write" && t !== "edit" && t !== "batch" && t !== "batch_read",
121
122
  );
122
123
  return filtered.includes("batch")
123
124
  ? filtered
@@ -128,10 +129,11 @@ function buildFlowArgs(
128
129
  flow: FlowConfig,
129
130
  intent: string,
130
131
  forkSessionPath: string | null,
131
- tieredModels?: { lite?: string; flash?: string; full?: string },
132
+ model?: string,
132
133
  parentDepth: number = 0,
133
134
  maxDepth: number = 0,
134
135
  toolOptimize: boolean = false,
136
+ structuredOutput: boolean = true,
135
137
  ): string[] {
136
138
  const args: string[] = [
137
139
  "--mode",
@@ -145,10 +147,21 @@ function buildFlowArgs(
145
147
  args.push("--session", forkSessionPath);
146
148
  }
147
149
 
148
- const tier = getFlowTier(flow.name);
149
- const tierModel = tieredModels?.[tier] ?? inheritedCliArgs.tieredModels?.[tier];
150
- const model = flow.model ?? tierModel ?? inheritedCliArgs.fallbackModel;
151
- if (model) args.push("--model", model);
150
+ if (inheritedCliArgs.flowModelConfig) {
151
+ args.push("--flow-model-config", inheritedCliArgs.flowModelConfig);
152
+ }
153
+ if (inheritedCliArgs.tieredModels?.lite) {
154
+ args.push("--flow-lite-model", inheritedCliArgs.tieredModels.lite);
155
+ }
156
+ if (inheritedCliArgs.tieredModels?.flash) {
157
+ args.push("--flow-flash-model", inheritedCliArgs.tieredModels.flash);
158
+ }
159
+ if (inheritedCliArgs.tieredModels?.full) {
160
+ args.push("--flow-full-model", inheritedCliArgs.tieredModels.full);
161
+ }
162
+
163
+ const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
164
+ if (resolvedModel) args.push("--model", resolvedModel);
152
165
 
153
166
  const thinking = flow.thinking ?? inheritedCliArgs.fallbackThinking;
154
167
  if (thinking) args.push("--thinking", thinking);
@@ -199,8 +212,42 @@ function buildFlowArgs(
199
212
  `</activation>`;
200
213
 
201
214
  // Phase 3: Directive — the flow's system prompt (renamed from <system-directive>)
202
- const directive = flow.systemPrompt.trim()
203
- ? `\n\n<directive>\n${flow.systemPrompt.trim()}\n</directive>`
215
+ let directiveBody = flow.systemPrompt.trim();
216
+
217
+ // Append structured output instructions when enabled
218
+ if (structuredOutput && directiveBody) {
219
+ directiveBody +=
220
+ `\n\n## Structured Output\n\n` +
221
+ `End your response with a JSON code block containing:\n` +
222
+ `\n` +
223
+ `\`\`\`json\n` +
224
+ `{\n` +
225
+ ` "version": "1.0",\n` +
226
+ ` "status": "complete",\n` +
227
+ ` "summary": "2-3 sentence summary of what was accomplished",\n` +
228
+ ` "files": [\n` +
229
+ ` { "path": "relative/path", "role": "read", "description": "why it matters", "snippet": "short excerpt", "ranges": [{ "start": 10, "end": 25, "label": "bug" }] }\n` +
230
+ ` ],\n` +
231
+ ` "actions": [\n` +
232
+ ` { "type": "read", "description": "what was done", "target": "file.ts", "result": "success", "evidence": "output or proof" }\n` +
233
+ ` ],\n` +
234
+ ` "commands": [\n` +
235
+ ` { "command": "npm test", "tool": "bash", "target": ".", "result": "success", "output": "12 passing, 2 failing", "purpose": "Run test suite to verify fix" }\n` +
236
+ ` ],\n` +
237
+ ` "notDone": [\n` +
238
+ ` { "item": "unfinished work", "reason": "why it was not completed", "blocker": "blocking issue if any", "nextStep": "specific follow-up" }\n` +
239
+ ` ],\n` +
240
+ ` "nextSteps": ["recommended follow-up action"],\n` +
241
+ ` "reasoning": ["key hypothesis or inference"],\n` +
242
+ ` "notes": ["observation or warning"]\n` +
243
+ `}\n` +
244
+ `\`\`\`\n` +
245
+ `\n` +
246
+ `Only include fields that have data. Omit empty arrays; missing array fields are acceptable. Keep snippets under 300 characters. List at most 10 files, 10 actions, 10 commands, and 10 notDone items. If you cannot produce valid structured output, omit the JSON block entirely.`;
247
+ }
248
+
249
+ const directive = directiveBody
250
+ ? `\n\n<directive>\n${directiveBody}\n</directive>`
204
251
  : "";
205
252
 
206
253
  // Phase 4: Mission — the intent wrapped with execution contract
@@ -245,8 +292,10 @@ export interface RunFlowOptions {
245
292
  preventCycles: boolean;
246
293
  /** Whether to transform tool lists to use batch. */
247
294
  toolOptimize?: boolean;
248
- /** Tiered model overrides (lite/flash/full). */
249
- tieredModels?: { lite?: string; flash?: string; full?: string };
295
+ /** Whether to inject structured JSON output instructions. Default: true. */
296
+ structuredOutput?: boolean;
297
+ /** Explicit model to use for this flow execution. */
298
+ model?: string;
250
299
  /** Abort signal for cancellation. */
251
300
  signal?: AbortSignal;
252
301
  /** Streaming update callback. */
@@ -276,6 +325,8 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
276
325
  maxDepth,
277
326
  preventCycles,
278
327
  toolOptimize = false,
328
+ structuredOutput = true,
329
+ model,
279
330
  signal,
280
331
  onUpdate,
281
332
  makeDetails,
@@ -297,7 +348,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
297
348
  };
298
349
  }
299
350
 
300
- const resolvedModel = flow.model ?? inheritedCliArgs.fallbackModel;
351
+ const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
301
352
  const result: SingleResult = {
302
353
  type: normalizedFlowName,
303
354
  agentSource: flow.source,
@@ -310,21 +361,30 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
310
361
  model: resolvedModel,
311
362
  };
312
363
 
364
+ let liveStreamingText = "";
365
+ let liveEstimatedOutputTokens = 0;
366
+ let lastActualOutputTokens = result.usage.output;
313
367
  const emitUpdate = () => {
314
- const streaming = drainStreamingText(result);
368
+ const streamingDelta = drainStreamingText(result);
369
+ if (streamingDelta) liveStreamingText += streamingDelta;
315
370
  const estimatedTokens = drainStreamingEstimate(result);
371
+ if (result.usage.output !== lastActualOutputTokens) {
372
+ lastActualOutputTokens = result.usage.output;
373
+ liveEstimatedOutputTokens = result.usage.output;
374
+ }
375
+ liveEstimatedOutputTokens += estimatedTokens;
316
376
  const ctxEst = drainCtxEstimate(result);
317
377
  updateSmoothedTps(result, estimatedTokens);
318
378
  const smoothedTps = drainSmoothedTps(result);
319
- const mergedUsage = mergeStreamingUsage(result.usage, estimatedTokens, ctxEst, smoothedTps);
379
+ const mergedUsage = mergeStreamingUsage(result.usage, liveEstimatedOutputTokens, ctxEst, smoothedTps);
320
380
  onUpdate?.({
321
381
  content: [
322
382
  {
323
383
  type: "text",
324
- text: streaming || getFlowOutput(result.messages) || "(running...)",
384
+ text: liveStreamingText || getFlowOutput(result.messages) || "(running...)",
325
385
  },
326
386
  ],
327
- details: makeDetails([{ ...result, usage: mergedUsage }]),
387
+ details: makeDetails([{ ...result, usage: mergedUsage, streamingText: liveStreamingText || undefined }]),
328
388
  });
329
389
  };
330
390
 
@@ -342,10 +402,11 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
342
402
  flow,
343
403
  intent,
344
404
  forkSessionTmpPath,
345
- opts.tieredModels,
405
+ model,
346
406
  parentDepth,
347
407
  maxDepth,
348
408
  toolOptimize,
409
+ structuredOutput,
349
410
  );
350
411
  let wasAborted = false;
351
412
 
@@ -511,7 +572,18 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
511
572
  result.usage.smoothedTps = finalSmoothedTps;
512
573
  }
513
574
 
514
- return normalizeFlowResult(result, wasAborted);
575
+ const normalized = normalizeFlowResult(result, wasAborted);
576
+
577
+ // Extract structured JSON output from the final assistant text
578
+ if (structuredOutput) {
579
+ const flowText = getFlowOutput(normalized.messages);
580
+ const extracted = extractStructuredOutput(flowText);
581
+ if (extracted) {
582
+ normalized.structuredOutput = extracted;
583
+ }
584
+ }
585
+
586
+ return normalized;
515
587
  } finally {
516
588
  cleanupFlowTempDir(forkSessionTmpDir);
517
589
  }
@@ -104,19 +104,19 @@ registerHook({
104
104
  },
105
105
  });
106
106
 
107
- /** Suggest explore flow after a successful audit flow. */
107
+ /** Suggest scout flow after a successful audit flow. */
108
108
  registerHook({
109
- name: "pi-agent-flow/audit-to-explore",
109
+ name: "pi-agent-flow/audit-to-scout",
110
110
  trigger: { flowTypes: ["audit"], onlyOnSuccess: true },
111
111
  action: (ctx) => {
112
- const exploreWasRequested = ctx.params.some(
113
- (p) => p.type.toLowerCase() === "explore",
112
+ const scoutWasRequested = ctx.params.some(
113
+ (p) => p.type.toLowerCase() === "scout",
114
114
  );
115
- if (exploreWasRequested) return null;
115
+ if (scoutWasRequested) return null;
116
116
 
117
117
  return {
118
118
  content:
119
- "Audit complete. Consider running an [explore] flow to review the audit findings.",
119
+ "Audit complete. Consider running a [scout] flow to trace the audit findings across the codebase.",
120
120
  priority: 15,
121
121
  };
122
122
  },