pi-agent-flow 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +3 -2
- package/index.ts +7 -6
- package/package.json +4 -3
- package/render.ts +69 -73
- package/runner-events.js +57 -0
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
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": "
|
|
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();
|
|
@@ -52,6 +28,8 @@ function shortenPath(p: string): string {
|
|
|
52
28
|
}
|
|
53
29
|
|
|
54
30
|
type ThemeFg = (color: string, text: string) => string;
|
|
31
|
+
type ThemeBg = (color: string, text: string) => string;
|
|
32
|
+
type FlowTheme = { fg: ThemeFg; bold: (s: string) => string; bg: ThemeBg };
|
|
55
33
|
|
|
56
34
|
function formatFlowToolCall(toolName: string, args: Record<string, unknown>, fg: ThemeFg): string {
|
|
57
35
|
const pathArg = (args.file_path || args.path || "...") as string;
|
|
@@ -137,28 +115,11 @@ function truncateText(text: string): string {
|
|
|
137
115
|
return `${words.slice(0, 3).join(" ")} ... ${words.slice(-8).join(" ")}`;
|
|
138
116
|
}
|
|
139
117
|
|
|
140
|
-
export function renderFlowCall(args: Record<string, any>, theme:
|
|
118
|
+
export function renderFlowCall(args: Record<string, any>, theme: FlowTheme): Text {
|
|
141
119
|
const flows = args.flow as Array<{ type: string; intent: string }> | undefined;
|
|
142
120
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
}
|
|
121
|
+
// Minimal — renderFlowResult owns the full display
|
|
122
|
+
return new Text("", 0, 0);
|
|
162
123
|
|
|
163
124
|
return new Text(theme.fg("muted", "(empty flow call)"), 0, 0);
|
|
164
125
|
}
|
|
@@ -170,16 +131,17 @@ export function renderFlowCall(args: Record<string, any>, theme: { fg: ThemeFg;
|
|
|
170
131
|
export function renderFlowResult(
|
|
171
132
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
172
133
|
expanded: boolean,
|
|
173
|
-
theme:
|
|
134
|
+
theme: FlowTheme,
|
|
174
135
|
): Container | Text {
|
|
175
136
|
const details = result.details as FlowDetails | undefined;
|
|
137
|
+
const streamingText = result.content?.[0]?.type === "text" ? result.content[0].text : undefined;
|
|
138
|
+
|
|
176
139
|
if (!details || details.results.length === 0) {
|
|
177
|
-
|
|
178
|
-
return new Text(first?.type === "text" && first.text ? first.text : "(no output)", 0, 0);
|
|
140
|
+
return new Text(streamingText || "", 0, 0);
|
|
179
141
|
}
|
|
180
142
|
|
|
181
143
|
if (details.results.length === 1) {
|
|
182
|
-
return renderSingleFlowResult(details.results[0], expanded, theme);
|
|
144
|
+
return renderSingleFlowResult(details.results[0], expanded, theme, streamingText);
|
|
183
145
|
}
|
|
184
146
|
|
|
185
147
|
return renderMultiFlowResult(details, expanded, theme);
|
|
@@ -192,7 +154,8 @@ export function renderFlowResult(
|
|
|
192
154
|
function renderSingleFlowResult(
|
|
193
155
|
r: SingleResult,
|
|
194
156
|
expanded: boolean,
|
|
195
|
-
theme:
|
|
157
|
+
theme: FlowTheme,
|
|
158
|
+
streamingText?: string,
|
|
196
159
|
): Container | Text {
|
|
197
160
|
const error = isFlowError(r);
|
|
198
161
|
const icon = flowStatusIcon(r, theme);
|
|
@@ -202,7 +165,7 @@ function renderSingleFlowResult(
|
|
|
202
165
|
if (expanded) {
|
|
203
166
|
return renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme);
|
|
204
167
|
}
|
|
205
|
-
return renderFlowCollapsed(r, icon, error, flowOutput, theme);
|
|
168
|
+
return renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText);
|
|
206
169
|
}
|
|
207
170
|
|
|
208
171
|
function renderFlowExpanded(
|
|
@@ -211,7 +174,7 @@ function renderFlowExpanded(
|
|
|
211
174
|
error: boolean,
|
|
212
175
|
displayItems: DisplayItem[],
|
|
213
176
|
flowOutput: string,
|
|
214
|
-
theme:
|
|
177
|
+
theme: FlowTheme,
|
|
215
178
|
): Container {
|
|
216
179
|
const mdTheme = getMarkdownTheme();
|
|
217
180
|
const container = new Container();
|
|
@@ -257,24 +220,34 @@ function renderFlowExpanded(
|
|
|
257
220
|
return container;
|
|
258
221
|
}
|
|
259
222
|
|
|
223
|
+
|
|
224
|
+
|
|
260
225
|
function renderFlowCollapsed(
|
|
261
226
|
r: SingleResult,
|
|
262
227
|
icon: string,
|
|
263
228
|
error: boolean,
|
|
264
229
|
flowOutput: string,
|
|
265
|
-
theme:
|
|
230
|
+
theme: FlowTheme,
|
|
231
|
+
streamingText?: string,
|
|
266
232
|
): Text {
|
|
267
|
-
const
|
|
268
|
-
let text = `${
|
|
269
|
-
if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
|
|
233
|
+
const stats = formatCompactStats(r.usage, r.model);
|
|
234
|
+
let text = `${theme.bg("selectedBg", theme.fg("accent", theme.bold(r.type)))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
270
235
|
if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
271
236
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
237
|
+
// Intent line
|
|
238
|
+
const hasOutput = !!(flowOutput || streamingText || (error && r.errorMessage));
|
|
239
|
+
if (r.intent) {
|
|
240
|
+
const prefix = hasOutput ? "├" : "└";
|
|
241
|
+
text += `\n${theme.fg("dim", `${prefix}─ int:`)} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Output line
|
|
245
|
+
if (flowOutput) {
|
|
246
|
+
text += `\n${theme.fg("dim", "└─ msg:")} ${renderFlowReport(truncateChars(flowOutput, 25), theme)}`;
|
|
247
|
+
} else if (streamingText) {
|
|
248
|
+
text += `\n${theme.fg("dim", "└─ msg:")} ${theme.fg("dim", tailText(streamingText, 40))}`;
|
|
249
|
+
} else if (error && r.errorMessage) {
|
|
250
|
+
text += `\n${theme.fg("dim", "└─ msg:")} ${theme.fg("error", truncateChars(r.errorMessage, 25))}`;
|
|
278
251
|
}
|
|
279
252
|
|
|
280
253
|
return new Text(text, 0, 0);
|
|
@@ -287,7 +260,7 @@ function renderFlowCollapsed(
|
|
|
287
260
|
function renderMultiFlowResult(
|
|
288
261
|
details: FlowDetails,
|
|
289
262
|
expanded: boolean,
|
|
290
|
-
theme:
|
|
263
|
+
theme: FlowTheme,
|
|
291
264
|
): Container | Text {
|
|
292
265
|
const results = details.results;
|
|
293
266
|
const successCount = results.filter((r) => isFlowSuccess(r)).length;
|
|
@@ -304,7 +277,7 @@ function renderMultiFlowExpanded(
|
|
|
304
277
|
results: SingleResult[],
|
|
305
278
|
successCount: number,
|
|
306
279
|
icon: string,
|
|
307
|
-
theme:
|
|
280
|
+
theme: FlowTheme,
|
|
308
281
|
): Container {
|
|
309
282
|
const mdTheme = getMarkdownTheme();
|
|
310
283
|
const container = new Container();
|
|
@@ -353,20 +326,43 @@ function renderMultiFlowCollapsed(
|
|
|
353
326
|
results: SingleResult[],
|
|
354
327
|
successCount: number,
|
|
355
328
|
icon: string,
|
|
356
|
-
theme:
|
|
329
|
+
theme: FlowTheme,
|
|
357
330
|
): Text {
|
|
358
331
|
let text = `${icon} ${theme.fg("toolTitle", theme.bold("flow "))}${theme.fg("accent", `${successCount}/${results.length} flows`)}`;
|
|
359
332
|
|
|
360
|
-
for (
|
|
361
|
-
const
|
|
333
|
+
for (let i = 0; i < results.length; i++) {
|
|
334
|
+
const r = results[i];
|
|
335
|
+
const isLast = i === results.length - 1;
|
|
362
336
|
const flowOutput = getFlowOutput(r.messages);
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
337
|
+
const stats = formatCompactStats(r.usage, r.model);
|
|
338
|
+
const error = isFlowError(r);
|
|
339
|
+
|
|
340
|
+
// Header line
|
|
341
|
+
const headerPrefix = isLast ? "└─" : "├─";
|
|
342
|
+
let line = `\n${theme.fg("dim", headerPrefix)} ${theme.bg("selectedBg", theme.fg("accent", theme.bold(r.type)))}`;
|
|
343
|
+
if (stats) {
|
|
344
|
+
line += ` ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
345
|
+
}
|
|
346
|
+
if (error && r.stopReason) {
|
|
347
|
+
line += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
348
|
+
}
|
|
349
|
+
text += line;
|
|
350
|
+
|
|
351
|
+
// Continuation indent for sub-lines
|
|
352
|
+
const indent = isLast ? " " : "│ ";
|
|
353
|
+
|
|
354
|
+
// Intent line
|
|
355
|
+
const hasOutput = !!(flowOutput || (error && r.errorMessage));
|
|
356
|
+
if (r.intent) {
|
|
357
|
+
const intentPrefix = hasOutput ? "├─" : "└─";
|
|
358
|
+
text += `\n${theme.fg("dim", indent + intentPrefix + " int:")} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Output line
|
|
366
362
|
if (flowOutput) {
|
|
367
|
-
text += `\n${renderFlowReport(
|
|
368
|
-
} else {
|
|
369
|
-
text += `\n${theme.fg("
|
|
363
|
+
text += `\n${theme.fg("dim", indent + "└─ msg:")} ${renderFlowReport(truncateChars(flowOutput, 25), theme)}`;
|
|
364
|
+
} else if (error && r.errorMessage) {
|
|
365
|
+
text += `\n${theme.fg("dim", indent + "└─ msg:")} ${theme.fg("error", truncateChars(r.errorMessage, 25))}`;
|
|
370
366
|
}
|
|
371
367
|
}
|
|
372
368
|
|
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
|
}
|