@stigmer/react 0.0.56 → 0.0.58
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/execution/ApprovalCard.d.ts.map +1 -1
- package/execution/ApprovalCard.js +1 -1
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ArtifactsWidget.d.ts +1 -1
- package/execution/ArtifactsWidget.js +1 -1
- package/execution/ExecutionProgress.d.ts.map +1 -1
- package/execution/ExecutionProgress.js +2 -59
- package/execution/ExecutionProgress.js.map +1 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +31 -6
- package/execution/MessageThread.js.map +1 -1
- package/execution/SubAgentSection.d.ts +25 -4
- package/execution/SubAgentSection.d.ts.map +1 -1
- package/execution/SubAgentSection.js +70 -11
- package/execution/SubAgentSection.js.map +1 -1
- package/execution/TodoList.d.ts +42 -0
- package/execution/TodoList.d.ts.map +1 -0
- package/execution/TodoList.js +108 -0
- package/execution/TodoList.js.map +1 -0
- package/execution/ToolCallItem.js +1 -1
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/UsageWidget.d.ts +57 -0
- package/execution/UsageWidget.d.ts.map +1 -0
- package/execution/UsageWidget.js +72 -0
- package/execution/UsageWidget.js.map +1 -0
- package/execution/index.d.ts +4 -4
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +2 -2
- package/execution/index.js.map +1 -1
- package/execution/useExecutionArtifacts.d.ts +1 -1
- package/execution/useExecutionArtifacts.js +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/session/index.d.ts +2 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +1 -0
- package/session/index.js.map +1 -1
- package/session/useSessionUsage.d.ts +65 -0
- package/session/useSessionUsage.d.ts.map +1 -0
- package/session/useSessionUsage.js +107 -0
- package/session/useSessionUsage.js.map +1 -0
- package/src/execution/ApprovalCard.tsx +7 -13
- package/src/execution/ArtifactsWidget.tsx +1 -1
- package/src/execution/ExecutionProgress.tsx +2 -134
- package/src/execution/MessageThread.tsx +39 -6
- package/src/execution/SubAgentSection.tsx +323 -16
- package/src/execution/TodoList.tsx +202 -0
- package/src/execution/ToolCallItem.tsx +1 -1
- package/src/execution/{ExecutionCostSummary.tsx → UsageWidget.tsx} +43 -50
- package/src/execution/index.ts +10 -4
- package/src/execution/useExecutionArtifacts.ts +1 -1
- package/src/index.ts +12 -5
- package/src/session/index.ts +6 -0
- package/src/session/useSessionUsage.ts +159 -0
- package/styles.css +1 -1
- package/execution/ExecutionCostSummary.d.ts +0 -47
- package/execution/ExecutionCostSummary.d.ts.map +0 -1
- package/execution/ExecutionCostSummary.js +0 -77
- package/execution/ExecutionCostSummary.js.map +0 -1
- package/execution/__tests__/ExecutionCostSummary.test.d.ts +0 -2
- package/execution/__tests__/ExecutionCostSummary.test.d.ts.map +0 -1
- package/execution/__tests__/ExecutionCostSummary.test.js +0 -255
- package/execution/__tests__/ExecutionCostSummary.test.js.map +0 -1
- package/execution/__tests__/useExecutionUsage.test.d.ts +0 -2
- package/execution/__tests__/useExecutionUsage.test.d.ts.map +0 -1
- package/execution/__tests__/useExecutionUsage.test.js +0 -303
- package/execution/__tests__/useExecutionUsage.test.js.map +0 -1
- package/execution/useExecutionUsage.d.ts +0 -45
- package/execution/useExecutionUsage.d.ts.map +0 -1
- package/execution/useExecutionUsage.js +0 -157
- package/execution/useExecutionUsage.js.map +0 -1
- package/src/execution/__tests__/ExecutionCostSummary.test.tsx +0 -416
- package/src/execution/__tests__/useExecutionUsage.test.tsx +0 -408
- package/src/execution/useExecutionUsage.ts +0 -213
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { renderHook } from "@testing-library/react";
|
|
3
|
-
import { create } from "@bufbuild/protobuf";
|
|
4
|
-
import {
|
|
5
|
-
AgentExecutionSchema,
|
|
6
|
-
AgentExecutionStatusSchema,
|
|
7
|
-
type AgentExecution,
|
|
8
|
-
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
9
|
-
import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
10
|
-
import {
|
|
11
|
-
UsageMetricsSchema,
|
|
12
|
-
ModelUsageSchema,
|
|
13
|
-
LlmCallMetricsSchema,
|
|
14
|
-
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
|
|
15
|
-
import { aggregateUsage, useExecutionUsage } from "../useExecutionUsage";
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Test helpers
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
function makeUsage(
|
|
22
|
-
overrides: Partial<{
|
|
23
|
-
promptTokens: number;
|
|
24
|
-
completionTokens: number;
|
|
25
|
-
totalTokens: number;
|
|
26
|
-
llmCallCount: number;
|
|
27
|
-
estimatedCostUsd: number;
|
|
28
|
-
primaryModel: string;
|
|
29
|
-
primaryProvider: string;
|
|
30
|
-
totalDurationMs: number;
|
|
31
|
-
llmDurationMs: number;
|
|
32
|
-
toolDurationMs: number;
|
|
33
|
-
approvalWaitDurationMs: number;
|
|
34
|
-
cacheCreationTokens: number;
|
|
35
|
-
cacheReadTokens: number;
|
|
36
|
-
toolResultCharsTruncated: bigint;
|
|
37
|
-
modelBreakdown: ReturnType<typeof makeModelUsage>[];
|
|
38
|
-
llmCalls: ReturnType<typeof makeLlmCall>[];
|
|
39
|
-
}> = {},
|
|
40
|
-
) {
|
|
41
|
-
return create(UsageMetricsSchema, overrides);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function makeModelUsage(
|
|
45
|
-
model: string,
|
|
46
|
-
provider: string,
|
|
47
|
-
overrides: Partial<{
|
|
48
|
-
inputTokens: number;
|
|
49
|
-
outputTokens: number;
|
|
50
|
-
callCount: number;
|
|
51
|
-
estimatedCostUsd: number;
|
|
52
|
-
cacheCreationTokens: number;
|
|
53
|
-
cacheReadTokens: number;
|
|
54
|
-
inputPricePerMillion: number;
|
|
55
|
-
outputPricePerMillion: number;
|
|
56
|
-
}> = {},
|
|
57
|
-
) {
|
|
58
|
-
return create(ModelUsageSchema, { model, provider, ...overrides });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function makeLlmCall(
|
|
62
|
-
sequence: number,
|
|
63
|
-
timestamp: string,
|
|
64
|
-
overrides: Partial<{
|
|
65
|
-
model: string;
|
|
66
|
-
provider: string;
|
|
67
|
-
inputTokens: number;
|
|
68
|
-
outputTokens: number;
|
|
69
|
-
estimatedCostUsd: number;
|
|
70
|
-
durationMs: number;
|
|
71
|
-
}> = {},
|
|
72
|
-
) {
|
|
73
|
-
return create(LlmCallMetricsSchema, { sequence, timestamp, ...overrides });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function makeExecution(
|
|
77
|
-
mainUsage?: ReturnType<typeof makeUsage>,
|
|
78
|
-
subAgents: Array<{
|
|
79
|
-
name: string;
|
|
80
|
-
usage?: ReturnType<typeof makeUsage>;
|
|
81
|
-
}> = [],
|
|
82
|
-
): AgentExecution {
|
|
83
|
-
const status = create(AgentExecutionStatusSchema, {
|
|
84
|
-
usage: mainUsage,
|
|
85
|
-
subAgentExecutions: subAgents.map((s) =>
|
|
86
|
-
create(SubAgentExecutionSchema, { name: s.name, usage: s.usage }),
|
|
87
|
-
),
|
|
88
|
-
});
|
|
89
|
-
return create(AgentExecutionSchema, { status });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// aggregateUsage — pure function tests
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
describe("aggregateUsage", () => {
|
|
97
|
-
it("returns null when execution is null", () => {
|
|
98
|
-
expect(aggregateUsage(null)).toBeNull();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("returns null when status is undefined", () => {
|
|
102
|
-
const exec = create(AgentExecutionSchema);
|
|
103
|
-
expect(aggregateUsage(exec)).toBeNull();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("returns null when status.usage is undefined", () => {
|
|
107
|
-
const exec = create(AgentExecutionSchema, {
|
|
108
|
-
status: create(AgentExecutionStatusSchema),
|
|
109
|
-
});
|
|
110
|
-
expect(aggregateUsage(exec)).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("returns main-only usage when no sub-agents exist", () => {
|
|
114
|
-
const usage = makeUsage({
|
|
115
|
-
promptTokens: 100,
|
|
116
|
-
completionTokens: 50,
|
|
117
|
-
totalTokens: 150,
|
|
118
|
-
llmCallCount: 3,
|
|
119
|
-
estimatedCostUsd: 0.005,
|
|
120
|
-
primaryModel: "claude-sonnet-4",
|
|
121
|
-
primaryProvider: "anthropic",
|
|
122
|
-
});
|
|
123
|
-
const exec = makeExecution(usage);
|
|
124
|
-
|
|
125
|
-
const result = aggregateUsage(exec);
|
|
126
|
-
expect(result).toBe(usage);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("returns main-only usage when sub-agents exist but none have usage", () => {
|
|
130
|
-
const usage = makeUsage({ promptTokens: 100, totalTokens: 100 });
|
|
131
|
-
const exec = makeExecution(usage, [
|
|
132
|
-
{ name: "researcher" },
|
|
133
|
-
{ name: "coder" },
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
const result = aggregateUsage(exec);
|
|
137
|
-
expect(result).toBe(usage);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("sums token counts across main and sub-agents", () => {
|
|
141
|
-
const main = makeUsage({
|
|
142
|
-
promptTokens: 100,
|
|
143
|
-
completionTokens: 50,
|
|
144
|
-
totalTokens: 150,
|
|
145
|
-
cacheCreationTokens: 10,
|
|
146
|
-
cacheReadTokens: 20,
|
|
147
|
-
});
|
|
148
|
-
const sub1 = makeUsage({
|
|
149
|
-
promptTokens: 200,
|
|
150
|
-
completionTokens: 80,
|
|
151
|
-
totalTokens: 280,
|
|
152
|
-
cacheCreationTokens: 5,
|
|
153
|
-
cacheReadTokens: 30,
|
|
154
|
-
});
|
|
155
|
-
const sub2 = makeUsage({
|
|
156
|
-
promptTokens: 50,
|
|
157
|
-
completionTokens: 20,
|
|
158
|
-
totalTokens: 70,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const exec = makeExecution(main, [
|
|
162
|
-
{ name: "sub1", usage: sub1 },
|
|
163
|
-
{ name: "sub2", usage: sub2 },
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
const result = aggregateUsage(exec)!;
|
|
167
|
-
expect(result.promptTokens).toBe(350);
|
|
168
|
-
expect(result.completionTokens).toBe(150);
|
|
169
|
-
expect(result.totalTokens).toBe(500);
|
|
170
|
-
expect(result.cacheCreationTokens).toBe(15);
|
|
171
|
-
expect(result.cacheReadTokens).toBe(50);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("sums cost across main and sub-agents", () => {
|
|
175
|
-
const main = makeUsage({ estimatedCostUsd: 0.01 });
|
|
176
|
-
const sub = makeUsage({ estimatedCostUsd: 0.005 });
|
|
177
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
178
|
-
|
|
179
|
-
const result = aggregateUsage(exec)!;
|
|
180
|
-
expect(result.estimatedCostUsd).toBeCloseTo(0.015);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("sums durations across main and sub-agents", () => {
|
|
184
|
-
const main = makeUsage({
|
|
185
|
-
totalDurationMs: 5000,
|
|
186
|
-
llmDurationMs: 3000,
|
|
187
|
-
toolDurationMs: 1500,
|
|
188
|
-
approvalWaitDurationMs: 500,
|
|
189
|
-
});
|
|
190
|
-
const sub = makeUsage({
|
|
191
|
-
totalDurationMs: 2000,
|
|
192
|
-
llmDurationMs: 1000,
|
|
193
|
-
toolDurationMs: 800,
|
|
194
|
-
approvalWaitDurationMs: 200,
|
|
195
|
-
});
|
|
196
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
197
|
-
|
|
198
|
-
const result = aggregateUsage(exec)!;
|
|
199
|
-
expect(result.totalDurationMs).toBe(7000);
|
|
200
|
-
expect(result.llmDurationMs).toBe(4000);
|
|
201
|
-
expect(result.toolDurationMs).toBe(2300);
|
|
202
|
-
expect(result.approvalWaitDurationMs).toBe(700);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("uses main agent primaryModel and primaryProvider", () => {
|
|
206
|
-
const main = makeUsage({
|
|
207
|
-
primaryModel: "claude-sonnet-4",
|
|
208
|
-
primaryProvider: "anthropic",
|
|
209
|
-
});
|
|
210
|
-
const sub = makeUsage({
|
|
211
|
-
primaryModel: "gpt-4o",
|
|
212
|
-
primaryProvider: "openai",
|
|
213
|
-
});
|
|
214
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
215
|
-
|
|
216
|
-
const result = aggregateUsage(exec)!;
|
|
217
|
-
expect(result.primaryModel).toBe("claude-sonnet-4");
|
|
218
|
-
expect(result.primaryProvider).toBe("anthropic");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("merges modelBreakdown entries with same model+provider key", () => {
|
|
222
|
-
const main = makeUsage({
|
|
223
|
-
modelBreakdown: [
|
|
224
|
-
makeModelUsage("claude-sonnet-4", "anthropic", {
|
|
225
|
-
inputTokens: 100,
|
|
226
|
-
outputTokens: 50,
|
|
227
|
-
callCount: 2,
|
|
228
|
-
estimatedCostUsd: 0.005,
|
|
229
|
-
}),
|
|
230
|
-
],
|
|
231
|
-
});
|
|
232
|
-
const sub = makeUsage({
|
|
233
|
-
modelBreakdown: [
|
|
234
|
-
makeModelUsage("claude-sonnet-4", "anthropic", {
|
|
235
|
-
inputTokens: 200,
|
|
236
|
-
outputTokens: 80,
|
|
237
|
-
callCount: 3,
|
|
238
|
-
estimatedCostUsd: 0.008,
|
|
239
|
-
}),
|
|
240
|
-
],
|
|
241
|
-
});
|
|
242
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
243
|
-
|
|
244
|
-
const result = aggregateUsage(exec)!;
|
|
245
|
-
expect(result.modelBreakdown).toHaveLength(1);
|
|
246
|
-
expect(result.modelBreakdown[0].model).toBe("claude-sonnet-4");
|
|
247
|
-
expect(result.modelBreakdown[0].provider).toBe("anthropic");
|
|
248
|
-
expect(result.modelBreakdown[0].inputTokens).toBe(300);
|
|
249
|
-
expect(result.modelBreakdown[0].outputTokens).toBe(130);
|
|
250
|
-
expect(result.modelBreakdown[0].callCount).toBe(5);
|
|
251
|
-
expect(result.modelBreakdown[0].estimatedCostUsd).toBeCloseTo(0.013);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("keeps modelBreakdown entries with different models separate", () => {
|
|
255
|
-
const main = makeUsage({
|
|
256
|
-
modelBreakdown: [
|
|
257
|
-
makeModelUsage("claude-sonnet-4", "anthropic", { callCount: 2 }),
|
|
258
|
-
],
|
|
259
|
-
});
|
|
260
|
-
const sub = makeUsage({
|
|
261
|
-
modelBreakdown: [
|
|
262
|
-
makeModelUsage("gpt-4o", "openai", { callCount: 1 }),
|
|
263
|
-
],
|
|
264
|
-
});
|
|
265
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
266
|
-
|
|
267
|
-
const result = aggregateUsage(exec)!;
|
|
268
|
-
expect(result.modelBreakdown).toHaveLength(2);
|
|
269
|
-
|
|
270
|
-
const models = result.modelBreakdown.map((m) => m.model).sort();
|
|
271
|
-
expect(models).toEqual(["claude-sonnet-4", "gpt-4o"]);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("concatenates llmCalls from all agents sorted by timestamp", () => {
|
|
275
|
-
const main = makeUsage({
|
|
276
|
-
llmCalls: [
|
|
277
|
-
makeLlmCall(1, "2026-03-19T10:00:00Z"),
|
|
278
|
-
makeLlmCall(2, "2026-03-19T10:01:00Z"),
|
|
279
|
-
],
|
|
280
|
-
});
|
|
281
|
-
const sub = makeUsage({
|
|
282
|
-
llmCalls: [
|
|
283
|
-
makeLlmCall(1, "2026-03-19T10:00:30Z"),
|
|
284
|
-
],
|
|
285
|
-
});
|
|
286
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
287
|
-
|
|
288
|
-
const result = aggregateUsage(exec)!;
|
|
289
|
-
expect(result.llmCalls).toHaveLength(3);
|
|
290
|
-
expect(result.llmCalls[0].timestamp).toBe("2026-03-19T10:00:00Z");
|
|
291
|
-
expect(result.llmCalls[1].timestamp).toBe("2026-03-19T10:00:30Z");
|
|
292
|
-
expect(result.llmCalls[2].timestamp).toBe("2026-03-19T10:01:00Z");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("skips sub-agents with undefined usage gracefully", () => {
|
|
296
|
-
const main = makeUsage({
|
|
297
|
-
promptTokens: 100,
|
|
298
|
-
llmCallCount: 2,
|
|
299
|
-
});
|
|
300
|
-
const subWithUsage = makeUsage({
|
|
301
|
-
promptTokens: 50,
|
|
302
|
-
llmCallCount: 1,
|
|
303
|
-
});
|
|
304
|
-
const exec = makeExecution(main, [
|
|
305
|
-
{ name: "has-usage", usage: subWithUsage },
|
|
306
|
-
{ name: "no-usage" },
|
|
307
|
-
]);
|
|
308
|
-
|
|
309
|
-
const result = aggregateUsage(exec)!;
|
|
310
|
-
expect(result.promptTokens).toBe(150);
|
|
311
|
-
expect(result.llmCallCount).toBe(3);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("sums toolResultCharsTruncated as bigint", () => {
|
|
315
|
-
const main = makeUsage({ toolResultCharsTruncated: 1000n });
|
|
316
|
-
const sub = makeUsage({ toolResultCharsTruncated: 500n });
|
|
317
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
318
|
-
|
|
319
|
-
const result = aggregateUsage(exec)!;
|
|
320
|
-
expect(result.toolResultCharsTruncated).toBe(1500n);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("preserves pricing rates from first model entry", () => {
|
|
324
|
-
const main = makeUsage({
|
|
325
|
-
modelBreakdown: [
|
|
326
|
-
makeModelUsage("claude-sonnet-4", "anthropic", {
|
|
327
|
-
inputPricePerMillion: 3.0,
|
|
328
|
-
outputPricePerMillion: 15.0,
|
|
329
|
-
callCount: 1,
|
|
330
|
-
}),
|
|
331
|
-
],
|
|
332
|
-
});
|
|
333
|
-
const sub = makeUsage({
|
|
334
|
-
modelBreakdown: [
|
|
335
|
-
makeModelUsage("claude-sonnet-4", "anthropic", {
|
|
336
|
-
inputPricePerMillion: 3.0,
|
|
337
|
-
outputPricePerMillion: 15.0,
|
|
338
|
-
callCount: 1,
|
|
339
|
-
}),
|
|
340
|
-
],
|
|
341
|
-
});
|
|
342
|
-
const exec = makeExecution(main, [{ name: "sub", usage: sub }]);
|
|
343
|
-
|
|
344
|
-
const result = aggregateUsage(exec)!;
|
|
345
|
-
expect(result.modelBreakdown[0].inputPricePerMillion).toBe(3.0);
|
|
346
|
-
expect(result.modelBreakdown[0].outputPricePerMillion).toBe(15.0);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// ---------------------------------------------------------------------------
|
|
351
|
-
// useExecutionUsage — hook tests
|
|
352
|
-
// ---------------------------------------------------------------------------
|
|
353
|
-
|
|
354
|
-
describe("useExecutionUsage", () => {
|
|
355
|
-
it("returns null usage and zero metadata for null execution", () => {
|
|
356
|
-
const { result } = renderHook(() => useExecutionUsage(null));
|
|
357
|
-
|
|
358
|
-
expect(result.current.usage).toBeNull();
|
|
359
|
-
expect(result.current.hasSubAgentUsage).toBe(false);
|
|
360
|
-
expect(result.current.subAgentUsageCount).toBe(0);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("returns aggregated UsageMetrics for valid execution", () => {
|
|
364
|
-
const main = makeUsage({
|
|
365
|
-
promptTokens: 100,
|
|
366
|
-
estimatedCostUsd: 0.01,
|
|
367
|
-
primaryModel: "claude-sonnet-4",
|
|
368
|
-
});
|
|
369
|
-
const exec = makeExecution(main);
|
|
370
|
-
|
|
371
|
-
const { result } = renderHook(() => useExecutionUsage(exec));
|
|
372
|
-
|
|
373
|
-
expect(result.current.usage).not.toBeNull();
|
|
374
|
-
expect(result.current.usage!.promptTokens).toBe(100);
|
|
375
|
-
expect(result.current.usage!.estimatedCostUsd).toBeCloseTo(0.01);
|
|
376
|
-
expect(result.current.usage!.primaryModel).toBe("claude-sonnet-4");
|
|
377
|
-
expect(result.current.hasSubAgentUsage).toBe(false);
|
|
378
|
-
expect(result.current.subAgentUsageCount).toBe(0);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("returns stable reference when execution has not changed", () => {
|
|
382
|
-
const exec = makeExecution(makeUsage({ promptTokens: 100 }));
|
|
383
|
-
const { result, rerender } = renderHook(() => useExecutionUsage(exec));
|
|
384
|
-
|
|
385
|
-
const first = result.current;
|
|
386
|
-
rerender();
|
|
387
|
-
const second = result.current;
|
|
388
|
-
|
|
389
|
-
expect(first).toBe(second);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("sets hasSubAgentUsage and subAgentUsageCount correctly", () => {
|
|
393
|
-
const main = makeUsage({ promptTokens: 100 });
|
|
394
|
-
const sub1 = makeUsage({ promptTokens: 50 });
|
|
395
|
-
const sub2 = makeUsage({ promptTokens: 30 });
|
|
396
|
-
const exec = makeExecution(main, [
|
|
397
|
-
{ name: "sub1", usage: sub1 },
|
|
398
|
-
{ name: "sub2", usage: sub2 },
|
|
399
|
-
{ name: "sub3-no-usage" },
|
|
400
|
-
]);
|
|
401
|
-
|
|
402
|
-
const { result } = renderHook(() => useExecutionUsage(exec));
|
|
403
|
-
|
|
404
|
-
expect(result.current.hasSubAgentUsage).toBe(true);
|
|
405
|
-
expect(result.current.subAgentUsageCount).toBe(2);
|
|
406
|
-
expect(result.current.usage!.promptTokens).toBe(180);
|
|
407
|
-
});
|
|
408
|
-
});
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo } from "react";
|
|
4
|
-
import { create } from "@bufbuild/protobuf";
|
|
5
|
-
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
6
|
-
import type {
|
|
7
|
-
UsageMetrics,
|
|
8
|
-
ModelUsage,
|
|
9
|
-
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
|
|
10
|
-
import {
|
|
11
|
-
UsageMetricsSchema,
|
|
12
|
-
ModelUsageSchema,
|
|
13
|
-
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
|
|
14
|
-
|
|
15
|
-
export interface UseExecutionUsageReturn {
|
|
16
|
-
/** Aggregated usage (main agent + sub-agents). Null before usage data arrives. */
|
|
17
|
-
readonly usage: UsageMetrics | null;
|
|
18
|
-
/** Whether any sub-agent contributed usage to the aggregated total. */
|
|
19
|
-
readonly hasSubAgentUsage: boolean;
|
|
20
|
-
/** Count of sub-agents that have non-null usage data. */
|
|
21
|
-
readonly subAgentUsageCount: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Pure derivation hook that aggregates {@link UsageMetrics} across the
|
|
26
|
-
* main agent and all sub-agents into a single {@link UsageMetrics} total.
|
|
27
|
-
*
|
|
28
|
-
* The proto scoping rule is:
|
|
29
|
-
* > `status.usage` = main agent's direct LLM usage (excludes sub-agents)
|
|
30
|
-
* > `subAgentExecutions[].usage` = each sub-agent's LLM usage
|
|
31
|
-
* > Total cost = `status.usage` + sum(`subAgentExecutions[].usage`)
|
|
32
|
-
*
|
|
33
|
-
* This hook performs that summation, merges `modelBreakdown` entries by
|
|
34
|
-
* `model+provider` key, and concatenates `llmCalls` sorted by timestamp.
|
|
35
|
-
*
|
|
36
|
-
* Returns `null` when the execution is absent or usage data has not yet
|
|
37
|
-
* arrived from the agent runner.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```tsx
|
|
41
|
-
* const { execution } = useExecutionStream(executionId);
|
|
42
|
-
* const { usage, hasSubAgentUsage } = useExecutionUsage(execution);
|
|
43
|
-
*
|
|
44
|
-
* if (usage) {
|
|
45
|
-
* console.log(`Cost: $${usage.estimatedCostUsd}`);
|
|
46
|
-
* console.log(`Tokens: ${usage.totalTokens}`);
|
|
47
|
-
* }
|
|
48
|
-
* ```
|
|
49
|
-
*/
|
|
50
|
-
export function useExecutionUsage(
|
|
51
|
-
execution: AgentExecution | null,
|
|
52
|
-
): UseExecutionUsageReturn {
|
|
53
|
-
return useMemo(() => {
|
|
54
|
-
const usage = aggregateUsage(execution);
|
|
55
|
-
|
|
56
|
-
if (!usage) {
|
|
57
|
-
return { usage: null, hasSubAgentUsage: false, subAgentUsageCount: 0 };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const subAgents = execution?.status?.subAgentExecutions ?? [];
|
|
61
|
-
let subAgentUsageCount = 0;
|
|
62
|
-
for (const sub of subAgents) {
|
|
63
|
-
if (sub.usage) subAgentUsageCount++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
usage,
|
|
68
|
-
hasSubAgentUsage: subAgentUsageCount > 0,
|
|
69
|
-
subAgentUsageCount,
|
|
70
|
-
};
|
|
71
|
-
}, [execution]);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Pure aggregation function — testable without React
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Aggregates usage metrics from the main agent and all sub-agents into
|
|
80
|
-
* a single {@link UsageMetrics} proto object.
|
|
81
|
-
*
|
|
82
|
-
* Returns `null` when the execution or its usage data is not yet available.
|
|
83
|
-
*/
|
|
84
|
-
export function aggregateUsage(
|
|
85
|
-
execution: AgentExecution | null,
|
|
86
|
-
): UsageMetrics | null {
|
|
87
|
-
const mainUsage = execution?.status?.usage;
|
|
88
|
-
if (!mainUsage) return null;
|
|
89
|
-
|
|
90
|
-
const subAgents = execution?.status?.subAgentExecutions ?? [];
|
|
91
|
-
const subUsages: UsageMetrics[] = [];
|
|
92
|
-
for (const sub of subAgents) {
|
|
93
|
-
if (sub.usage) subUsages.push(sub.usage);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (subUsages.length === 0) return mainUsage;
|
|
97
|
-
|
|
98
|
-
const allUsages = [mainUsage, ...subUsages];
|
|
99
|
-
|
|
100
|
-
return create(UsageMetricsSchema, {
|
|
101
|
-
promptTokens: sumField(allUsages, "promptTokens"),
|
|
102
|
-
completionTokens: sumField(allUsages, "completionTokens"),
|
|
103
|
-
totalTokens: sumField(allUsages, "totalTokens"),
|
|
104
|
-
llmCallCount: sumField(allUsages, "llmCallCount"),
|
|
105
|
-
cacheCreationTokens: sumField(allUsages, "cacheCreationTokens"),
|
|
106
|
-
cacheReadTokens: sumField(allUsages, "cacheReadTokens"),
|
|
107
|
-
estimatedCostUsd: sumField(allUsages, "estimatedCostUsd"),
|
|
108
|
-
totalDurationMs: sumField(allUsages, "totalDurationMs"),
|
|
109
|
-
llmDurationMs: sumField(allUsages, "llmDurationMs"),
|
|
110
|
-
toolDurationMs: sumField(allUsages, "toolDurationMs"),
|
|
111
|
-
approvalWaitDurationMs: sumField(allUsages, "approvalWaitDurationMs"),
|
|
112
|
-
toolResultCharsTruncated: sumBigIntField(allUsages),
|
|
113
|
-
primaryModel: mainUsage.primaryModel,
|
|
114
|
-
primaryProvider: mainUsage.primaryProvider,
|
|
115
|
-
modelBreakdown: mergeModelBreakdowns(allUsages),
|
|
116
|
-
llmCalls: mergeLlmCalls(allUsages),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Internal helpers
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
type NumericField = keyof {
|
|
125
|
-
[K in keyof UsageMetrics as UsageMetrics[K] extends number ? K : never]: true;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
function sumField(usages: UsageMetrics[], field: NumericField): number {
|
|
129
|
-
let total = 0;
|
|
130
|
-
for (const u of usages) {
|
|
131
|
-
total += u[field] as number;
|
|
132
|
-
}
|
|
133
|
-
return total;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function sumBigIntField(usages: UsageMetrics[]): bigint {
|
|
137
|
-
let total = BigInt(0);
|
|
138
|
-
for (const u of usages) {
|
|
139
|
-
total += u.toolResultCharsTruncated;
|
|
140
|
-
}
|
|
141
|
-
return total;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Merges model breakdown entries across all usages by `model+provider` key.
|
|
146
|
-
* Entries for the same model and provider are combined into a single
|
|
147
|
-
* {@link ModelUsage} with summed numeric fields. Pricing rates are taken
|
|
148
|
-
* from the first entry encountered for each key (rates are stamped at
|
|
149
|
-
* execution time and are identical for the same model).
|
|
150
|
-
*/
|
|
151
|
-
function mergeModelBreakdowns(usages: UsageMetrics[]): ModelUsage[] {
|
|
152
|
-
const merged = new Map<
|
|
153
|
-
string,
|
|
154
|
-
{
|
|
155
|
-
model: string;
|
|
156
|
-
provider: string;
|
|
157
|
-
inputTokens: number;
|
|
158
|
-
outputTokens: number;
|
|
159
|
-
cacheCreationTokens: number;
|
|
160
|
-
cacheReadTokens: number;
|
|
161
|
-
callCount: number;
|
|
162
|
-
estimatedCostUsd: number;
|
|
163
|
-
inputPricePerMillion: number;
|
|
164
|
-
outputPricePerMillion: number;
|
|
165
|
-
cacheCreationPricePerMillion: number;
|
|
166
|
-
cacheReadPricePerMillion: number;
|
|
167
|
-
}
|
|
168
|
-
>();
|
|
169
|
-
|
|
170
|
-
for (const usage of usages) {
|
|
171
|
-
for (const entry of usage.modelBreakdown) {
|
|
172
|
-
const key = `${entry.model}\0${entry.provider}`;
|
|
173
|
-
const existing = merged.get(key);
|
|
174
|
-
|
|
175
|
-
if (existing) {
|
|
176
|
-
existing.inputTokens += entry.inputTokens;
|
|
177
|
-
existing.outputTokens += entry.outputTokens;
|
|
178
|
-
existing.cacheCreationTokens += entry.cacheCreationTokens;
|
|
179
|
-
existing.cacheReadTokens += entry.cacheReadTokens;
|
|
180
|
-
existing.callCount += entry.callCount;
|
|
181
|
-
existing.estimatedCostUsd += entry.estimatedCostUsd;
|
|
182
|
-
} else {
|
|
183
|
-
merged.set(key, {
|
|
184
|
-
model: entry.model,
|
|
185
|
-
provider: entry.provider,
|
|
186
|
-
inputTokens: entry.inputTokens,
|
|
187
|
-
outputTokens: entry.outputTokens,
|
|
188
|
-
cacheCreationTokens: entry.cacheCreationTokens,
|
|
189
|
-
cacheReadTokens: entry.cacheReadTokens,
|
|
190
|
-
callCount: entry.callCount,
|
|
191
|
-
estimatedCostUsd: entry.estimatedCostUsd,
|
|
192
|
-
inputPricePerMillion: entry.inputPricePerMillion,
|
|
193
|
-
outputPricePerMillion: entry.outputPricePerMillion,
|
|
194
|
-
cacheCreationPricePerMillion: entry.cacheCreationPricePerMillion,
|
|
195
|
-
cacheReadPricePerMillion: entry.cacheReadPricePerMillion,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return Array.from(merged.values()).map((m) => create(ModelUsageSchema, m));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Concatenates `llmCalls` from all usages, sorted by ISO 8601 timestamp.
|
|
206
|
-
* This gives a globally chronological view across main agent and sub-agents,
|
|
207
|
-
* since per-agent `sequence` numbers overlap.
|
|
208
|
-
*/
|
|
209
|
-
function mergeLlmCalls(usages: UsageMetrics[]): UsageMetrics["llmCalls"] {
|
|
210
|
-
const all = usages.flatMap((u) => u.llmCalls);
|
|
211
|
-
if (all.length <= 1) return all;
|
|
212
|
-
return all.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
213
|
-
}
|