pi-agent-flow 0.1.0-alpha.2 → 0.1.0-alpha.3

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 } from "./agents.js";
14
14
  import { parseFlowCliArgs } from "./runner-cli.js";
15
- import { processFlowJsonLine } from "./runner-events.js";
15
+ import { processFlowJsonLine, drainStreamingText } from "./runner-events.js";
16
16
  import {
17
17
  type SingleResult,
18
18
  type FlowDetails,
@@ -224,11 +224,12 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
224
224
  };
225
225
 
226
226
  const emitUpdate = () => {
227
+ const streaming = drainStreamingText(result);
227
228
  onUpdate?.({
228
229
  content: [
229
230
  {
230
231
  type: "text",
231
- text: getFlowOutput(result.messages) || "(running...)",
232
+ text: streaming || getFlowOutput(result.messages) || "(running...)",
232
233
  },
233
234
  ],
234
235
  details: makeDetails([result]),
package/index.ts CHANGED
@@ -379,9 +379,8 @@ Multiple independent flows? Batch them into one call:
379
379
 
380
380
  Each call renders as:
381
381
 
382
- routing to:
383
- • flow [explore] — Map the full directory structure...
384
- • flow [review] — Audit security and quality...
382
+ flow [explore] — Map the full directory structure...
383
+ • flow [review] — Audit security and quality...
385
384
 
386
385
  Each flow returns:
387
386
 
@@ -489,10 +488,12 @@ flow [type] accomplished
489
488
  };
490
489
  }
491
490
 
492
- const emitProgress = () => {
491
+ let lastStreamingText = "";
492
+ const emitProgress = (streamingText?: string) => {
493
493
  if (!onUpdate) return;
494
+ if (streamingText) lastStreamingText = streamingText;
494
495
  onUpdate({
495
- content: [{ type: "text", text: `Flow: ${allResults.filter((r) => r.exitCode !== -1).length}/${allResults.length} done` }],
496
+ content: [{ type: "text", text: lastStreamingText || "" }],
496
497
  details: makeDetails([...allResults]),
497
498
  });
498
499
  };
@@ -527,7 +528,7 @@ flow [type] accomplished
527
528
  onUpdate: (partial) => {
528
529
  if (partial.details?.results[0]) {
529
530
  allResults[index] = partial.details.results[0];
530
- emitProgress();
531
+ emitProgress(partial.content?.[0]?.text);
531
532
  }
532
533
  },
533
534
  makeDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -36,10 +36,11 @@
36
36
  },
37
37
  "scripts": {
38
38
  "lint": "tsc --noEmit",
39
- "test": "echo \"no tests yet\" && exit 0"
39
+ "test": "vitest run"
40
40
  },
41
41
  "devDependencies": {
42
- "typescript": "^5.0.0"
42
+ "typescript": "^5.0.0",
43
+ "vitest": "^3.2.4"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "@mariozechner/pi-agent-core": ">=0.37.0",
package/render.ts CHANGED
@@ -20,31 +20,7 @@ import {
20
20
  isFlowError,
21
21
  isFlowSuccess,
22
22
  } from "./types.js";
23
-
24
- // ---------------------------------------------------------------------------
25
- // Formatting helpers
26
- // ---------------------------------------------------------------------------
27
-
28
- function formatTokens(count: number): string {
29
- if (count < 1000) return count.toString();
30
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
31
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
32
- return `${(count / 1000000).toFixed(1)}M`;
33
- }
34
-
35
- function formatFlowUsage(usage: Partial<UsageStats>, model?: string): string {
36
- const parts: string[] = [];
37
- if (usage.toolCalls && usage.toolCalls > 0) parts.push(`${usage.toolCalls} calls`);
38
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
39
- if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
40
- if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
41
- if (usage.cacheRead) parts.push(`CR:${formatTokens(usage.cacheRead)}`);
42
- if (usage.cacheWrite) parts.push(`CW:${formatTokens(usage.cacheWrite)}`);
43
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
44
- if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
45
- if (model) parts.push(model);
46
- return parts.join(" ");
47
- }
23
+ import { formatTokens, formatFlowUsage, formatCompactStats, truncateChars, tailText } from "./render-utils.js";
48
24
 
49
25
  function shortenPath(p: string): string {
50
26
  const home = os.homedir();
@@ -140,25 +116,8 @@ function truncateText(text: string): string {
140
116
  export function renderFlowCall(args: Record<string, any>, theme: { fg: ThemeFg; bold: (s: string) => string }): Text {
141
117
  const flows = args.flow as Array<{ type: string; intent: string }> | undefined;
142
118
 
143
- if (flows && flows.length > 0) {
144
- if (flows.length === 1) {
145
- const f = flows[0];
146
- const text =
147
- theme.fg("toolTitle", "routing to ") +
148
- theme.fg("accent", `flow [${f.type}]`) +
149
- theme.fg("dim", ` — ${truncateText(f.intent)}`);
150
- return new Text(text, 0, 0);
151
- }
152
-
153
- let text = theme.fg("toolTitle", "routing to:");
154
- for (const f of flows) {
155
- text +=
156
- `\n ${theme.fg("muted", "•")} ` +
157
- theme.fg("accent", `flow [${f.type}]`) +
158
- theme.fg("dim", ` — ${truncateText(f.intent)}`);
159
- }
160
- return new Text(text, 0, 0);
161
- }
119
+ // Minimal renderFlowResult owns the full display
120
+ return new Text("", 0, 0);
162
121
 
163
122
  return new Text(theme.fg("muted", "(empty flow call)"), 0, 0);
164
123
  }
@@ -173,13 +132,14 @@ export function renderFlowResult(
173
132
  theme: { fg: ThemeFg; bold: (s: string) => string },
174
133
  ): Container | Text {
175
134
  const details = result.details as FlowDetails | undefined;
135
+ const streamingText = result.content?.[0]?.type === "text" ? result.content[0].text : undefined;
136
+
176
137
  if (!details || details.results.length === 0) {
177
- const first = result.content[0];
178
- return new Text(first?.type === "text" && first.text ? first.text : "(no output)", 0, 0);
138
+ return new Text(streamingText || "", 0, 0);
179
139
  }
180
140
 
181
141
  if (details.results.length === 1) {
182
- return renderSingleFlowResult(details.results[0], expanded, theme);
142
+ return renderSingleFlowResult(details.results[0], expanded, theme, streamingText);
183
143
  }
184
144
 
185
145
  return renderMultiFlowResult(details, expanded, theme);
@@ -193,6 +153,7 @@ function renderSingleFlowResult(
193
153
  r: SingleResult,
194
154
  expanded: boolean,
195
155
  theme: { fg: ThemeFg; bold: (s: string) => string },
156
+ streamingText?: string,
196
157
  ): Container | Text {
197
158
  const error = isFlowError(r);
198
159
  const icon = flowStatusIcon(r, theme);
@@ -202,7 +163,7 @@ function renderSingleFlowResult(
202
163
  if (expanded) {
203
164
  return renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme);
204
165
  }
205
- return renderFlowCollapsed(r, icon, error, flowOutput, theme);
166
+ return renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText);
206
167
  }
207
168
 
208
169
  function renderFlowExpanded(
@@ -257,24 +218,34 @@ function renderFlowExpanded(
257
218
  return container;
258
219
  }
259
220
 
221
+
222
+
260
223
  function renderFlowCollapsed(
261
224
  r: SingleResult,
262
225
  icon: string,
263
226
  error: boolean,
264
227
  flowOutput: string,
265
228
  theme: { fg: ThemeFg; bold: (s: string) => string },
229
+ streamingText?: string,
266
230
  ): Text {
267
- const usageStr = formatFlowUsage(r.usage, r.model);
268
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(`[${r.type}]`))}${theme.fg("muted", ` (${r.agentSource})`)}`;
269
- if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
231
+ const stats = formatCompactStats(r.usage, r.model);
232
+ let text = `${theme.fg("accent", r.type)}${theme.fg("dim", stats)}`;
270
233
  if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
271
234
 
272
- if (error && r.errorMessage) {
273
- text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
274
- } else if (flowOutput) {
275
- text += `\n${renderFlowReport(truncateText(flowOutput), theme)}`;
276
- } else {
277
- text += `\n${theme.fg(error ? "error" : "muted", getFlowSummaryText(r))}`;
235
+ // Intent line
236
+ const hasOutput = !!(flowOutput || streamingText || (error && r.errorMessage));
237
+ if (r.intent) {
238
+ const prefix = hasOutput ? "├" : "└";
239
+ text += `\n${theme.fg("muted", `${prefix} int:`)} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
240
+ }
241
+
242
+ // Output line
243
+ if (flowOutput) {
244
+ text += `\n${theme.fg("muted", "└─ msg:")} ${renderFlowReport(truncateChars(flowOutput, 25), theme)}`;
245
+ } else if (streamingText) {
246
+ text += `\n${theme.fg("muted", "└─ msg:")} ${theme.fg("dim", tailText(streamingText, 40))}`;
247
+ } else if (error && r.errorMessage) {
248
+ text += `\n${theme.fg("muted", "└─ msg:")} ${theme.fg("error", truncateChars(r.errorMessage, 25))}`;
278
249
  }
279
250
 
280
251
  return new Text(text, 0, 0);
@@ -358,15 +329,12 @@ function renderMultiFlowCollapsed(
358
329
  let text = `${icon} ${theme.fg("toolTitle", theme.bold("flow "))}${theme.fg("accent", `${successCount}/${results.length} flows`)}`;
359
330
 
360
331
  for (const r of results) {
361
- const rIcon = flowStatusIcon(r, theme);
362
332
  const flowOutput = getFlowOutput(r.messages);
363
333
  const usageStr = formatFlowUsage(r.usage, r.model);
364
- text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", `[${r.type}]`)} ${rIcon}`;
365
- if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
334
+ text += `\n${theme.fg("accent", `flow [${r.type}]`)}`;
335
+ if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
366
336
  if (flowOutput) {
367
337
  text += `\n${renderFlowReport(truncateText(flowOutput), theme)}`;
368
- } else {
369
- text += `\n${theme.fg("muted", getFlowSummaryText(r))}`;
370
338
  }
371
339
  }
372
340
 
package/runner-events.js CHANGED
@@ -14,6 +14,51 @@ function getSeenFlowMessageSignatures(result) {
14
14
  return result.__seenMessageSignatures;
15
15
  }
16
16
 
17
+ function getStreamingTextBuffer(result) {
18
+ if (!Object.prototype.hasOwnProperty.call(result, "__streamingTextBuffer")) {
19
+ Object.defineProperty(result, "__streamingTextBuffer", {
20
+ value: "",
21
+ enumerable: false,
22
+ configurable: false,
23
+ writable: true,
24
+ });
25
+ Object.defineProperty(result, "__lastEmittedWordCount", {
26
+ value: 0,
27
+ enumerable: false,
28
+ configurable: false,
29
+ writable: true,
30
+ });
31
+ }
32
+ return result.__streamingTextBuffer;
33
+ }
34
+
35
+ /**
36
+ * Drain the accumulated streaming text buffer and return it.
37
+ * Updates the last-emitted word count for threshold tracking.
38
+ */
39
+ export function drainStreamingText(result) {
40
+ const buf = getStreamingTextBuffer(result);
41
+ if (!buf) return "";
42
+ result.__streamingTextBuffer = "";
43
+ result.__lastEmittedWordCount = 0;
44
+ return buf;
45
+ }
46
+
47
+ /**
48
+ * Accumulate a text or thinking delta into the streaming buffer.
49
+ * Returns true if the caller should emit an update.
50
+ */
51
+ function accumulateStreamingDelta(result, delta) {
52
+ if (!delta) return false;
53
+ const buf = getStreamingTextBuffer(result);
54
+ result.__streamingTextBuffer = buf + delta;
55
+ if (result.__streamingTextBuffer.length - result.__lastEmittedWordCount >= 40) {
56
+ result.__lastEmittedWordCount = result.__streamingTextBuffer.length;
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+
17
62
  function stableStringify(value) {
18
63
  if (value === null || typeof value !== "object") {
19
64
  return JSON.stringify(value);
@@ -98,6 +143,18 @@ export function processFlowEvent(event, result) {
98
143
  result.sawAgentEnd = true;
99
144
  return addFlowAssistantMessages(result, event.messages);
100
145
 
146
+ case "message_update": {
147
+ const evt = event.assistantMessageEvent;
148
+ if (!evt || typeof evt !== "object") return false;
149
+ if (evt.type === "text_delta") {
150
+ return accumulateStreamingDelta(result, evt.delta);
151
+ }
152
+ if (evt.type === "thinking_delta") {
153
+ return accumulateStreamingDelta(result, evt.delta);
154
+ }
155
+ return false;
156
+ }
157
+
101
158
  default:
102
159
  return false;
103
160
  }