@fosterg4/pi-subagent 1.0.2 → 1.0.4
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/index.ts +68 -11
- package/package.json +1 -1
- package/ui.ts +176 -0
- package/utils.ts +37 -0
package/index.ts
CHANGED
|
@@ -35,12 +35,16 @@ import {
|
|
|
35
35
|
formatAgentList,
|
|
36
36
|
} from "./agents.ts";
|
|
37
37
|
import { type ValidationResult, validateSchema } from "./validate.ts";
|
|
38
|
+
import { fmt, usageLine, sumUsage } from "./utils.ts";
|
|
39
|
+
import { AgentWidget, type WidgetEntry } from "./ui.ts";
|
|
38
40
|
|
|
39
41
|
const MAX_PARALLEL_TASKS = 8;
|
|
40
42
|
const MAX_CONCURRENCY = 4;
|
|
41
43
|
const PER_TASK_OUTPUT_CAP = 50 * 1024;
|
|
42
44
|
|
|
45
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
43
46
|
|
|
47
|
+
interface UsageStats {
|
|
44
48
|
input: number;
|
|
45
49
|
output: number;
|
|
46
50
|
cacheRead: number;
|
|
@@ -233,6 +237,7 @@ async function runSingleAgent(
|
|
|
233
237
|
signal: AbortSignal | undefined,
|
|
234
238
|
onUpdate: OnUpdateCallback | undefined,
|
|
235
239
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
240
|
+
onStats?: (stats: { turns: number; tokens: number }) => void,
|
|
236
241
|
): Promise<SingleResult> {
|
|
237
242
|
const agent = agents.find((a) => a.name === agentName);
|
|
238
243
|
|
|
@@ -370,6 +375,8 @@ async function runSingleAgent(
|
|
|
370
375
|
currentResult.usage.cost += usage.cost?.total || 0;
|
|
371
376
|
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
372
377
|
}
|
|
378
|
+
const total = currentResult.usage.input + currentResult.usage.output + currentResult.usage.cacheRead;
|
|
379
|
+
onStats?.({ turns: currentResult.usage.turns, tokens: total });
|
|
373
380
|
if (!currentResult.model && msg.model)
|
|
374
381
|
currentResult.model = msg.model;
|
|
375
382
|
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
@@ -637,8 +644,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
637
644
|
if (params.chain && params.chain.length > 0) {
|
|
638
645
|
const results: SingleResult[] = [];
|
|
639
646
|
let previousStructured: Record<string, unknown> | undefined;
|
|
647
|
+
let widgetHandle: ReturnType<typeof pi.ui.custom> | undefined;
|
|
648
|
+
|
|
649
|
+
const closeWidget = () => {
|
|
650
|
+
if (widgetHandle) {
|
|
651
|
+
widgetHandle.close();
|
|
652
|
+
widgetHandle = undefined;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
640
655
|
|
|
641
656
|
for (let i = 0; i < params.chain.length; i++) {
|
|
657
|
+
closeWidget();
|
|
658
|
+
|
|
642
659
|
const step = params.chain[i];
|
|
643
660
|
let taskWithContext = step.task;
|
|
644
661
|
|
|
@@ -648,13 +665,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
648
665
|
/\{previous\}/g,
|
|
649
666
|
JSON.stringify(previousStructured, null, 2),
|
|
650
667
|
);
|
|
651
|
-
} else {
|
|
668
|
+
} else if (i > 0) {
|
|
652
669
|
taskWithContext = taskWithContext.replace(
|
|
653
670
|
/\{previous\}/g,
|
|
654
671
|
getFinalOutput(results[i - 1]?.messages ?? ""),
|
|
655
672
|
);
|
|
656
673
|
}
|
|
657
674
|
|
|
675
|
+
// Spawn live widget for this step
|
|
676
|
+
if (ctx.hasUI) {
|
|
677
|
+
const widget = new AgentWidget();
|
|
678
|
+
widget.addAgent(step.agent, step.task.replace(/\{[^}]+\}/g, "").trim());
|
|
679
|
+
widgetHandle = ctx.ui.custom(widget, { overlay: true });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const stepStats = (stats: { turns: number; tokens: number }) => {
|
|
683
|
+
if (widgetHandle) {
|
|
684
|
+
widgetHandle.requestRender();
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
658
688
|
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
659
689
|
? (partial) => {
|
|
660
690
|
const currentResult = partial.details?.results[0];
|
|
@@ -678,6 +708,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
678
708
|
signal,
|
|
679
709
|
chainUpdate,
|
|
680
710
|
makeDetails("chain"),
|
|
711
|
+
stepStats,
|
|
681
712
|
);
|
|
682
713
|
results.push(result);
|
|
683
714
|
|
|
@@ -694,6 +725,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
694
725
|
}
|
|
695
726
|
|
|
696
727
|
if (isFailedResult(result)) {
|
|
728
|
+
closeWidget();
|
|
697
729
|
const errorMsg = getResultOutput(result);
|
|
698
730
|
return {
|
|
699
731
|
content: [
|
|
@@ -708,6 +740,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
708
740
|
}
|
|
709
741
|
}
|
|
710
742
|
|
|
743
|
+
closeWidget();
|
|
744
|
+
|
|
711
745
|
const lastResult = results[results.length - 1];
|
|
712
746
|
return {
|
|
713
747
|
content: [
|
|
@@ -954,11 +988,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
954
988
|
return theme.fg("error", "\u2717");
|
|
955
989
|
};
|
|
956
990
|
|
|
957
|
-
|
|
991
|
+
// --- Single mode ---
|
|
958
992
|
if (details.mode === "single" && details.results.length === 1) {
|
|
959
993
|
const r = details.results[0];
|
|
960
994
|
const status = getStatusText(r);
|
|
961
995
|
const finalOutput = getFinalOutputText(r.messages);
|
|
996
|
+
const usage = usageLine(r.usage);
|
|
997
|
+
const usg = usage ? theme.fg("dim", usage) : "";
|
|
962
998
|
|
|
963
999
|
if (expanded) {
|
|
964
1000
|
const container = new Container();
|
|
@@ -969,6 +1005,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
969
1005
|
container.addChild(new Spacer(1));
|
|
970
1006
|
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
971
1007
|
}
|
|
1008
|
+
if (usg) {
|
|
1009
|
+
container.addChild(new Spacer(1));
|
|
1010
|
+
container.addChild(new Text(usg, 0, 0));
|
|
1011
|
+
}
|
|
972
1012
|
return container;
|
|
973
1013
|
}
|
|
974
1014
|
|
|
@@ -981,6 +1021,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
981
1021
|
} else {
|
|
982
1022
|
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
983
1023
|
}
|
|
1024
|
+
if (usg) text += `\n${usg}`;
|
|
984
1025
|
return new Text(text, 0, 0);
|
|
985
1026
|
}
|
|
986
1027
|
|
|
@@ -991,6 +1032,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
991
1032
|
const allOk = details.results.every((r) => r.exitCode === 0);
|
|
992
1033
|
const icon = allOk ? theme.fg("success", "\u2713") : theme.fg("error", "\u2717");
|
|
993
1034
|
const steps = details.results.map((r) => r.agent).join(" \u2192 ");
|
|
1035
|
+
const total = sumUsage(details.results);
|
|
1036
|
+
const totalUsg = usageLine(total);
|
|
994
1037
|
|
|
995
1038
|
if (expanded) {
|
|
996
1039
|
const container = new Container();
|
|
@@ -999,18 +1042,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
999
1042
|
);
|
|
1000
1043
|
for (const r of details.results) {
|
|
1001
1044
|
const out = getFinalOutputText(r.messages);
|
|
1045
|
+
const stepUsage = usageLine(r.usage);
|
|
1046
|
+
const stepUsg = stepUsage ? theme.fg("dim", stepUsage) : "";
|
|
1047
|
+
const label = `${getStatusText(r)} ${theme.fg("accent", r.agent)}${r.model ? theme.fg("muted", ` \u00B7 ${r.model}`) : ""}${stepUsg ? " " + stepUsg : ""}`;
|
|
1002
1048
|
if (out) {
|
|
1003
1049
|
container.addChild(new Spacer(1));
|
|
1004
|
-
container.addChild(
|
|
1005
|
-
new Text(
|
|
1006
|
-
`${getStatusText(r)} ${theme.fg("accent", r.agent)}${r.model ? theme.fg("muted", ` \u00B7 ${r.model}`) : ""}`,
|
|
1007
|
-
0,
|
|
1008
|
-
0,
|
|
1009
|
-
),
|
|
1010
|
-
);
|
|
1050
|
+
container.addChild(new Text(label, 0, 0));
|
|
1011
1051
|
container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
|
|
1012
1052
|
}
|
|
1013
1053
|
}
|
|
1054
|
+
if (totalUsg && details.results.length > 1) {
|
|
1055
|
+
container.addChild(new Spacer(1));
|
|
1056
|
+
container.addChild(new Text(totalUsg, 0, 0));
|
|
1057
|
+
}
|
|
1014
1058
|
return container;
|
|
1015
1059
|
}
|
|
1016
1060
|
|
|
@@ -1023,6 +1067,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1023
1067
|
} else {
|
|
1024
1068
|
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1025
1069
|
}
|
|
1070
|
+
if (totalUsg) text += `\n${totalUsg}`;
|
|
1026
1071
|
return new Text(text, 0, 0);
|
|
1027
1072
|
}
|
|
1028
1073
|
|
|
@@ -1039,6 +1084,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1039
1084
|
|
|
1040
1085
|
if (expanded && running === 0) {
|
|
1041
1086
|
const container = new Container();
|
|
1087
|
+
const total = sumUsage(details.results);
|
|
1088
|
+
const totalUsg = usageLine(total);
|
|
1042
1089
|
container.addChild(
|
|
1043
1090
|
new Text(
|
|
1044
1091
|
`${icon} ${theme.fg("accent", `${done} tasks`)}`,
|
|
@@ -1048,11 +1095,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1048
1095
|
);
|
|
1049
1096
|
for (const r of details.results) {
|
|
1050
1097
|
const out = getFinalOutputText(r.messages);
|
|
1098
|
+
const stepUsage = usageLine(r.usage);
|
|
1099
|
+
const stepUsg = stepUsage ? theme.fg("dim", stepUsage) : "";
|
|
1051
1100
|
if (out) {
|
|
1052
1101
|
container.addChild(new Spacer(1));
|
|
1053
1102
|
container.addChild(
|
|
1054
1103
|
new Text(
|
|
1055
|
-
`${getStatusText(r)} ${theme.fg("accent", r.agent)}`,
|
|
1104
|
+
`${getStatusText(r)} ${theme.fg("accent", r.agent)}${stepUsg ? " " + stepUsg : ""}`,
|
|
1056
1105
|
0,
|
|
1057
1106
|
0,
|
|
1058
1107
|
),
|
|
@@ -1060,11 +1109,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1060
1109
|
container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
|
|
1061
1110
|
}
|
|
1062
1111
|
}
|
|
1112
|
+
if (totalUsg && details.results.length > 1) {
|
|
1113
|
+
container.addChild(new Spacer(1));
|
|
1114
|
+
container.addChild(new Text(totalUsg, 0, 0));
|
|
1115
|
+
}
|
|
1063
1116
|
return container;
|
|
1064
1117
|
}
|
|
1065
1118
|
|
|
1119
|
+
const usg = usageLine(sumUsage(details.results));
|
|
1066
1120
|
let text = `${icon} ${theme.fg("accent", `${done}/${details.results.length} tasks`)}`;
|
|
1067
|
-
if (running === 0
|
|
1121
|
+
if (running === 0) {
|
|
1122
|
+
if (usg) text += `\n${usg}`;
|
|
1123
|
+
if (!expanded) text += theme.fg("muted", " (Ctrl+O to expand)");
|
|
1124
|
+
}
|
|
1068
1125
|
return new Text(text, 0, 0);
|
|
1069
1126
|
}
|
|
1070
1127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fosterg4/pi-subagent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Delegate tasks to specialized subagents with isolated context windows, structured JSON handoff, contract schemas, and live TUI streaming",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/ui.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code-style live progress widget for subagent execution.
|
|
3
|
+
*
|
|
4
|
+
* Renders a compact overlay showing spinner + agent name + stats.
|
|
5
|
+
* Designed to be used with pi's ctx.ui.custom(…) overlay system.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const widget = new AgentWidget();
|
|
9
|
+
* widget.addAgent("scout", "Find auth files");
|
|
10
|
+
* const handle = ctx.ui.custom(widget, { overlay: true });
|
|
11
|
+
* widget.updateAgent("scout", { turns: 3, tokens: 12400, elapsedMs: 4100 });
|
|
12
|
+
* handle.close();
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
16
|
+
import { fmt } from "./utils.ts";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export type AgentStatus = "running" | "done" | "error";
|
|
23
|
+
|
|
24
|
+
export interface WidgetEntry {
|
|
25
|
+
name: string;
|
|
26
|
+
task: string;
|
|
27
|
+
status: AgentStatus;
|
|
28
|
+
turns: number;
|
|
29
|
+
tokens: number;
|
|
30
|
+
contextUsagePct?: number;
|
|
31
|
+
elapsedMs: number;
|
|
32
|
+
model?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Spinner
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function formatTime(ms: number): string {
|
|
46
|
+
const s = ms / 1000;
|
|
47
|
+
if (s < 10) return `${s.toFixed(1)}s`;
|
|
48
|
+
if (s < 60) return `${Math.round(s)}s`;
|
|
49
|
+
const m = Math.floor(s / 60);
|
|
50
|
+
const sec = Math.round(s % 60);
|
|
51
|
+
return `${m}m${sec}s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function statusIcon(status: AgentStatus, frame: number): string {
|
|
55
|
+
switch (status) {
|
|
56
|
+
case "done":
|
|
57
|
+
return "✓";
|
|
58
|
+
case "error":
|
|
59
|
+
return "✗";
|
|
60
|
+
case "running":
|
|
61
|
+
default:
|
|
62
|
+
return SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Widget
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export class AgentWidget {
|
|
71
|
+
private entries: WidgetEntry[] = [];
|
|
72
|
+
private frame = 0;
|
|
73
|
+
private startTimes: Map<string, number> = new Map();
|
|
74
|
+
private cachedWidth?: number;
|
|
75
|
+
private cachedLines?: string[];
|
|
76
|
+
|
|
77
|
+
addAgent(name: string, task: string): void {
|
|
78
|
+
const exists = this.entries.find((e) => e.name === name);
|
|
79
|
+
if (exists) return;
|
|
80
|
+
this.entries.push({
|
|
81
|
+
name,
|
|
82
|
+
task,
|
|
83
|
+
status: "running",
|
|
84
|
+
turns: 0,
|
|
85
|
+
tokens: 0,
|
|
86
|
+
elapsedMs: 0,
|
|
87
|
+
});
|
|
88
|
+
this.startTimes.set(name, Date.now());
|
|
89
|
+
this.invalidate();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updateAgent(name: string, update: Partial<WidgetEntry>): void {
|
|
93
|
+
const entry = this.entries.find((e) => e.name === name);
|
|
94
|
+
if (!entry) return;
|
|
95
|
+
Object.assign(entry, update);
|
|
96
|
+
entry.elapsedMs = Date.now() - (this.startTimes.get(name) ?? Date.now());
|
|
97
|
+
this.invalidate();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
removeAgent(name: string): void {
|
|
101
|
+
this.entries = this.entries.filter((e) => e.name !== name);
|
|
102
|
+
this.startTimes.delete(name);
|
|
103
|
+
this.invalidate();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getAgent(name: string): WidgetEntry | undefined {
|
|
107
|
+
return this.entries.find((e) => e.name === name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
invalidate(): void {
|
|
111
|
+
this.cachedWidth = undefined;
|
|
112
|
+
this.cachedLines = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render(width: number): string[] {
|
|
116
|
+
if (this.cachedLines && this.cachedWidth === width) {
|
|
117
|
+
return this.cachedLines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
|
|
122
|
+
if (this.entries.length === 0) {
|
|
123
|
+
this.cachedWidth = width;
|
|
124
|
+
this.cachedLines = [];
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Header
|
|
129
|
+
lines.push("● Agents");
|
|
130
|
+
|
|
131
|
+
for (const entry of this.entries) {
|
|
132
|
+
const icon = statusIcon(entry.status, this.frame);
|
|
133
|
+
const parts: string[] = [];
|
|
134
|
+
|
|
135
|
+
// Turns
|
|
136
|
+
if (entry.turns > 0) parts.push(`↻${entry.turns}`);
|
|
137
|
+
|
|
138
|
+
// Tokens
|
|
139
|
+
if (entry.tokens > 0) {
|
|
140
|
+
const tokStr = fmt(entry.tokens);
|
|
141
|
+
parts.push(`${tokStr} token`);
|
|
142
|
+
if (entry.contextUsagePct !== undefined && entry.contextUsagePct > 0) {
|
|
143
|
+
parts.push(`(${entry.contextUsagePct}%)`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Time
|
|
148
|
+
if (entry.elapsedMs > 0) parts.push(formatTime(entry.elapsedMs));
|
|
149
|
+
|
|
150
|
+
// Model
|
|
151
|
+
if (entry.model) parts.push(entry.model);
|
|
152
|
+
|
|
153
|
+
const stats = parts.length > 0 ? ` · ${parts.join(" · ")}` : "";
|
|
154
|
+
|
|
155
|
+
let line = ` ${icon} ${entry.name}${stats}`;
|
|
156
|
+
if (line.length > width) {
|
|
157
|
+
line = line.slice(0, width - 1) + "…";
|
|
158
|
+
}
|
|
159
|
+
lines.push(line);
|
|
160
|
+
|
|
161
|
+
// Activity sub-line
|
|
162
|
+
if (entry.task && entry.status === "running") {
|
|
163
|
+
const preview =
|
|
164
|
+
entry.task.length > 60
|
|
165
|
+
? `${entry.task.slice(0, 57)}…`
|
|
166
|
+
: entry.task;
|
|
167
|
+
lines.push(` ⎿ ${preview}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.frame++;
|
|
172
|
+
this.cachedWidth = width;
|
|
173
|
+
this.cachedLines = lines;
|
|
174
|
+
return lines;
|
|
175
|
+
}
|
|
176
|
+
}
|
package/utils.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting helpers — testable without pi runtime deps
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { UsageStats } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
export function fmt(n: number): string {
|
|
8
|
+
if (n < 1000) return n.toString();
|
|
9
|
+
if (n < 10000) return (n / 1000).toFixed(1) + "k";
|
|
10
|
+
if (n < 1000000) return Math.round(n / 1000) + "k";
|
|
11
|
+
return (n / 1000000).toFixed(1) + "M";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function usageLine(u: UsageStats): string {
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
if (u.input) parts.push("\u2191" + fmt(u.input));
|
|
17
|
+
if (u.output) parts.push("\u2193" + fmt(u.output));
|
|
18
|
+
if (u.cacheRead) {
|
|
19
|
+
parts.push("R" + fmt(u.cacheRead));
|
|
20
|
+
const total = u.input + u.cacheRead;
|
|
21
|
+
if (total > 0) parts.push("CH" + ((u.cacheRead / total) * 100).toFixed(1) + "%");
|
|
22
|
+
}
|
|
23
|
+
if (u.cost) parts.push("$" + u.cost.toFixed(4));
|
|
24
|
+
return parts.join(" ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sumUsage(results: ReadonlyArray<{ usage: UsageStats }>): UsageStats {
|
|
28
|
+
const u: UsageStats = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
29
|
+
for (const r of results) {
|
|
30
|
+
u.input += r.usage.input;
|
|
31
|
+
u.output += r.usage.output;
|
|
32
|
+
u.cacheRead += r.usage.cacheRead;
|
|
33
|
+
u.cacheWrite += r.usage.cacheWrite;
|
|
34
|
+
u.cost += r.usage.cost;
|
|
35
|
+
}
|
|
36
|
+
return u;
|
|
37
|
+
}
|