pi-taskflow 0.0.5 → 0.0.6

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.
@@ -11,20 +11,7 @@ import * as path from "node:path";
11
11
  import type { Message } from "@earendil-works/pi-ai";
12
12
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
13
13
  import type { AgentConfig } from "./agents.ts";
14
-
15
- export interface UsageStats {
16
- input: number;
17
- output: number;
18
- cacheRead: number;
19
- cacheWrite: number;
20
- cost: number;
21
- contextTokens: number;
22
- turns: number;
23
- }
24
-
25
- export function emptyUsage(): UsageStats {
26
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
27
- }
14
+ import { emptyUsage, type UsageStats } from "./usage.ts";
28
15
 
29
16
  export interface RunResult {
30
17
  agent: string;
@@ -36,6 +23,8 @@ export interface RunResult {
36
23
  model?: string;
37
24
  stopReason?: string;
38
25
  errorMessage?: string;
26
+ /** Total subagent attempts incl. retries (set by the runtime's retry wrapper). */
27
+ attempts?: number;
39
28
  }
40
29
 
41
30
  export interface LiveUpdate {
@@ -71,6 +60,56 @@ function getFinalOutput(messages: Message[]): string {
71
60
  return "";
72
61
  }
73
62
 
63
+ /** Accumulated state folded from a subagent's NDJSON event stream. */
64
+ export interface EventAccumulator {
65
+ messages: Message[];
66
+ usage: UsageStats;
67
+ model?: string;
68
+ stopReason?: string;
69
+ errorMessage?: string;
70
+ lastActivity: string;
71
+ }
72
+
73
+ export function newAccumulator(model?: string): EventAccumulator {
74
+ return { messages: [], usage: emptyUsage(), model, lastActivity: "" };
75
+ }
76
+
77
+ /**
78
+ * Fold one NDJSON line into the accumulator. Returns a LiveUpdate when an
79
+ * assistant message ended (for streaming), else null. Empty, malformed, and
80
+ * non-`message_end` lines are ignored — making the parser robust to partial
81
+ * buffers/noise and unit-testable without spawning a process.
82
+ */
83
+ export function foldEventLine(acc: EventAccumulator, line: string): LiveUpdate | null {
84
+ if (!line.trim()) return null;
85
+ let event: any;
86
+ try {
87
+ event = JSON.parse(line);
88
+ } catch {
89
+ return null;
90
+ }
91
+ if (event.type !== "message_end" || !event.message) return null;
92
+ const msg = event.message as Message;
93
+ acc.messages.push(msg);
94
+ if (msg.role !== "assistant") return null;
95
+ acc.usage.turns++;
96
+ const u = (msg as any).usage;
97
+ if (u) {
98
+ acc.usage.input += u.input || 0;
99
+ acc.usage.output += u.output || 0;
100
+ acc.usage.cacheRead += u.cacheRead || 0;
101
+ acc.usage.cacheWrite += u.cacheWrite || 0;
102
+ acc.usage.cost += u.cost?.total || 0;
103
+ acc.usage.contextTokens = u.totalTokens || 0;
104
+ }
105
+ if (!acc.model && (msg as any).model) acc.model = (msg as any).model;
106
+ if ((msg as any).stopReason) acc.stopReason = (msg as any).stopReason;
107
+ if ((msg as any).errorMessage) acc.errorMessage = (msg as any).errorMessage;
108
+ const activity = describeActivity(msg);
109
+ if (activity) acc.lastActivity = activity;
110
+ return { text: acc.lastActivity, usage: { ...acc.usage }, model: acc.model };
111
+ }
112
+
74
113
  /** One-line description of the most recent assistant activity (text or tool call). */
75
114
  function describeActivity(msg: Message): string {
76
115
  if (msg.role !== "assistant") return "";
@@ -177,8 +216,7 @@ export async function runAgentTask(
177
216
  let tmpPromptDir: string | null = null;
178
217
  let tmpPromptPath: string | null = null;
179
218
 
180
- const messages: Message[] = [];
181
- let lastActivity = "";
219
+ const acc = newAccumulator(model);
182
220
  const result: RunResult = {
183
221
  agent: agentName,
184
222
  task,
@@ -209,36 +247,8 @@ export async function runAgentTask(
209
247
  let buffer = "";
210
248
 
211
249
  const processLine = (line: string) => {
212
- if (!line.trim()) return;
213
- let event: any;
214
- try {
215
- event = JSON.parse(line);
216
- } catch {
217
- return;
218
- }
219
- if (event.type === "message_end" && event.message) {
220
- const msg = event.message as Message;
221
- messages.push(msg);
222
- if (msg.role === "assistant") {
223
- result.usage.turns++;
224
- const u = (msg as any).usage;
225
- if (u) {
226
- result.usage.input += u.input || 0;
227
- result.usage.output += u.output || 0;
228
- result.usage.cacheRead += u.cacheRead || 0;
229
- result.usage.cacheWrite += u.cacheWrite || 0;
230
- result.usage.cost += u.cost?.total || 0;
231
- result.usage.contextTokens = u.totalTokens || 0;
232
- }
233
- if (!result.model && (msg as any).model) result.model = (msg as any).model;
234
- if ((msg as any).stopReason) result.stopReason = (msg as any).stopReason;
235
- if ((msg as any).errorMessage) result.errorMessage = (msg as any).errorMessage;
236
- const activity = describeActivity(msg);
237
- if (activity) lastActivity = activity;
238
- if (opts.onLive)
239
- opts.onLive({ text: lastActivity, usage: { ...result.usage }, model: result.model });
240
- }
241
- }
250
+ const live = foldEventLine(acc, line);
251
+ if (live && opts.onLive) opts.onLive(live);
242
252
  };
243
253
 
244
254
  proc.stdout.on("data", (data) => {
@@ -270,7 +280,11 @@ export async function runAgentTask(
270
280
  });
271
281
 
272
282
  result.exitCode = exitCode;
273
- result.output = getFinalOutput(messages);
283
+ result.usage = acc.usage;
284
+ result.model = acc.model;
285
+ result.stopReason = acc.stopReason;
286
+ result.errorMessage = acc.errorMessage;
287
+ result.output = getFinalOutput(acc.messages);
274
288
  if (wasAborted) {
275
289
  result.stopReason = "aborted";
276
290
  result.errorMessage = "Subagent was aborted";
@@ -317,34 +331,3 @@ export async function mapWithConcurrencyLimit<TIn, TOut>(
317
331
  await Promise.all(workers);
318
332
  return results;
319
333
  }
320
-
321
- export function formatTokens(count: number): string {
322
- if (count < 1000) return count.toString();
323
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
324
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
325
- return `${(count / 1000000).toFixed(1)}M`;
326
- }
327
-
328
- export function formatUsage(usage: UsageStats, model?: string): string {
329
- const parts: string[] = [];
330
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
331
- if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
332
- if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
333
- if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
334
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
335
- if (model) parts.push(model);
336
- return parts.join(" ");
337
- }
338
-
339
- export function aggregateUsage(usages: UsageStats[]): UsageStats {
340
- const total = emptyUsage();
341
- for (const u of usages) {
342
- total.input += u.input;
343
- total.output += u.output;
344
- total.cacheRead += u.cacheRead;
345
- total.cacheWrite += u.cacheWrite;
346
- total.cost += u.cost;
347
- total.turns += u.turns;
348
- }
349
- return total;
350
- }