pi-agent-flow 1.0.0 → 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/config.ts +67 -0
- package/flow.ts +7 -3
- package/package.json +2 -1
- package/render-utils.ts +8 -2
- package/render.ts +6 -4
- package/runner-events.js +112 -7
- package/types.ts +5 -1
package/config.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load flow tier model configuration from Pi settings files.
|
|
3
|
+
*
|
|
4
|
+
* Reads global (~/.pi/agent/settings.json) and project (.pi/settings.json)
|
|
5
|
+
* settings, with project overriding global for flowModels.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
export interface FlowModelConfig {
|
|
13
|
+
lite?: string;
|
|
14
|
+
flash?: string;
|
|
15
|
+
full?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readSettingsJson(filePath: string): Record<string, unknown> | null {
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
21
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractFlowModels(settings: Record<string, unknown> | null): FlowModelConfig {
|
|
28
|
+
if (!settings) return {};
|
|
29
|
+
const flowModels = settings.flowModels;
|
|
30
|
+
if (!flowModels || typeof flowModels !== "object" || Array.isArray(flowModels)) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const obj = flowModels as Record<string, unknown>;
|
|
34
|
+
const result: FlowModelConfig = {};
|
|
35
|
+
for (const key of ["lite", "flash", "full"] as const) {
|
|
36
|
+
if (typeof obj[key] === "string") {
|
|
37
|
+
result[key] = obj[key] as string;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getGlobalSettingsPath(): string {
|
|
44
|
+
const agentDir = process.env["PI_CODING_AGENT_DIR"]?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
45
|
+
return path.join(agentDir, "settings.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getProjectSettingsPath(cwd: string): string {
|
|
49
|
+
return path.join(cwd, ".pi", "settings.json");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load flowModels from global and project settings.json.
|
|
54
|
+
* Project overrides global (shallow merge per key).
|
|
55
|
+
*/
|
|
56
|
+
export function loadFlowModels(cwd: string): FlowModelConfig {
|
|
57
|
+
const globalSettings = readSettingsJson(getGlobalSettingsPath());
|
|
58
|
+
const globalModels = extractFlowModels(globalSettings);
|
|
59
|
+
|
|
60
|
+
const projectSettings = readSettingsJson(getProjectSettingsPath(cwd));
|
|
61
|
+
const projectModels = extractFlowModels(projectSettings);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...globalModels,
|
|
65
|
+
...projectModels,
|
|
66
|
+
};
|
|
67
|
+
}
|
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
|
-
|
|
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,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-flow",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.ts",
|
|
9
9
|
"agents.ts",
|
|
10
|
+
"config.ts",
|
|
10
11
|
"flow.ts",
|
|
11
12
|
"hooks.ts",
|
|
12
13
|
"runner-cli.js",
|
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(`
|
|
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
|
|
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", "├─
|
|
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 + "├─
|
|
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
|
-
|
|
296
|
-
|
|
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
|
|
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
|
}
|