@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,51 +1,59 @@
1
1
  "use client";
2
2
 
3
3
  import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
4
- import type { ModelUsage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
5
4
  import { cn } from "@stigmer/theme";
6
- import { useExecutionUsage } from "./useExecutionUsage";
7
-
8
- export interface ExecutionCostSummaryProps {
9
- /** The execution to display cost data for. Renders nothing when null or when usage data has not yet arrived. */
10
- readonly execution: AgentExecution | null;
5
+ import {
6
+ useSessionUsage,
7
+ type ModelCostEntry,
8
+ } from "../session/useSessionUsage";
9
+
10
+ export interface UsageWidgetProps {
11
+ /**
12
+ * All executions for the current session — both completed and
13
+ * actively streaming. The widget aggregates cost data across
14
+ * every execution's per-message `llm_metrics`, presenting a
15
+ * session-level total that never resets.
16
+ *
17
+ * Renders nothing when the list is empty or no execution has
18
+ * usage data.
19
+ */
20
+ readonly executions: readonly AgentExecution[];
21
+ /** Additional CSS classes for the root element. */
11
22
  readonly className?: string;
12
23
  }
13
24
 
14
25
  /**
15
- * Displays real-time execution cost and token usage metrics aggregated
16
- * across the main agent and any sub-agents.
17
- *
18
- * The headline shows estimated USD cost. Below it: model identification,
19
- * total token and LLM call counts, a prompt/completion breakdown, and
20
- * conditional annotations for cache usage and sub-agent contributions.
26
+ * Right-sidebar widget that displays session-level cost and token
27
+ * usage aggregated from per-message {@link LlmCallMetrics}.
21
28
  *
22
- * During active streaming, cost and token numbers update on every
23
- * progressive status snapshot. Uses `tabular-nums` for stable digit
24
- * widths so the layout does not shift as numbers grow.
29
+ * Usage data is computed purely from messages the frontend already
30
+ * has no server RPC is required for the real-time widget.
25
31
  *
26
- * When multiple models are used (common in sub-agent executions), the
27
- * single model line is replaced with a per-model cost breakdown.
32
+ * Returns `null` when no execution has usage data, matching the
33
+ * conditional-render pattern of {@link ArtifactsWidget} and
34
+ * {@link WriteBacksWidget}.
28
35
  *
29
- * Renders without card chrome the consumer provides the container.
30
- * All visual properties flow through `--stgm-*` tokens.
36
+ * All visual properties flow through `--stgm-*` tokens. Zero
37
+ * Console dependencies.
31
38
  *
32
39
  * @example
33
40
  * ```tsx
34
- * const stream = useExecutionStream(executionId);
41
+ * const conv = useSessionConversation(sessionId, org);
35
42
  *
36
- * <div className="rounded-lg border border-border bg-card p-3">
37
- * <ExecutionCostSummary execution={stream.execution} />
38
- * </div>
43
+ * <UsageWidget
44
+ * executions={[
45
+ * ...conv.completedExecutions,
46
+ * ...(conv.activeStreamExecution ? [conv.activeStreamExecution] : []),
47
+ * ]}
48
+ * />
39
49
  * ```
50
+ *
51
+ * @see {@link useSessionUsage} — headless session-level usage aggregation hook
40
52
  */
41
- export function ExecutionCostSummary({
42
- execution,
43
- className,
44
- }: ExecutionCostSummaryProps) {
45
- const { usage, hasSubAgentUsage, subAgentUsageCount } =
46
- useExecutionUsage(execution);
53
+ export function UsageWidget({ executions, className }: UsageWidgetProps) {
54
+ const usage = useSessionUsage(executions);
47
55
 
48
- if (!usage) return null;
56
+ if (!usage.hasUsage) return null;
49
57
 
50
58
  const multiModel = usage.modelBreakdown.length > 1;
51
59
 
@@ -53,10 +61,10 @@ export function ExecutionCostSummary({
53
61
  <div
54
62
  className={cn("flex flex-col gap-1.5", className)}
55
63
  role="region"
56
- aria-label="Execution cost summary"
64
+ aria-label="Session cost summary"
57
65
  >
58
66
  <div className="text-sm font-medium tabular-nums text-foreground">
59
- {formatCost(usage.estimatedCostUsd)}
67
+ {formatCost(usage.totalCostUsd)}
60
68
  </div>
61
69
 
62
70
  {multiModel ? (
@@ -74,8 +82,8 @@ export function ExecutionCostSummary({
74
82
  </div>
75
83
 
76
84
  <div className="pl-2 text-xs tabular-nums text-muted-foreground">
77
- prompt {formatTokenCount(usage.promptTokens)} · completion{" "}
78
- {formatTokenCount(usage.completionTokens)}
85
+ prompt {formatTokenCount(usage.inputTokens)} · completion{" "}
86
+ {formatTokenCount(usage.outputTokens)}
79
87
  </div>
80
88
 
81
89
  {(usage.cacheReadTokens > 0 || usage.cacheCreationTokens > 0) && (
@@ -84,25 +92,14 @@ export function ExecutionCostSummary({
84
92
  creationTokens={usage.cacheCreationTokens}
85
93
  />
86
94
  )}
87
-
88
- {hasSubAgentUsage && (
89
- <div className="text-xs text-muted-foreground">
90
- Includes {subAgentUsageCount} sub-agent
91
- {subAgentUsageCount > 1 ? "s" : ""}
92
- </div>
93
- )}
94
95
  </div>
95
96
  );
96
97
  }
97
98
 
98
- // ---------------------------------------------------------------------------
99
- // Internal sub-components
100
- // ---------------------------------------------------------------------------
101
-
102
99
  function ModelBreakdown({
103
100
  models,
104
101
  }: {
105
- readonly models: readonly ModelUsage[];
102
+ readonly models: readonly ModelCostEntry[];
106
103
  }) {
107
104
  return (
108
105
  <div
@@ -145,10 +142,6 @@ function CacheLine({
145
142
  );
146
143
  }
147
144
 
148
- // ---------------------------------------------------------------------------
149
- // Formatting utilities
150
- // ---------------------------------------------------------------------------
151
-
152
145
  /**
153
146
  * Formats a USD cost value for display.
154
147
  * - Zero: "$0.00"
@@ -10,8 +10,8 @@ export { isTerminalPhase } from "./execution-phases";
10
10
  export { useExecutionStream } from "./useExecutionStream";
11
11
  export type { UseExecutionStreamReturn } from "./useExecutionStream";
12
12
 
13
- export { useExecutionUsage, aggregateUsage } from "./useExecutionUsage";
14
- export type { UseExecutionUsageReturn } from "./useExecutionUsage";
13
+ export { UsageWidget, formatCost, formatTokenCount } from "./UsageWidget";
14
+ export type { UsageWidgetProps } from "./UsageWidget";
15
15
 
16
16
  export { useExecutionArtifacts } from "./useExecutionArtifacts";
17
17
  export type { UseExecutionArtifactsReturn } from "./useExecutionArtifacts";
@@ -89,8 +89,14 @@ export type { FollowUpInputProps } from "./FollowUpInput";
89
89
  export { ExecutionProgress } from "./ExecutionProgress";
90
90
  export type { ExecutionProgressProps } from "./ExecutionProgress";
91
91
 
92
- export { ExecutionCostSummary, formatCost, formatTokenCount } from "./ExecutionCostSummary";
93
- export type { ExecutionCostSummaryProps } from "./ExecutionCostSummary";
92
+ export {
93
+ TodoList,
94
+ TodoInProgressIcon,
95
+ findActiveTodo,
96
+ todoCompletionSummary,
97
+ } from "./TodoList";
98
+ export type { TodoListProps } from "./TodoList";
99
+
94
100
 
95
101
  export { useSubmitApproval } from "./useSubmitApproval";
96
102
  export type { UseSubmitApprovalReturn } from "./useSubmitApproval";
@@ -17,7 +17,7 @@ export interface UseExecutionArtifactsReturn {
17
17
  * Pure derivation hook that extracts artifact metadata from an
18
18
  * {@link AgentExecution} snapshot.
19
19
  *
20
- * Follows the same pattern as {@link useExecutionUsage}: a `useMemo`-based
20
+ * Follows the same pattern as {@link useSessionUsage}: a `useMemo`-based
21
21
  * derivation with no side effects and no data fetching. The execution
22
22
  * object (typically from {@link useExecutionStream}) is the single input.
23
23
  *
package/src/index.ts CHANGED
@@ -54,6 +54,7 @@ export {
54
54
  useSessionConversation,
55
55
  useSessionArtifacts,
56
56
  useSessionWriteBacks,
57
+ useSessionUsage,
57
58
  useAgentRefFromSession,
58
59
  groupSessionsByTime,
59
60
  PENDING_SUBJECT,
@@ -74,6 +75,8 @@ export type {
74
75
  UseSessionArtifactsReturn,
75
76
  SessionWriteBackEntry,
76
77
  UseSessionWriteBacksReturn,
78
+ ModelCostEntry,
79
+ UseSessionUsageReturn,
77
80
  UseAgentRefFromSessionReturn,
78
81
  SessionGroup,
79
82
  } from "./session";
@@ -83,13 +86,17 @@ export {
83
86
  isTerminalPhase,
84
87
  useCreateAgentExecution,
85
88
  useExecutionStream,
86
- useExecutionUsage,
87
- aggregateUsage,
88
89
  useSubmitApproval,
89
90
  ExecutionPhaseBadge,
90
91
  SetupProgress,
91
92
  ExecutionProgress,
92
- ExecutionCostSummary,
93
+ TodoList,
94
+ TodoInProgressIcon,
95
+ findActiveTodo,
96
+ todoCompletionSummary,
97
+ UsageWidget,
98
+ formatCost,
99
+ formatTokenCount,
93
100
  ToolCallGroup,
94
101
  ToolCallDetail,
95
102
  McpToolDetail,
@@ -133,12 +140,12 @@ export type {
133
140
  CreateAgentExecutionResult,
134
141
  UseCreateAgentExecutionReturn,
135
142
  UseExecutionStreamReturn,
136
- UseExecutionUsageReturn,
137
143
  UseSubmitApprovalReturn,
138
144
  ExecutionPhaseBadgeProps,
139
145
  SetupProgressProps,
140
146
  ExecutionProgressProps,
141
- ExecutionCostSummaryProps,
147
+ TodoListProps,
148
+ UsageWidgetProps,
142
149
  ToolCallGroupProps,
143
150
  ToolCallDetailProps,
144
151
  McpToolDetailProps,
@@ -38,6 +38,12 @@ export type {
38
38
  UseSessionWriteBacksReturn,
39
39
  } from "./useSessionWriteBacks";
40
40
 
41
+ export { useSessionUsage } from "./useSessionUsage";
42
+ export type {
43
+ ModelCostEntry,
44
+ UseSessionUsageReturn,
45
+ } from "./useSessionUsage";
46
+
41
47
  export { useAgentRefFromSession } from "./useAgentRefFromSession";
42
48
  export type { UseAgentRefFromSessionReturn } from "./useAgentRefFromSession";
43
49
 
@@ -24,6 +24,14 @@ import { useSession } from "./useSession";
24
24
  import { useSessionExecutions } from "./useSessionExecutions";
25
25
  import { useUpdateSession } from "./useUpdateSession";
26
26
 
27
+ /**
28
+ * After an approval is submitted, the card is optimistically hidden for
29
+ * this many milliseconds. If the server still lists the same tool call
30
+ * as pending after the window expires, the card reappears so the user
31
+ * knows the approval was not processed.
32
+ */
33
+ const DISMISS_GRACE_MS = 8_000;
34
+
27
35
  /**
28
36
  * Options for {@link UseSessionConversationReturn.sendFollowUp}.
29
37
  *
@@ -224,9 +232,9 @@ export function useSessionConversation(
224
232
  const [pendingUserMessage, setPendingUserMessage] = useState<string | null>(
225
233
  null,
226
234
  );
227
- const [dismissedApprovalIds, setDismissedApprovalIds] = useState<
228
- ReadonlySet<string>
229
- >(new Set());
235
+ const [dismissTimestamps, setDismissTimestamps] = useState<
236
+ ReadonlyMap<string, number>
237
+ >(new Map());
230
238
 
231
239
  const listActiveId = useMemo(() => {
232
240
  for (let i = executions.length - 1; i >= 0; i--) {
@@ -243,7 +251,7 @@ export function useSessionConversation(
243
251
  const stream = useExecutionStream(activeExecutionId);
244
252
 
245
253
  useEffect(() => {
246
- setDismissedApprovalIds(new Set());
254
+ setDismissTimestamps(new Map());
247
255
  }, [activeExecutionId]);
248
256
 
249
257
  // Clear pendingExecutionId once the execution appears in the fetched list
@@ -357,10 +365,49 @@ export function useSessionConversation(
357
365
  [sessionId, session, org, stigmer, create, updateSession, refetch, refetchSession],
358
366
  );
359
367
 
368
+ // Reconcile dismissed entries against server state on each stream
369
+ // snapshot. Two outcomes:
370
+ // - Server no longer lists the approval -> clean up (confirmed).
371
+ // - Server still lists it past the grace window -> evict so the card
372
+ // reappears, signaling to the user that the approval was not
373
+ // processed.
374
+ useEffect(() => {
375
+ if (dismissTimestamps.size === 0) return;
376
+
377
+ const serverPendingIds = new Set(
378
+ (activeStreamExecution?.status?.pendingApprovals ?? []).map(
379
+ (a) => a.toolCallId,
380
+ ),
381
+ );
382
+ const now = Date.now();
383
+ const toRemove: string[] = [];
384
+
385
+ for (const [tcId, ts] of dismissTimestamps) {
386
+ if (!serverPendingIds.has(tcId)) {
387
+ toRemove.push(tcId);
388
+ } else if (now - ts > DISMISS_GRACE_MS) {
389
+ toRemove.push(tcId);
390
+ }
391
+ }
392
+
393
+ if (toRemove.length > 0) {
394
+ setDismissTimestamps((prev) => {
395
+ const next = new Map(prev);
396
+ for (const id of toRemove) next.delete(id);
397
+ return next;
398
+ });
399
+ }
400
+ }, [activeStreamExecution, dismissTimestamps]);
401
+
402
+ const dismissedApprovalIds = useMemo<ReadonlySet<string>>(
403
+ () => new Set(dismissTimestamps.keys()),
404
+ [dismissTimestamps],
405
+ );
406
+
360
407
  const pendingApprovals = useMemo<readonly PendingApproval[]>(() => {
361
408
  const all = activeStreamExecution?.status?.pendingApprovals ?? [];
362
- return all.filter((a) => !dismissedApprovalIds.has(a.toolCallId));
363
- }, [activeStreamExecution, dismissedApprovalIds]);
409
+ return all.filter((a) => !dismissTimestamps.has(a.toolCallId));
410
+ }, [activeStreamExecution, dismissTimestamps]);
364
411
 
365
412
  const submitApproval = useCallback(
366
413
  async (
@@ -370,7 +417,9 @@ export function useSessionConversation(
370
417
  ): Promise<void> => {
371
418
  if (!activeExecutionId) return;
372
419
  await rawSubmitApproval(activeExecutionId, toolCallId, action, comment);
373
- setDismissedApprovalIds((prev) => new Set(prev).add(toolCallId));
420
+ setDismissTimestamps((prev) =>
421
+ new Map(prev).set(toolCallId, Date.now()),
422
+ );
374
423
  },
375
424
  [activeExecutionId, rawSubmitApproval],
376
425
  );
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
+ import type { LlmCallMetrics } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
7
+
8
+ /**
9
+ * Per-model cost breakdown computed from per-message {@link LlmCallMetrics}.
10
+ */
11
+ export interface ModelCostEntry {
12
+ readonly model: string;
13
+ readonly provider: string;
14
+ readonly estimatedCostUsd: number;
15
+ readonly inputTokens: number;
16
+ readonly outputTokens: number;
17
+ readonly cacheCreationTokens: number;
18
+ readonly cacheReadTokens: number;
19
+ readonly callCount: number;
20
+ }
21
+
22
+ export interface UseSessionUsageReturn {
23
+ /** Total estimated cost across all executions in the session. */
24
+ readonly totalCostUsd: number;
25
+ /** Total tokens (all types) across all executions. */
26
+ readonly totalTokens: number;
27
+ /** Total input tokens (non-cached) across all executions. */
28
+ readonly inputTokens: number;
29
+ /** Total output tokens across all executions. */
30
+ readonly outputTokens: number;
31
+ /** Total cache read tokens across all executions. */
32
+ readonly cacheReadTokens: number;
33
+ /** Total cache creation tokens across all executions. */
34
+ readonly cacheCreationTokens: number;
35
+ /** Total number of LLM calls across all executions. */
36
+ readonly llmCallCount: number;
37
+ /** Per-model breakdown, sorted by cost descending. */
38
+ readonly modelBreakdown: readonly ModelCostEntry[];
39
+ /** Primary model (first model encountered). */
40
+ readonly primaryModel: string;
41
+ /** Primary provider (first provider encountered). */
42
+ readonly primaryProvider: string;
43
+ /** `true` when at least one execution has cost data. */
44
+ readonly hasUsage: boolean;
45
+ }
46
+
47
+ /**
48
+ * Pure derivation hook that aggregates usage data across all executions
49
+ * in a session from per-message {@link LlmCallMetrics}.
50
+ *
51
+ * Follows the same pattern as {@link useSessionArtifacts} and
52
+ * {@link useSessionWriteBacks}: `useMemo`-based derivation, no side
53
+ * effects, no data fetching. Takes the same `executions` array input.
54
+ *
55
+ * Per-message `llm_metrics` on `AgentMessage` (type == MESSAGE_AI) is
56
+ * the single source of truth for cost data. This hook walks all messages
57
+ * (main agent + sub-agents) across all executions and sums the fields.
58
+ *
59
+ * @param executions - All executions for a session, in chronological
60
+ * order. Pass both completed and active-stream executions.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const conv = useSessionConversation(sessionId, org);
65
+ * const allExecutions = [
66
+ * ...conv.completedExecutions,
67
+ * ...(conv.activeStreamExecution ? [conv.activeStreamExecution] : []),
68
+ * ];
69
+ * const { totalCostUsd, totalTokens, hasUsage } = useSessionUsage(allExecutions);
70
+ * ```
71
+ */
72
+ export function useSessionUsage(
73
+ executions: readonly AgentExecution[],
74
+ ): UseSessionUsageReturn {
75
+ return useMemo(() => {
76
+ let totalCostUsd = 0;
77
+ let totalTokens = 0;
78
+ let inputTokens = 0;
79
+ let outputTokens = 0;
80
+ let cacheReadTokens = 0;
81
+ let cacheCreationTokens = 0;
82
+ let llmCallCount = 0;
83
+ let primaryModel = "";
84
+ let primaryProvider = "";
85
+ const modelMap = new Map<string, ModelCostEntry>();
86
+
87
+ const processMessage = (msg: { type: MessageType; llmMetrics?: LlmCallMetrics }) => {
88
+ const m = msg.llmMetrics;
89
+ if (!m) return;
90
+
91
+ totalCostUsd += m.estimatedCostUsd;
92
+ totalTokens += m.totalTokens;
93
+ inputTokens += m.inputTokens;
94
+ outputTokens += m.outputTokens;
95
+ cacheReadTokens += m.cacheReadTokens;
96
+ cacheCreationTokens += m.cacheCreationTokens;
97
+ llmCallCount++;
98
+
99
+ if (!primaryModel && m.model) {
100
+ primaryModel = m.model;
101
+ primaryProvider = m.provider;
102
+ }
103
+
104
+ const key = `${m.model}\0${m.provider}`;
105
+ const existing = modelMap.get(key);
106
+ if (existing) {
107
+ modelMap.set(key, {
108
+ ...existing,
109
+ estimatedCostUsd: existing.estimatedCostUsd + m.estimatedCostUsd,
110
+ inputTokens: existing.inputTokens + m.inputTokens,
111
+ outputTokens: existing.outputTokens + m.outputTokens,
112
+ cacheCreationTokens: existing.cacheCreationTokens + m.cacheCreationTokens,
113
+ cacheReadTokens: existing.cacheReadTokens + m.cacheReadTokens,
114
+ callCount: existing.callCount + 1,
115
+ });
116
+ } else {
117
+ modelMap.set(key, {
118
+ model: m.model,
119
+ provider: m.provider,
120
+ estimatedCostUsd: m.estimatedCostUsd,
121
+ inputTokens: m.inputTokens,
122
+ outputTokens: m.outputTokens,
123
+ cacheCreationTokens: m.cacheCreationTokens,
124
+ cacheReadTokens: m.cacheReadTokens,
125
+ callCount: 1,
126
+ });
127
+ }
128
+ };
129
+
130
+ for (const execution of executions) {
131
+ for (const msg of execution.status?.messages ?? []) {
132
+ processMessage(msg);
133
+ }
134
+ for (const sub of execution.status?.subAgentExecutions ?? []) {
135
+ for (const msg of sub.messages) {
136
+ processMessage(msg);
137
+ }
138
+ }
139
+ }
140
+
141
+ const modelBreakdown = Array.from(modelMap.values()).sort(
142
+ (a, b) => b.estimatedCostUsd - a.estimatedCostUsd,
143
+ );
144
+
145
+ return {
146
+ totalCostUsd,
147
+ totalTokens,
148
+ inputTokens,
149
+ outputTokens,
150
+ cacheReadTokens,
151
+ cacheCreationTokens,
152
+ llmCallCount,
153
+ modelBreakdown,
154
+ primaryModel,
155
+ primaryProvider,
156
+ hasUsage: llmCallCount > 0,
157
+ };
158
+ }, [executions]);
159
+ }