pi-agent-flow 0.1.0-alpha.1 → 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 -16
- 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
|
@@ -352,16 +352,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
352
352
|
|
|
353
353
|
const discovery = discoverFlows(ctx.cwd, "all");
|
|
354
354
|
discoveredFlows = discovery.flows;
|
|
355
|
-
|
|
356
|
-
if (discoveredFlows.length > 0 && ctx.hasUI) {
|
|
357
|
-
const list = discoveredFlows
|
|
358
|
-
.map((f) => ` - ${f.name} (${f.source})`)
|
|
359
|
-
.join("\n");
|
|
360
|
-
ctx.ui.notify(
|
|
361
|
-
`Found ${discoveredFlows.length} flow(s):\n${list}`,
|
|
362
|
-
"info",
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
355
|
});
|
|
366
356
|
|
|
367
357
|
// Inject available flows into the system prompt
|
|
@@ -389,9 +379,8 @@ Multiple independent flows? Batch them into one call:
|
|
|
389
379
|
|
|
390
380
|
Each call renders as:
|
|
391
381
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
• flow [review] — Audit security and quality...
|
|
382
|
+
• flow [explore] — Map the full directory structure...
|
|
383
|
+
• flow [review] — Audit security and quality...
|
|
395
384
|
|
|
396
385
|
Each flow returns:
|
|
397
386
|
|
|
@@ -499,10 +488,12 @@ flow [type] accomplished
|
|
|
499
488
|
};
|
|
500
489
|
}
|
|
501
490
|
|
|
502
|
-
|
|
491
|
+
let lastStreamingText = "";
|
|
492
|
+
const emitProgress = (streamingText?: string) => {
|
|
503
493
|
if (!onUpdate) return;
|
|
494
|
+
if (streamingText) lastStreamingText = streamingText;
|
|
504
495
|
onUpdate({
|
|
505
|
-
content: [{ type: "text", text:
|
|
496
|
+
content: [{ type: "text", text: lastStreamingText || "" }],
|
|
506
497
|
details: makeDetails([...allResults]),
|
|
507
498
|
});
|
|
508
499
|
};
|
|
@@ -537,7 +528,7 @@ flow [type] accomplished
|
|
|
537
528
|
onUpdate: (partial) => {
|
|
538
529
|
if (partial.details?.results[0]) {
|
|
539
530
|
allResults[index] = partial.details.results[0];
|
|
540
|
-
emitProgress();
|
|
531
|
+
emitProgress(partial.content?.[0]?.text);
|
|
541
532
|
}
|
|
542
533
|
},
|
|
543
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
|
}
|