pi-agent-flow 1.0.1 → 1.0.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/flow.ts CHANGED
@@ -12,7 +12,7 @@ import * as path from "node:path";
12
12
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
13
13
  import { type FlowConfig, getFlowTier } from "./agents.js";
14
14
  import { parseFlowCliArgs } from "./runner-cli.js";
15
- import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate, drainCtxEstimate } from "./runner-events.js";
15
+ import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate, drainCtxEstimate, updateSmoothedTps, drainSmoothedTps } from "./runner-events.js";
16
16
  import {
17
17
  type SingleResult,
18
18
  type FlowDetails,
@@ -42,12 +42,14 @@ function mergeStreamingUsage(
42
42
  actual: SingleResult["usage"],
43
43
  estimatedOutputTokens: number,
44
44
  ctxEstimate: number,
45
+ smoothedTps: number,
45
46
  ): SingleResult["usage"] {
46
- if (estimatedOutputTokens <= 0 && ctxEstimate <= 0) return actual;
47
+ if (estimatedOutputTokens <= 0 && ctxEstimate <= 0 && smoothedTps <= 0) return actual;
47
48
  return {
48
49
  ...actual,
49
50
  ...(estimatedOutputTokens > 0 ? { output: Math.max(actual.output, estimatedOutputTokens) } : {}),
50
51
  ...(ctxEstimate > 0 ? { contextTokens: Math.max(actual.contextTokens, ctxEstimate) } : {}),
52
+ ...(smoothedTps > 0 ? { smoothedTps } : {}),
51
53
  };
52
54
  }
53
55
 
@@ -238,7 +240,9 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
238
240
  const streaming = drainStreamingText(result);
239
241
  const estimatedTokens = drainStreamingEstimate(result);
240
242
  const ctxEst = drainCtxEstimate(result);
241
- const mergedUsage = mergeStreamingUsage(result.usage, estimatedTokens, ctxEst);
243
+ updateSmoothedTps(result, estimatedTokens);
244
+ const smoothedTps = drainSmoothedTps(result);
245
+ const mergedUsage = mergeStreamingUsage(result.usage, estimatedTokens, ctxEst, smoothedTps);
242
246
  onUpdate?.({
243
247
  content: [
244
248
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/render-utils.ts CHANGED
@@ -37,11 +37,17 @@ export function formatFlowTypeName(type: string): string {
37
37
  return lower.padEnd(targetWidth, " ");
38
38
  }
39
39
 
40
+ /** Format tokens-per-second to a 5-char display (e.g., " 42.3", " -"). */
41
+ function formatTps(value: number | undefined): string {
42
+ if (!value || value <= 0) return " -";
43
+ return value.toFixed(1).padStart(5);
44
+ }
45
+
40
46
  export function formatCompactStats(usage: Partial<UsageStats>, model?: string, maxWidth?: number): string {
41
47
  const parts: string[] = [];
42
48
  parts.push(`↑ ${formatFixedTokens(usage.input || 0)}`);
43
49
  parts.push(`↓ ${formatFixedTokens(usage.output || 0)}`);
44
- parts.push(`act: ${formatFixedTokens(usage.toolCalls || 0)}`);
50
+ parts.push(`tps: ${formatTps(usage.smoothedTps)}`);
45
51
  parts.push(`ctx: ${formatFixedTokens(usage.contextTokens || 0)}`);
46
52
 
47
53
  let result = parts.join(" · ") + (model ? ` · ${model}` : "");
@@ -52,7 +58,7 @@ export function formatCompactStats(usage: Partial<UsageStats>, model?: string, m
52
58
  if (visibleLength(narrow) <= maxWidth) return narrow;
53
59
 
54
60
  // Drop context tokens
55
- const narrowParts = parts.slice(0, 3); // up to act
61
+ const narrowParts = parts.slice(0, 3); // up to tps
56
62
  narrow = narrowParts.join(" · ");
57
63
  if (visibleLength(narrow) <= maxWidth) return narrow;
58
64
 
package/render.ts CHANGED
@@ -249,12 +249,13 @@ function renderFlowCollapsed(
249
249
  container.addChild(new TruncatedText(`${theme.fg("dim", "├─ dir:")} ${theme.fg("dim", dirContent)}`, 0, 0));
250
250
  }
251
251
 
252
- // act: line (last tool call)
252
+ // act: line (last tool call with count)
253
253
  const lastTool = getLastToolCall(r.messages);
254
254
  if (lastTool) {
255
255
  const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
256
+ const actPrefix = `act: [${r.usage.toolCalls}] - `;
256
257
  const actContent = truncateChars(actStr, contentBudget(10));
257
- container.addChild(new TruncatedText(`${theme.fg("dim", "├─ act:")} ${actContent}`, 0, 0));
258
+ container.addChild(new TruncatedText(`${theme.fg("dim", "├─ " + actPrefix)}${actContent}`, 0, 0));
258
259
  }
259
260
 
260
261
  // log: line (last assistant text or streaming)
@@ -380,12 +381,13 @@ function renderActivityPanel(
380
381
  container.addChild(new TruncatedText(`${theme.fg("dim", indent + "├─ dir:")} ${theme.fg("dim", dirContent)}`, 0, 0));
381
382
  }
382
383
 
383
- // act: line (last tool call)
384
+ // act: line (last tool call with count)
384
385
  const lastTool = getLastToolCall(r.messages);
385
386
  if (lastTool) {
386
387
  const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
388
+ const actPrefix = `act: [${r.usage.toolCalls}] - `;
387
389
  const actContent = truncateChars(actStr, contentBudget(10));
388
- container.addChild(new TruncatedText(`${theme.fg("dim", indent + "├─ act:")} ${actContent}`, 0, 0));
390
+ container.addChild(new TruncatedText(`${theme.fg("dim", indent + "├─ " + actPrefix)}${actContent}`, 0, 0));
389
391
  }
390
392
 
391
393
  // log: line (last assistant text)
package/runner-events.js CHANGED
@@ -51,6 +51,9 @@ export function drainStreamingText(result) {
51
51
  /** Chars per token heuristic for output estimation. */
52
52
  const CHARS_PER_TOKEN = 4;
53
53
 
54
+ /** EMA smoothing factor for tokens-per-second (higher = more responsive). */
55
+ const EMA_ALPHA = 0.3;
56
+
54
57
  function getStreamingEstimate(result) {
55
58
  if (!Object.prototype.hasOwnProperty.call(result, "__streamingEstimate")) {
56
59
  Object.defineProperty(result, "__streamingEstimate", {
@@ -63,6 +66,62 @@ function getStreamingEstimate(result) {
63
66
  return result.__streamingEstimate;
64
67
  }
65
68
 
69
+ /**
70
+ * Lazily initialize TPS tracking properties on the result object.
71
+ * - __lastEmitTime: timestamp (ms) of the last streaming emit
72
+ * - __smoothedTps: EMA-smoothed tokens-per-second value
73
+ */
74
+ function getTpsTracker(result) {
75
+ if (!Object.prototype.hasOwnProperty.call(result, "__smoothedTps")) {
76
+ Object.defineProperty(result, "__smoothedTps", {
77
+ value: 0,
78
+ enumerable: false,
79
+ configurable: false,
80
+ writable: true,
81
+ });
82
+ Object.defineProperty(result, "__lastEmitTime", {
83
+ value: 0,
84
+ enumerable: false,
85
+ configurable: false,
86
+ writable: true,
87
+ });
88
+ }
89
+ return result;
90
+ }
91
+
92
+ /**
93
+ * Update the EMA-smoothed tokens-per-second based on a new sample.
94
+ * Called from emitUpdate() with the estimated output tokens since last emit.
95
+ * Skips the update when delta time or tokens are zero (e.g., first emit).
96
+ */
97
+ export function updateSmoothedTps(result, estimatedTokens) {
98
+ if (estimatedTokens <= 0) return;
99
+ const tracker = getTpsTracker(result);
100
+ const now = Date.now();
101
+ if (tracker.__lastEmitTime === 0) {
102
+ // First emit — seed the value directly
103
+ tracker.__lastEmitTime = now;
104
+ return;
105
+ }
106
+ const deltaSec = (now - tracker.__lastEmitTime) / 1000;
107
+ if (deltaSec <= 0) return;
108
+ const instantRate = estimatedTokens / deltaSec;
109
+ if (tracker.__smoothedTps === 0) {
110
+ tracker.__smoothedTps = instantRate;
111
+ } else {
112
+ tracker.__smoothedTps = EMA_ALPHA * instantRate + (1 - EMA_ALPHA) * tracker.__smoothedTps;
113
+ }
114
+ tracker.__lastEmitTime = now;
115
+ }
116
+
117
+ /**
118
+ * Return the current EMA-smoothed tokens-per-second value.
119
+ */
120
+ export function drainSmoothedTps(result) {
121
+ const tracker = getTpsTracker(result);
122
+ return tracker.__smoothedTps;
123
+ }
124
+
66
125
  /**
67
126
  * Lazily initialize ctx baseline tracking properties on the result object.
68
127
  * - __ctxBaseline: last known real totalTokens from message_end
@@ -279,22 +338,68 @@ export function getFlowFinalText(messages) {
279
338
  return "";
280
339
  }
281
340
 
341
+ function extractNonReadToolCalls(messages) {
342
+ const calls = [];
343
+ if (!Array.isArray(messages)) return calls;
344
+ for (const msg of messages) {
345
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
346
+ for (const part of msg.content) {
347
+ if (part.type === "toolCall" && part.name !== "read") {
348
+ calls.push({ name: part.name, args: part.arguments || part.input || {} });
349
+ }
350
+ }
351
+ }
352
+ return calls;
353
+ }
354
+
355
+ function formatToolCallShort(tc) {
356
+ const args = tc.args || {};
357
+ switch (tc.name) {
358
+ case "edit":
359
+ case "write":
360
+ return `${tc.name} ${(args.file_path || args.path || "?").split("/").pop()}`;
361
+ case "bash": {
362
+ const cmd = (args.command || "").replace(/[\n\r\t]+/g, " ").replace(/ +/g, " ").trim();
363
+ return `bash ${cmd.length > 40 ? cmd.slice(0, 40) + "..." : cmd}`;
364
+ }
365
+ case "grep":
366
+ return `grep /${args.pattern || "?"}/ in ${args.path || "."}`;
367
+ case "find":
368
+ return `find ${args.pattern || "*"} in ${args.path || "."}`;
369
+ case "ls":
370
+ return `ls ${args.path || "."}`;
371
+ default:
372
+ return tc.name;
373
+ }
374
+ }
375
+
282
376
  export function getFlowSummaryText(result) {
283
377
  const finalText = getFlowFinalText(result?.messages);
284
378
  if (finalText) return finalText;
285
379
 
286
- if (typeof result?.errorMessage === "string" && result.errorMessage.trim()) {
287
- return result.errorMessage.trim();
288
- }
289
-
290
380
  const isError =
291
381
  (typeof result?.exitCode === "number" && result.exitCode > 0) ||
292
382
  result?.stopReason === "error" ||
293
383
  result?.stopReason === "aborted";
294
384
 
295
- if (isError && typeof result?.stderr === "string" && result.stderr.trim()) {
296
- return result.stderr.trim();
385
+ // Build base message for failed/aborted flows
386
+ let base = "";
387
+ if (typeof result?.errorMessage === "string" && result.errorMessage.trim()) {
388
+ base = result.errorMessage.trim();
389
+ } else if (isError && typeof result?.stderr === "string" && result.stderr.trim()) {
390
+ base = result.stderr.trim();
391
+ } else if (isError) {
392
+ base = "Flow failed";
393
+ } else {
394
+ return "(no output)";
395
+ }
396
+
397
+ // Surface partial tool calls (excluding read) for failed/aborted flows
398
+ const toolCalls = extractNonReadToolCalls(result?.messages);
399
+ if (toolCalls.length > 0) {
400
+ const formatted = toolCalls.map(formatToolCallShort).join(", ");
401
+ return `${base}\nPartial work: ${formatted}`;
297
402
  }
298
403
 
299
- return "(no output)";
404
+ return base;
300
405
  }
package/types.ts CHANGED
@@ -15,6 +15,7 @@ export interface UsageStats {
15
15
  contextTokens: number;
16
16
  turns: number;
17
17
  toolCalls: number;
18
+ smoothedTps?: number;
18
19
  }
19
20
 
20
21
  /** Result of a single flow invocation. */
@@ -47,7 +48,7 @@ export type DisplayItem =
47
48
 
48
49
  /** Create an empty UsageStats object. */
49
50
  export function emptyFlowUsage(): UsageStats {
50
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0, toolCalls: 0 };
51
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0, toolCalls: 0, smoothedTps: 0 };
51
52
  }
52
53
 
53
54
  /** Sum usage across multiple results. */
@@ -61,6 +62,9 @@ export function aggregateFlowUsage(results: SingleResult[]): UsageStats {
61
62
  total.cost += r.usage.cost;
62
63
  total.turns += r.usage.turns;
63
64
  total.toolCalls += r.usage.toolCalls;
65
+ if ((r.usage.smoothedTps ?? 0) > (total.smoothedTps ?? 0)) {
66
+ total.smoothedTps = r.usage.smoothedTps;
67
+ }
64
68
  }
65
69
  return total;
66
70
  }