@stigmer/react 0.0.55 → 0.0.57

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.
Files changed (81) hide show
  1. package/execution/ApprovalCard.d.ts.map +1 -1
  2. package/execution/ApprovalCard.js +1 -1
  3. package/execution/ApprovalCard.js.map +1 -1
  4. package/execution/ArtifactsWidget.d.ts +1 -1
  5. package/execution/ArtifactsWidget.js +1 -1
  6. package/execution/ExecutionProgress.d.ts.map +1 -1
  7. package/execution/ExecutionProgress.js +2 -59
  8. package/execution/ExecutionProgress.js.map +1 -1
  9. package/execution/MessageThread.d.ts.map +1 -1
  10. package/execution/MessageThread.js +31 -6
  11. package/execution/MessageThread.js.map +1 -1
  12. package/execution/SubAgentSection.d.ts +25 -4
  13. package/execution/SubAgentSection.d.ts.map +1 -1
  14. package/execution/SubAgentSection.js +70 -11
  15. package/execution/SubAgentSection.js.map +1 -1
  16. package/execution/TodoList.d.ts +42 -0
  17. package/execution/TodoList.d.ts.map +1 -0
  18. package/execution/TodoList.js +108 -0
  19. package/execution/TodoList.js.map +1 -0
  20. package/execution/ToolCallItem.js +1 -1
  21. package/execution/ToolCallItem.js.map +1 -1
  22. package/execution/UsageWidget.d.ts +57 -0
  23. package/execution/UsageWidget.d.ts.map +1 -0
  24. package/execution/UsageWidget.js +72 -0
  25. package/execution/UsageWidget.js.map +1 -0
  26. package/execution/index.d.ts +4 -4
  27. package/execution/index.d.ts.map +1 -1
  28. package/execution/index.js +2 -2
  29. package/execution/index.js.map +1 -1
  30. package/execution/useExecutionArtifacts.d.ts +1 -1
  31. package/execution/useExecutionArtifacts.js +1 -1
  32. package/index.d.ts +4 -4
  33. package/index.d.ts.map +1 -1
  34. package/index.js +2 -2
  35. package/index.js.map +1 -1
  36. package/package.json +4 -4
  37. package/session/index.d.ts +2 -0
  38. package/session/index.d.ts.map +1 -1
  39. package/session/index.js +1 -0
  40. package/session/index.js.map +1 -1
  41. package/session/useSessionConversation.d.ts.map +1 -1
  42. package/session/useSessionConversation.js +42 -5
  43. package/session/useSessionConversation.js.map +1 -1
  44. package/session/useSessionUsage.d.ts +65 -0
  45. package/session/useSessionUsage.d.ts.map +1 -0
  46. package/session/useSessionUsage.js +107 -0
  47. package/session/useSessionUsage.js.map +1 -0
  48. package/src/execution/ApprovalCard.tsx +7 -13
  49. package/src/execution/ArtifactsWidget.tsx +1 -1
  50. package/src/execution/ExecutionProgress.tsx +2 -134
  51. package/src/execution/MessageThread.tsx +39 -6
  52. package/src/execution/SubAgentSection.tsx +323 -16
  53. package/src/execution/TodoList.tsx +202 -0
  54. package/src/execution/ToolCallItem.tsx +1 -1
  55. package/src/execution/{ExecutionCostSummary.tsx → UsageWidget.tsx} +43 -50
  56. package/src/execution/index.ts +10 -4
  57. package/src/execution/useExecutionArtifacts.ts +1 -1
  58. package/src/index.ts +12 -5
  59. package/src/session/index.ts +6 -0
  60. package/src/session/useSessionConversation.ts +56 -7
  61. package/src/session/useSessionUsage.ts +159 -0
  62. package/styles.css +1 -1
  63. package/execution/ExecutionCostSummary.d.ts +0 -47
  64. package/execution/ExecutionCostSummary.d.ts.map +0 -1
  65. package/execution/ExecutionCostSummary.js +0 -77
  66. package/execution/ExecutionCostSummary.js.map +0 -1
  67. package/execution/__tests__/ExecutionCostSummary.test.d.ts +0 -2
  68. package/execution/__tests__/ExecutionCostSummary.test.d.ts.map +0 -1
  69. package/execution/__tests__/ExecutionCostSummary.test.js +0 -255
  70. package/execution/__tests__/ExecutionCostSummary.test.js.map +0 -1
  71. package/execution/__tests__/useExecutionUsage.test.d.ts +0 -2
  72. package/execution/__tests__/useExecutionUsage.test.d.ts.map +0 -1
  73. package/execution/__tests__/useExecutionUsage.test.js +0 -303
  74. package/execution/__tests__/useExecutionUsage.test.js.map +0 -1
  75. package/execution/useExecutionUsage.d.ts +0 -45
  76. package/execution/useExecutionUsage.d.ts.map +0 -1
  77. package/execution/useExecutionUsage.js +0 -157
  78. package/execution/useExecutionUsage.js.map +0 -1
  79. package/src/execution/__tests__/ExecutionCostSummary.test.tsx +0 -416
  80. package/src/execution/__tests__/useExecutionUsage.test.tsx +0 -408
  81. 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
- }