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.
- package/DESIGN.md +15 -1
- package/README.md +45 -10
- package/examples/conditional-research.json +56 -0
- package/examples/guarded-refactor.json +50 -0
- package/extensions/agents.ts +8 -1
- package/extensions/index.ts +30 -15
- package/extensions/interpolate.ts +231 -0
- package/extensions/render.ts +14 -3
- package/extensions/runner.ts +61 -78
- package/extensions/runtime.ts +364 -44
- package/extensions/schema.ts +85 -2
- package/extensions/store.ts +29 -3
- package/extensions/usage.ts +42 -0
- package/package.json +2 -2
- package/skills/taskflow/SKILL.md +67 -2
package/extensions/runner.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
213
|
-
|
|
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.
|
|
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
|
-
}
|