pi-taskflow 0.0.4 → 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 +273 -54
- 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 +369 -46
- 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 +79 -0
- package/skills/taskflow/configuration.md +275 -0
|
@@ -149,3 +149,234 @@ export function coerceArray(value: unknown): unknown[] | null {
|
|
|
149
149
|
}
|
|
150
150
|
return null;
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Conditional expressions (phase.when)
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
//
|
|
157
|
+
// A tiny, safe boolean expression language — NO eval / Function. Operands are
|
|
158
|
+
// either interpolation placeholders `{...}` (resolved to their raw value) or
|
|
159
|
+
// literals (quoted string, number, true/false/null, or a bare word treated as
|
|
160
|
+
// a string). Operators, by precedence (low → high):
|
|
161
|
+
//
|
|
162
|
+
// || logical or
|
|
163
|
+
// && logical and
|
|
164
|
+
// == != == >= <= > < comparison
|
|
165
|
+
// ! logical not / unary
|
|
166
|
+
// ( ) grouping
|
|
167
|
+
//
|
|
168
|
+
// A bare operand is evaluated for truthiness. Parse errors fail OPEN (return
|
|
169
|
+
// true) so a malformed guard never silently drops a phase.
|
|
170
|
+
|
|
171
|
+
type Tok =
|
|
172
|
+
| { t: "ref"; v: string }
|
|
173
|
+
| { t: "str"; v: string }
|
|
174
|
+
| { t: "num"; v: number }
|
|
175
|
+
| { t: "bool"; v: boolean }
|
|
176
|
+
| { t: "null" }
|
|
177
|
+
| { t: "op"; v: string };
|
|
178
|
+
|
|
179
|
+
const OPS = ["&&", "||", "==", "!=", ">=", "<=", ">", "<", "!", "(", ")"];
|
|
180
|
+
|
|
181
|
+
function tokenize(input: string): Tok[] {
|
|
182
|
+
const toks: Tok[] = [];
|
|
183
|
+
let i = 0;
|
|
184
|
+
const n = input.length;
|
|
185
|
+
while (i < n) {
|
|
186
|
+
const c = input[i];
|
|
187
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
188
|
+
i++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// placeholder {path.to.value}
|
|
192
|
+
if (c === "{") {
|
|
193
|
+
const end = input.indexOf("}", i);
|
|
194
|
+
if (end === -1) throw new Error("unterminated placeholder");
|
|
195
|
+
toks.push({ t: "ref", v: input.slice(i + 1, end).trim() });
|
|
196
|
+
i = end + 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// quoted string
|
|
200
|
+
if (c === '"' || c === "'") {
|
|
201
|
+
const end = input.indexOf(c, i + 1);
|
|
202
|
+
if (end === -1) throw new Error("unterminated string");
|
|
203
|
+
toks.push({ t: "str", v: input.slice(i + 1, end) });
|
|
204
|
+
i = end + 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// multi/single char operators
|
|
208
|
+
const op = OPS.find((o) => input.startsWith(o, i));
|
|
209
|
+
if (op) {
|
|
210
|
+
toks.push({ t: "op", v: op });
|
|
211
|
+
i += op.length;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// number
|
|
215
|
+
const numMatch = /^-?\d+(?:\.\d+)?/.exec(input.slice(i));
|
|
216
|
+
if (numMatch) {
|
|
217
|
+
toks.push({ t: "num", v: Number(numMatch[0]) });
|
|
218
|
+
i += numMatch[0].length;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// bareword → literal (true/false/null keywords, else string)
|
|
222
|
+
const word = /^[^\s&|!=<>()"'{}]+/.exec(input.slice(i));
|
|
223
|
+
if (word) {
|
|
224
|
+
const w = word[0];
|
|
225
|
+
if (w === "true") toks.push({ t: "bool", v: true });
|
|
226
|
+
else if (w === "false") toks.push({ t: "bool", v: false });
|
|
227
|
+
else if (w === "null") toks.push({ t: "null" });
|
|
228
|
+
else toks.push({ t: "str", v: w });
|
|
229
|
+
i += w.length;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
throw new Error(`unexpected char '${c}'`);
|
|
233
|
+
}
|
|
234
|
+
return toks;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isNumeric(v: unknown): boolean {
|
|
238
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
239
|
+
if (typeof v === "string" && v.trim() !== "") return Number.isFinite(Number(v));
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function truthy(v: unknown): boolean {
|
|
244
|
+
if (v === undefined || v === null) return false;
|
|
245
|
+
if (typeof v === "boolean") return v;
|
|
246
|
+
if (typeof v === "number") return v !== 0;
|
|
247
|
+
if (typeof v === "string") {
|
|
248
|
+
const s = v.trim().toLowerCase();
|
|
249
|
+
return !(s === "" || s === "false" || s === "0" || s === "no" || s === "off" || s === "null");
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
252
|
+
if (typeof v === "object") return Object.keys(v as object).length > 0;
|
|
253
|
+
return Boolean(v);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function compare(a: unknown, op: string, b: unknown): boolean {
|
|
257
|
+
if (isNumeric(a) && isNumeric(b)) {
|
|
258
|
+
const x = Number(a);
|
|
259
|
+
const y = Number(b);
|
|
260
|
+
switch (op) {
|
|
261
|
+
case "==": return x === y;
|
|
262
|
+
case "!=": return x !== y;
|
|
263
|
+
case ">": return x > y;
|
|
264
|
+
case "<": return x < y;
|
|
265
|
+
case ">=": return x >= y;
|
|
266
|
+
case "<=": return x <= y;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const sa = a === undefined || a === null ? "" : String(a);
|
|
270
|
+
const sb = b === undefined || b === null ? "" : String(b);
|
|
271
|
+
switch (op) {
|
|
272
|
+
case "==": return sa === sb;
|
|
273
|
+
case "!=": return sa !== sb;
|
|
274
|
+
case ">": return sa > sb;
|
|
275
|
+
case "<": return sa < sb;
|
|
276
|
+
case ">=": return sa >= sb;
|
|
277
|
+
case "<=": return sa <= sb;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Recursive-descent parser/evaluator over the token stream. */
|
|
283
|
+
class CondParser {
|
|
284
|
+
private pos = 0;
|
|
285
|
+
private readonly toks: Tok[];
|
|
286
|
+
private readonly ctx: InterpolationContext;
|
|
287
|
+
constructor(toks: Tok[], ctx: InterpolationContext) {
|
|
288
|
+
this.toks = toks;
|
|
289
|
+
this.ctx = ctx;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
parse(): unknown {
|
|
293
|
+
const v = this.parseOr();
|
|
294
|
+
if (this.pos < this.toks.length) throw new Error("trailing tokens");
|
|
295
|
+
return v;
|
|
296
|
+
}
|
|
297
|
+
private peek(): Tok | undefined {
|
|
298
|
+
return this.toks[this.pos];
|
|
299
|
+
}
|
|
300
|
+
private eat(op: string): boolean {
|
|
301
|
+
const t = this.peek();
|
|
302
|
+
if (t && t.t === "op" && t.v === op) {
|
|
303
|
+
this.pos++;
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
private parseOr(): unknown {
|
|
309
|
+
let left = this.parseAnd();
|
|
310
|
+
while (this.eat("||")) {
|
|
311
|
+
const right = this.parseAnd();
|
|
312
|
+
left = truthy(left) || truthy(right);
|
|
313
|
+
}
|
|
314
|
+
return left;
|
|
315
|
+
}
|
|
316
|
+
private parseAnd(): unknown {
|
|
317
|
+
let left = this.parseNot();
|
|
318
|
+
while (this.eat("&&")) {
|
|
319
|
+
const right = this.parseNot();
|
|
320
|
+
left = truthy(left) && truthy(right);
|
|
321
|
+
}
|
|
322
|
+
return left;
|
|
323
|
+
}
|
|
324
|
+
private parseNot(): unknown {
|
|
325
|
+
if (this.eat("!")) return !truthy(this.parseNot());
|
|
326
|
+
return this.parseComparison();
|
|
327
|
+
}
|
|
328
|
+
private parseComparison(): unknown {
|
|
329
|
+
const left = this.parsePrimary();
|
|
330
|
+
const t = this.peek();
|
|
331
|
+
if (t && t.t === "op" && ["==", "!=", ">", "<", ">=", "<="].includes(t.v)) {
|
|
332
|
+
this.pos++;
|
|
333
|
+
const right = this.parsePrimary();
|
|
334
|
+
return compare(left, t.v, right);
|
|
335
|
+
}
|
|
336
|
+
return left;
|
|
337
|
+
}
|
|
338
|
+
private parsePrimary(): unknown {
|
|
339
|
+
if (this.eat("(")) {
|
|
340
|
+
const v = this.parseOr();
|
|
341
|
+
if (!this.eat(")")) throw new Error("missing )");
|
|
342
|
+
return v;
|
|
343
|
+
}
|
|
344
|
+
const t = this.peek();
|
|
345
|
+
if (!t) throw new Error("unexpected end");
|
|
346
|
+
this.pos++;
|
|
347
|
+
switch (t.t) {
|
|
348
|
+
case "ref": return resolvePath(t.v, this.ctx);
|
|
349
|
+
case "str": return t.v;
|
|
350
|
+
case "num": return t.v;
|
|
351
|
+
case "bool": return t.v;
|
|
352
|
+
case "null": return null;
|
|
353
|
+
default: throw new Error(`unexpected operator '${(t as { v: string }).v}'`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Evaluate a `when` expression to a boolean. Returns `{ value, error }`.
|
|
360
|
+
* Parse errors set `error` and fail OPEN (`value: true`) so a broken guard
|
|
361
|
+
* never silently drops a phase.
|
|
362
|
+
*/
|
|
363
|
+
export function tryEvaluateCondition(
|
|
364
|
+
expr: string,
|
|
365
|
+
ctx: InterpolationContext,
|
|
366
|
+
): { value: boolean; error?: string } {
|
|
367
|
+
const trimmed = (expr ?? "").trim();
|
|
368
|
+
if (!trimmed) return { value: true };
|
|
369
|
+
try {
|
|
370
|
+
const toks = tokenize(trimmed);
|
|
371
|
+
if (toks.length === 0) return { value: true };
|
|
372
|
+
const result = new CondParser(toks, ctx).parse();
|
|
373
|
+
return { value: truthy(result) };
|
|
374
|
+
} catch (e) {
|
|
375
|
+
return { value: true, error: e instanceof Error ? e.message : String(e) };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Boolean convenience wrapper over {@link tryEvaluateCondition}. */
|
|
380
|
+
export function evaluateCondition(expr: string, ctx: InterpolationContext): boolean {
|
|
381
|
+
return tryEvaluateCondition(expr, ctx).value;
|
|
382
|
+
}
|
package/extensions/render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
10
|
-
import { formatTokens, type UsageStats } from "./
|
|
10
|
+
import { formatTokens, type UsageStats } from "./usage.ts";
|
|
11
11
|
import type { PhaseState, RunState } from "./store.ts";
|
|
12
12
|
import { dependenciesOf, type Phase, topoLayers } from "./schema.ts";
|
|
13
13
|
|
|
@@ -121,7 +121,7 @@ function phaseDetail(phase: Phase, ps: PhaseState | undefined, theme: Theme): st
|
|
|
121
121
|
return theme.fg("muted", `skipped · ${snip}`);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
const isFanout = type === "map" || type === "parallel";
|
|
124
|
+
const isFanout = type === "map" || type === "parallel" || type === "flow";
|
|
125
125
|
|
|
126
126
|
if (ps.status === "failed") {
|
|
127
127
|
const e = (ps.error ?? "failed").replace(/\s+/g, " ");
|
|
@@ -171,6 +171,14 @@ function phaseDetail(phase: Phase, ps: PhaseState | undefined, theme: Theme): st
|
|
|
171
171
|
// single-agent done
|
|
172
172
|
const model = shortModel(ps.model);
|
|
173
173
|
const u = compactUsage(ps.usage, theme);
|
|
174
|
+
if (ps.approval) {
|
|
175
|
+
const d = ps.approval.decision;
|
|
176
|
+
const color = d === "reject" ? "error" : d === "edit" ? "warning" : "success";
|
|
177
|
+
let a = theme.fg(color as Parameters<typeof theme.fg>[0], theme.bold(d.toUpperCase()));
|
|
178
|
+
if (ps.approval.auto) a += theme.fg("dim", " auto");
|
|
179
|
+
if (time) a += ` ${time}`;
|
|
180
|
+
return a;
|
|
181
|
+
}
|
|
174
182
|
if (ps.gate) {
|
|
175
183
|
const badge =
|
|
176
184
|
ps.gate.verdict === "block" ? theme.fg("error", theme.bold("BLOCK")) : theme.fg("success", "PASS");
|
|
@@ -186,6 +194,7 @@ function phaseDetail(phase: Phase, ps: PhaseState | undefined, theme: Theme): st
|
|
|
186
194
|
let s = "";
|
|
187
195
|
if (model) s += theme.fg("accent", model);
|
|
188
196
|
if (u) s += (s ? " " : "") + u;
|
|
197
|
+
if (ps.attempts && ps.attempts > 1) s += theme.fg("warning", ` ↻${ps.attempts - 1}`);
|
|
189
198
|
if (time) s += ` ${time}`;
|
|
190
199
|
return s || theme.fg("dim", "done");
|
|
191
200
|
}
|
|
@@ -217,7 +226,9 @@ function headerLine(state: RunState, theme: Theme): string {
|
|
|
217
226
|
if (failed) line += theme.fg("error", ` · ${failed}✗`);
|
|
218
227
|
if (state.status === "blocked") line += theme.fg("error", " · blocked");
|
|
219
228
|
const cost = aggregateCost(state);
|
|
220
|
-
|
|
229
|
+
const budget = state.def.budget;
|
|
230
|
+
if (budget?.maxUSD !== undefined) line += theme.fg("muted", ` · $${cost.toFixed(3)}/$${budget.maxUSD}`);
|
|
231
|
+
else if (cost) line += theme.fg("muted", ` · $${cost.toFixed(3)}`);
|
|
221
232
|
const el = runElapsed(state);
|
|
222
233
|
if (el) line += theme.fg("dim", ` · ${elapsed(el)}`);
|
|
223
234
|
return line;
|
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
|
-
}
|