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 +3 -2
- package/index.ts +7 -6
- package/package.json +4 -3
- package/render.ts +30 -62
- 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.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": "
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
268
|
-
let text = `${
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
365
|
-
if (usageStr) text += `
|
|
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
|
}
|