@mastra/react 1.12.0-alpha.11 → 1.12.0-alpha.12

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.
@@ -0,0 +1,306 @@
1
+ import { n as useMastraClient } from "./mastra-client-context-Bx_g84YB.js";
2
+ import { t as useInView } from "./use-in-view-C9sYPGRm.js";
3
+ import { t as useCombineByIds } from "./combine-by-ids-CcaZKGT9.js";
4
+ import { t as getOrCreate } from "./map-CDv4fbu4.js";
5
+ import { useEffect } from "react";
6
+ import { keepPreviousData, useInfiniteQuery, useQueries, useQuery } from "@tanstack/react-query";
7
+ //#region src/hooks/experiments/usage-accumulator.ts
8
+ const INPUT_TOKEN_METRIC = "mastra_model_total_input_tokens";
9
+ const OUTPUT_TOKEN_METRIC = "mastra_model_total_output_tokens";
10
+ const createAccumulator = () => ({
11
+ estimatedCost: 0,
12
+ costUnits: /* @__PURE__ */ new Set(),
13
+ hasUnknownCostUnit: false
14
+ });
15
+ /** The token field a breakdown group feeds, or undefined when the group is not a token metric. */
16
+ function tokenFieldOf(group) {
17
+ const metricName = group.dimensions.name;
18
+ if (metricName === "mastra_model_total_input_tokens") return "inputTokens";
19
+ if (metricName === "mastra_model_total_output_tokens") return "outputTokens";
20
+ }
21
+ function accumulate(accumulator, tokenField, group) {
22
+ accumulator[tokenField] = (accumulator[tokenField] ?? 0) + group.value;
23
+ if (group.estimatedCost == null) return;
24
+ accumulator.estimatedCost += group.estimatedCost;
25
+ if (group.costUnit) accumulator.costUnits.add(group.costUnit.toLowerCase());
26
+ else accumulator.hasUnknownCostUnit = true;
27
+ }
28
+ function toUsage(accumulator) {
29
+ const isPriced = !accumulator.hasUnknownCostUnit && accumulator.costUnits.size === 1;
30
+ return {
31
+ inputTokens: accumulator.inputTokens,
32
+ outputTokens: accumulator.outputTokens,
33
+ estimatedCost: isPriced ? accumulator.estimatedCost : void 0,
34
+ costUnit: isPriced ? [...accumulator.costUnits][0] : void 0
35
+ };
36
+ }
37
+ //#endregion
38
+ //#region src/hooks/experiments/use-experiment-item-usage.ts
39
+ const POLL_INTERVAL_MS$1 = 2e3;
40
+ /** Builds the experiment totals and the per-trace usage in a single pass over the groups. */
41
+ function toExperimentUsage(response) {
42
+ const summary = createAccumulator();
43
+ const accumulators = /* @__PURE__ */ new Map();
44
+ for (const group of response.groups) {
45
+ const tokenField = tokenFieldOf(group);
46
+ if (!tokenField) continue;
47
+ accumulate(summary, tokenField, group);
48
+ const traceId = group.dimensions.traceId;
49
+ if (traceId) accumulate(getOrCreate(accumulators, traceId, createAccumulator), tokenField, group);
50
+ }
51
+ const traces = {};
52
+ for (const [traceId, accumulator] of accumulators) traces[traceId] = toUsage(accumulator);
53
+ return {
54
+ summary: toUsage(summary),
55
+ traces
56
+ };
57
+ }
58
+ /** How many breakdowns to fetch after completion while the exporter may still be flushing metrics. */
59
+ const MAX_EMPTY_ATTEMPTS = 5;
60
+ const isEmptyUsage = (usage) => !usage || Object.keys(usage.traces).length === 0 && usage.summary.inputTokens === void 0 && usage.summary.outputTokens === void 0;
61
+ /**
62
+ * Token and cost usage of several experiments, keyed by experiment id: each entry holds the
63
+ * experiment totals (`summary`) and every item's usage keyed by trace id (`traces`).
64
+ * Sends one metric breakdown request per experiment, filtered by its id rather than by trace ids,
65
+ * so it does not wait for the experiment results. Usage is only fetched once an experiment has
66
+ * finished; while the breakdown is still empty (metrics flushing), it retries every 2 seconds up to
67
+ * 5 attempts. No request is made when the observability store cannot serve metrics.
68
+ */
69
+ const useExperimentsItemUsage = ({ experiments, supportsMetrics }) => {
70
+ const client = useMastraClient();
71
+ const combine = useCombineByIds(experiments.map((experiment) => experiment.id));
72
+ return useQueries({
73
+ queries: experiments.map(({ id, status }) => {
74
+ const finished = status !== "running" && status !== "pending";
75
+ return {
76
+ queryKey: [
77
+ "experiment-item-usage",
78
+ id,
79
+ finished
80
+ ],
81
+ queryFn: async () => toExperimentUsage(await client.getMetricBreakdown({
82
+ name: [INPUT_TOKEN_METRIC, OUTPUT_TOKEN_METRIC],
83
+ aggregation: "sum",
84
+ groupBy: ["traceId", "name"],
85
+ filters: { experimentId: id }
86
+ })),
87
+ enabled: supportsMetrics && finished,
88
+ refetchInterval: (query) => isEmptyUsage(query.state.data) && query.state.dataUpdateCount < MAX_EMPTY_ATTEMPTS ? POLL_INTERVAL_MS$1 : false
89
+ };
90
+ }),
91
+ combine
92
+ });
93
+ };
94
+ //#endregion
95
+ //#region src/hooks/experiments/use-experiment-metrics.ts
96
+ /**
97
+ * Experiment-scoped metrics (tokens, cost, avg agent latency).
98
+ * Filters only by experimentId — no time window, since the experiment already bounds the row set.
99
+ * Polls every 2 seconds while the experiment is running or pending.
100
+ *
101
+ * Does not guard on empty ids; pass `queryOptions: { enabled }` to skip the fetch.
102
+ */
103
+ const useExperimentMetrics = ({ experimentId, experimentStatus, supportsMetrics, queryOptions }) => {
104
+ const client = useMastraClient();
105
+ const isEnabled = Boolean(experimentId) && supportsMetrics;
106
+ const query = useQuery({
107
+ queryKey: ["experiment-metrics", experimentId],
108
+ refetchInterval: experimentStatus === "running" || experimentStatus === "pending" ? 2e3 : false,
109
+ queryFn: async () => {
110
+ if (!experimentId) throw new Error("experimentId is required");
111
+ const filters = { experimentId };
112
+ const [tokens, avgDuration, runs] = await Promise.all([
113
+ client.getMetricAggregate({
114
+ name: ["mastra_model_total_input_tokens", "mastra_model_total_output_tokens"],
115
+ aggregation: "sum",
116
+ filters
117
+ }),
118
+ client.getMetricAggregate({
119
+ name: ["mastra_agent_duration_ms"],
120
+ aggregation: "avg",
121
+ filters
122
+ }),
123
+ client.getMetricAggregate({
124
+ name: ["mastra_agent_duration_ms"],
125
+ aggregation: "count",
126
+ filters
127
+ })
128
+ ]);
129
+ return {
130
+ totalTokens: tokens.value ?? null,
131
+ estimatedCost: tokens.estimatedCost ?? null,
132
+ costUnit: tokens.costUnit ?? null,
133
+ avgAgentDurationMs: avgDuration.value ?? null,
134
+ agentRuns: runs.value ?? null
135
+ };
136
+ },
137
+ ...queryOptions
138
+ });
139
+ return {
140
+ data: query.data,
141
+ isLoading: query.isLoading,
142
+ isEnabled
143
+ };
144
+ };
145
+ //#endregion
146
+ //#region src/hooks/experiments/use-experiment-trace.ts
147
+ /**
148
+ * Does not guard on empty ids; pass `queryOptions: { enabled }` to skip the fetch.
149
+ */
150
+ const useExperimentTrace = ({ traceId, queryOptions }) => {
151
+ const client = useMastraClient();
152
+ return useQuery({
153
+ queryKey: ["experiment-trace-light", traceId],
154
+ queryFn: async () => {
155
+ if (!traceId) throw new Error("Trace ID is required");
156
+ return client.getTraceLight(traceId);
157
+ },
158
+ ...queryOptions
159
+ });
160
+ };
161
+ //#endregion
162
+ //#region src/hooks/experiments/use-experiments-for-dataset-filter.ts
163
+ /** Explicit page size: server defaults are 20 (global) / 10 (per dataset), which is too small for the list. */
164
+ const EXPERIMENTS_PAGE_SIZE = 100;
165
+ function useExperimentsForDatasetFilter({ datasetId, target, queryOptions }) {
166
+ const client = useMastraClient();
167
+ const params = { perPage: 100 };
168
+ if (target?.targetType) params.targetType = target.targetType;
169
+ if (target?.targetId) params.targetId = target.targetId;
170
+ return useQuery({
171
+ queryKey: datasetId ? [
172
+ "dataset-experiments",
173
+ datasetId,
174
+ params
175
+ ] : ["experiments", params],
176
+ queryFn: () => datasetId ? client.listDatasetExperiments(datasetId, params) : client.listExperiments(params),
177
+ ...queryOptions
178
+ });
179
+ }
180
+ //#endregion
181
+ //#region src/hooks/experiments/use-experiments-list-metrics.ts
182
+ const AGENT_DURATION_METRIC = "mastra_agent_duration_ms";
183
+ const EXPERIMENT_IDS_PER_REQUEST = 500;
184
+ const POLL_INTERVAL_MS = 5e3;
185
+ function chunk(ids) {
186
+ const chunks = [];
187
+ for (let index = 0; index < ids.length; index += EXPERIMENT_IDS_PER_REQUEST) chunks.push(ids.slice(index, index + EXPERIMENT_IDS_PER_REQUEST));
188
+ return chunks;
189
+ }
190
+ function settledValues(results) {
191
+ return results.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
192
+ }
193
+ /**
194
+ * Builds each requested experiment's metrics. Groups for other experiments are skipped,
195
+ * in case a store ignores the `experimentIds` filter.
196
+ */
197
+ function toListMetrics(experimentIds, tokenResponses, durationResponses) {
198
+ const requested = new Set(experimentIds);
199
+ const accumulators = /* @__PURE__ */ new Map();
200
+ for (const group of tokenResponses.flatMap((response) => response.groups)) {
201
+ const experimentId = group.dimensions.experimentId;
202
+ const tokenField = tokenFieldOf(group);
203
+ if (!experimentId || !requested.has(experimentId) || !tokenField) continue;
204
+ accumulate(getOrCreate(accumulators, experimentId, createAccumulator), tokenField, group);
205
+ }
206
+ const metrics = /* @__PURE__ */ new Map();
207
+ for (const [experimentId, accumulator] of accumulators) metrics.set(experimentId, toUsage(accumulator));
208
+ for (const group of durationResponses.flatMap((response) => response.groups)) {
209
+ const experimentId = group.dimensions.experimentId;
210
+ if (!experimentId || !requested.has(experimentId)) continue;
211
+ getOrCreate(metrics, experimentId, () => ({})).avgDurationMs = group.value;
212
+ }
213
+ return metrics;
214
+ }
215
+ /**
216
+ * Token usage, estimated cost and average agent duration of many experiments, keyed by experiment id.
217
+ * Sends two metric breakdowns per 500 experiments (tokens grouped by experiment and metric name,
218
+ * durations grouped by experiment) instead of one request per experiment.
219
+ * Experiments without any metric are absent from the map. Polls while an experiment is running
220
+ * or pending. No request is made when the observability store cannot serve metrics.
221
+ */
222
+ const useExperimentsListMetrics = ({ experiments, supportsMetrics }) => {
223
+ const client = useMastraClient();
224
+ const experimentIds = [...new Set(experiments.map((experiment) => experiment.id))].toSorted();
225
+ const hasActiveExperiment = experiments.some(({ status }) => status === "running" || status === "pending");
226
+ return useQuery({
227
+ queryKey: ["experiments-list-metrics", experimentIds],
228
+ enabled: supportsMetrics && experimentIds.length > 0,
229
+ queryFn: async () => {
230
+ const batches = chunk(experimentIds);
231
+ const [tokenResults, durationResults] = await Promise.all([Promise.allSettled(batches.map((batch) => client.getMetricBreakdown({
232
+ name: [INPUT_TOKEN_METRIC, OUTPUT_TOKEN_METRIC],
233
+ aggregation: "sum",
234
+ groupBy: ["experimentId", "name"],
235
+ filters: { experimentIds: batch },
236
+ limit: batch.length * 2
237
+ }))), Promise.allSettled(batches.map((batch) => client.getMetricBreakdown({
238
+ name: [AGENT_DURATION_METRIC],
239
+ aggregation: "avg",
240
+ groupBy: ["experimentId"],
241
+ filters: { experimentIds: batch },
242
+ limit: batch.length
243
+ })))]);
244
+ return toListMetrics(experimentIds, settledValues(tokenResults), settledValues(durationResults));
245
+ },
246
+ placeholderData: keepPreviousData,
247
+ refetchInterval: hasActiveExperiment ? POLL_INTERVAL_MS : false
248
+ });
249
+ };
250
+ //#endregion
251
+ //#region src/hooks/experiments/use-infinite-experiments.ts
252
+ const EXPERIMENTS_PER_PAGE = 100;
253
+ function useInfiniteExperiments({ datasetId, target, orderBy, queryOptions }) {
254
+ const client = useMastraClient();
255
+ const { inView: isEndOfListInView, setRef: setEndOfListElement } = useInView();
256
+ const targetType = target?.targetType || void 0;
257
+ const targetId = target?.targetId || void 0;
258
+ const query = useInfiniteQuery({
259
+ queryKey: datasetId ? [
260
+ "dataset-experiments",
261
+ datasetId,
262
+ "infinite",
263
+ {
264
+ targetType,
265
+ targetId,
266
+ orderBy
267
+ }
268
+ ] : [
269
+ "experiments",
270
+ "infinite",
271
+ {
272
+ targetType,
273
+ targetId,
274
+ orderBy
275
+ }
276
+ ],
277
+ queryFn: ({ pageParam }) => {
278
+ const params = {
279
+ page: pageParam,
280
+ perPage: 100
281
+ };
282
+ if (targetType) params.targetType = targetType;
283
+ if (targetId) params.targetId = targetId;
284
+ if (orderBy) params.orderBy = orderBy;
285
+ return datasetId ? client.listDatasetExperiments(datasetId, params) : client.listExperiments(params);
286
+ },
287
+ initialPageParam: 0,
288
+ getNextPageParam: (lastPage, _, lastPageParam) => {
289
+ if (!lastPage?.experiments?.length || !lastPage.pagination?.hasMore) return;
290
+ return lastPageParam + 1;
291
+ },
292
+ select: (data) => data.pages.flatMap((page) => page?.experiments ?? []),
293
+ ...queryOptions
294
+ });
295
+ useEffect(() => {
296
+ if (isEndOfListInView && query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage();
297
+ }, [isEndOfListInView, query]);
298
+ return {
299
+ ...query,
300
+ setEndOfListElement
301
+ };
302
+ }
303
+ //#endregion
304
+ export { useExperimentsForDatasetFilter as a, useExperimentsItemUsage as c, EXPERIMENTS_PAGE_SIZE as i, useInfiniteExperiments as n, useExperimentTrace as o, useExperimentsListMetrics as r, useExperimentMetrics as s, EXPERIMENTS_PER_PAGE as t };
305
+
306
+ //# sourceMappingURL=experiments-B7vRMK5T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiments-B7vRMK5T.js","names":["POLL_INTERVAL_MS"],"sources":["../src/hooks/experiments/usage-accumulator.ts","../src/hooks/experiments/use-experiment-item-usage.ts","../src/hooks/experiments/use-experiment-metrics.ts","../src/hooks/experiments/use-experiment-trace.ts","../src/hooks/experiments/use-experiments-for-dataset-filter.ts","../src/hooks/experiments/use-experiments-list-metrics.ts","../src/hooks/experiments/use-infinite-experiments.ts"],"sourcesContent":["import type { GetMetricBreakdownResponse } from '@mastra/core/storage';\n\nexport const INPUT_TOKEN_METRIC = 'mastra_model_total_input_tokens';\nexport const OUTPUT_TOKEN_METRIC = 'mastra_model_total_output_tokens';\n\nexport interface ExperimentItemUsage {\n inputTokens?: number;\n outputTokens?: number;\n /** Undefined when no group is priced or when groups use different cost units. */\n estimatedCost?: number;\n costUnit?: string;\n}\n\nexport type UsageAccumulator = {\n inputTokens?: number;\n outputTokens?: number;\n estimatedCost: number;\n costUnits: Set<string>;\n hasUnknownCostUnit: boolean;\n};\n\nexport const createAccumulator = (): UsageAccumulator => ({\n estimatedCost: 0,\n costUnits: new Set(),\n hasUnknownCostUnit: false,\n});\n\ntype BreakdownGroup = GetMetricBreakdownResponse['groups'][number];\n\n/** The token field a breakdown group feeds, or undefined when the group is not a token metric. */\nexport function tokenFieldOf(group: BreakdownGroup): 'inputTokens' | 'outputTokens' | undefined {\n const metricName = group.dimensions.name;\n if (metricName === INPUT_TOKEN_METRIC) return 'inputTokens';\n if (metricName === OUTPUT_TOKEN_METRIC) return 'outputTokens';\n return undefined;\n}\n\nexport function accumulate(\n accumulator: UsageAccumulator,\n tokenField: 'inputTokens' | 'outputTokens',\n group: BreakdownGroup,\n) {\n accumulator[tokenField] = (accumulator[tokenField] ?? 0) + group.value;\n\n if (group.estimatedCost == null) return;\n accumulator.estimatedCost += group.estimatedCost;\n if (group.costUnit) {\n accumulator.costUnits.add(group.costUnit.toLowerCase());\n } else {\n accumulator.hasUnknownCostUnit = true;\n }\n}\n\nexport function toUsage(accumulator: UsageAccumulator): ExperimentItemUsage {\n // A single known unit implies at least one priced group.\n const isPriced = !accumulator.hasUnknownCostUnit && accumulator.costUnits.size === 1;\n return {\n inputTokens: accumulator.inputTokens,\n outputTokens: accumulator.outputTokens,\n estimatedCost: isPriced ? accumulator.estimatedCost : undefined,\n costUnit: isPriced ? [...accumulator.costUnits][0] : undefined,\n };\n}\n","import type { GetMetricBreakdownResponse } from '@mastra/core/storage';\nimport { useQueries } from '@tanstack/react-query';\nimport type { Query } from '@tanstack/react-query';\nimport { useMastraClient } from '../../mastra-client-context';\nimport type { ExperimentRef } from '../datasets/use-dataset-experiments';\nimport { useCombineByIds } from '../shared/combine-by-ids';\nimport type { ExperimentQueriesResult } from '../shared/combine-by-ids';\nimport { getOrCreate } from '../shared/map';\nimport {\n INPUT_TOKEN_METRIC,\n OUTPUT_TOKEN_METRIC,\n accumulate,\n createAccumulator,\n tokenFieldOf,\n toUsage,\n} from './usage-accumulator';\nimport type { ExperimentItemUsage, UsageAccumulator } from './usage-accumulator';\n\nexport type { ExperimentItemUsage } from './usage-accumulator';\n\nconst POLL_INTERVAL_MS = 2000;\n\n/** Usage of one experiment: its totals, and each item's usage keyed by the trace id of its run. */\nexport interface ExperimentUsage {\n /** Totals across every item. The cost sums priced groups only. */\n summary: ExperimentItemUsage;\n traces: Record<string, ExperimentItemUsage>;\n}\n\n/** Builds the experiment totals and the per-trace usage in a single pass over the groups. */\nfunction toExperimentUsage(response: GetMetricBreakdownResponse): ExperimentUsage {\n const summary = createAccumulator();\n const accumulators = new Map<string, UsageAccumulator>();\n\n for (const group of response.groups) {\n const tokenField = tokenFieldOf(group);\n if (!tokenField) continue;\n\n accumulate(summary, tokenField, group);\n\n const traceId = group.dimensions.traceId;\n if (traceId) accumulate(getOrCreate(accumulators, traceId, createAccumulator), tokenField, group);\n }\n\n const traces: Record<string, ExperimentItemUsage> = {};\n for (const [traceId, accumulator] of accumulators) {\n traces[traceId] = toUsage(accumulator);\n }\n return { summary: toUsage(summary), traces };\n}\n\n/** How many breakdowns to fetch after completion while the exporter may still be flushing metrics. */\nconst MAX_EMPTY_ATTEMPTS = 5;\n\nconst isEmptyUsage = (usage: ExperimentUsage | undefined) =>\n !usage ||\n (Object.keys(usage.traces).length === 0 &&\n usage.summary.inputTokens === undefined &&\n usage.summary.outputTokens === undefined);\n\n/**\n * Token and cost usage of several experiments, keyed by experiment id: each entry holds the\n * experiment totals (`summary`) and every item's usage keyed by trace id (`traces`).\n * Sends one metric breakdown request per experiment, filtered by its id rather than by trace ids,\n * so it does not wait for the experiment results. Usage is only fetched once an experiment has\n * finished; while the breakdown is still empty (metrics flushing), it retries every 2 seconds up to\n * 5 attempts. No request is made when the observability store cannot serve metrics.\n */\nexport const useExperimentsItemUsage = ({\n experiments,\n supportsMetrics,\n}: {\n experiments: ExperimentRef[];\n supportsMetrics: boolean;\n}): ExperimentQueriesResult<ExperimentUsage> => {\n const client = useMastraClient();\n const combine = useCombineByIds<ExperimentUsage>(experiments.map(experiment => experiment.id));\n\n return useQueries({\n queries: experiments.map(({ id, status }) => {\n const finished = status !== 'running' && status !== 'pending';\n return {\n queryKey: ['experiment-item-usage', id, finished],\n queryFn: async () =>\n toExperimentUsage(\n await client.getMetricBreakdown({\n name: [INPUT_TOKEN_METRIC, OUTPUT_TOKEN_METRIC],\n aggregation: 'sum',\n groupBy: ['traceId', 'name'],\n filters: { experimentId: id },\n }),\n ),\n enabled: supportsMetrics && finished,\n refetchInterval: (query: Query<ExperimentUsage>) =>\n isEmptyUsage(query.state.data) && query.state.dataUpdateCount < MAX_EMPTY_ATTEMPTS ? POLL_INTERVAL_MS : false,\n };\n }),\n combine,\n });\n};\n","import type { DatasetExperiment } from '@mastra/client-js';\nimport { useQuery } from '@tanstack/react-query';\nimport { useMastraClient } from '../../mastra-client-context';\nimport type { MastraQueryOptions } from '../shared/query-options';\n\nexport interface ExperimentMetrics {\n totalTokens: number | null;\n estimatedCost: number | null;\n costUnit: string | null;\n avgAgentDurationMs: number | null;\n agentRuns: number | null;\n}\n\ninterface UseExperimentMetricsArgs {\n experimentId: string | undefined;\n experimentStatus: DatasetExperiment['status'] | undefined;\n /** Whether the observability store can serve metrics; no requests are made when false. */\n supportsMetrics: boolean;\n queryOptions?: MastraQueryOptions<ExperimentMetrics>;\n}\n\n/**\n * Experiment-scoped metrics (tokens, cost, avg agent latency).\n * Filters only by experimentId — no time window, since the experiment already bounds the row set.\n * Polls every 2 seconds while the experiment is running or pending.\n *\n * Does not guard on empty ids; pass `queryOptions: { enabled }` to skip the fetch.\n */\nexport const useExperimentMetrics = ({\n experimentId,\n experimentStatus,\n supportsMetrics,\n queryOptions,\n}: UseExperimentMetricsArgs) => {\n const client = useMastraClient();\n const isEnabled = Boolean(experimentId) && supportsMetrics;\n const isActive = experimentStatus === 'running' || experimentStatus === 'pending';\n\n const query = useQuery({\n queryKey: ['experiment-metrics', experimentId],\n refetchInterval: isActive ? 2000 : false,\n queryFn: async (): Promise<ExperimentMetrics> => {\n // `enabled` already guards this; the check exists to narrow the type.\n if (!experimentId) throw new Error('experimentId is required');\n const filters = { experimentId };\n\n const [tokens, avgDuration, runs] = await Promise.all([\n client.getMetricAggregate({\n name: ['mastra_model_total_input_tokens', 'mastra_model_total_output_tokens'],\n aggregation: 'sum',\n filters,\n }),\n client.getMetricAggregate({ name: ['mastra_agent_duration_ms'], aggregation: 'avg', filters }),\n client.getMetricAggregate({ name: ['mastra_agent_duration_ms'], aggregation: 'count', filters }),\n ]);\n\n return {\n totalTokens: tokens.value ?? null,\n estimatedCost: tokens.estimatedCost ?? null,\n costUnit: tokens.costUnit ?? null,\n avgAgentDurationMs: avgDuration.value ?? null,\n agentRuns: runs.value ?? null,\n };\n },\n ...queryOptions,\n });\n\n return { data: query.data, isLoading: query.isLoading, isEnabled };\n};\n","import type { MastraClient } from '@mastra/client-js';\nimport type { UseQueryResult } from '@tanstack/react-query';\nimport { useQuery } from '@tanstack/react-query';\nimport { useMastraClient } from '../../mastra-client-context';\nimport type { MastraQueryOptions } from '../shared/query-options';\n\ntype TraceLightResponse = Awaited<ReturnType<MastraClient['getTraceLight']>>;\n\n/**\n * Does not guard on empty ids; pass `queryOptions: { enabled }` to skip the fetch.\n */\nexport const useExperimentTrace = <TData = TraceLightResponse>({\n traceId,\n queryOptions,\n}: {\n traceId: string | null | undefined;\n queryOptions?: MastraQueryOptions<TraceLightResponse, TData>;\n}): UseQueryResult<TData, Error> => {\n const client = useMastraClient();\n\n return useQuery({\n queryKey: ['experiment-trace-light', traceId],\n queryFn: async () => {\n if (!traceId) {\n throw new Error('Trace ID is required');\n }\n return client.getTraceLight(traceId);\n },\n ...queryOptions,\n });\n};\n","import type { ExperimentTargetType, ListExperimentsParams, MastraClient } from '@mastra/client-js';\nimport type { UseQueryResult } from '@tanstack/react-query';\nimport { useQuery } from '@tanstack/react-query';\nimport { useMastraClient } from '../../mastra-client-context';\nimport type { MastraQueryOptions } from '../shared/query-options';\n\n/** Explicit page size: server defaults are 20 (global) / 10 (per dataset), which is too small for the list. */\nexport const EXPERIMENTS_PAGE_SIZE = 100;\n\nexport interface ExperimentTargetFilter {\n targetType?: ExperimentTargetType | '';\n targetId?: string;\n}\n\n/**\n * Experiments for the list page: the global list, or the dataset-scoped list when a dataset filter is active.\n * A dataset's runs may not be in the first page of the global list, so filtering client-side is not enough.\n * The optional target filter is applied server-side for the same reason.\n */\ntype ListExperimentsResponse = Awaited<ReturnType<MastraClient['listExperiments']>>;\n\nexport function useExperimentsForDatasetFilter<TData = ListExperimentsResponse>({\n datasetId,\n target,\n queryOptions,\n}: {\n datasetId: string | undefined;\n target?: ExperimentTargetFilter;\n queryOptions?: MastraQueryOptions<ListExperimentsResponse, TData>;\n}): UseQueryResult<TData, Error> {\n const client = useMastraClient();\n const params: ListExperimentsParams = { perPage: EXPERIMENTS_PAGE_SIZE };\n if (target?.targetType) params.targetType = target.targetType;\n if (target?.targetId) params.targetId = target.targetId;\n\n return useQuery({\n // Prefixes match the keys invalidated by dataset/experiment mutations.\n queryKey: datasetId ? ['dataset-experiments', datasetId, params] : ['experiments', params],\n queryFn: () => (datasetId ? client.listDatasetExperiments(datasetId, params) : client.listExperiments(params)),\n ...queryOptions,\n });\n}\n","import type { GetMetricBreakdownResponse } from '@mastra/core/storage';\nimport { keepPreviousData, useQuery } from '@tanstack/react-query';\nimport type { UseQueryResult } from '@tanstack/react-query';\nimport { useMastraClient } from '../../mastra-client-context';\nimport type { ExperimentRef } from '../datasets/use-dataset-experiments';\nimport { getOrCreate } from '../shared/map';\nimport {\n INPUT_TOKEN_METRIC,\n OUTPUT_TOKEN_METRIC,\n accumulate,\n createAccumulator,\n tokenFieldOf,\n toUsage,\n} from './usage-accumulator';\nimport type { ExperimentItemUsage, UsageAccumulator } from './usage-accumulator';\n\nconst AGENT_DURATION_METRIC = 'mastra_agent_duration_ms';\n// Each experiment can return two token groups, while the breakdown endpoint allows at most 1,000.\nconst EXPERIMENT_IDS_PER_REQUEST = 500;\nconst POLL_INTERVAL_MS = 5000;\n\n/** Totals of one experiment: token usage, its estimated cost, and the average agent run duration. */\nexport interface ExperimentListMetrics extends ExperimentItemUsage {\n avgDurationMs?: number;\n}\n\nfunction chunk(ids: readonly string[]): string[][] {\n const chunks: string[][] = [];\n for (let index = 0; index < ids.length; index += EXPERIMENT_IDS_PER_REQUEST) {\n chunks.push(ids.slice(index, index + EXPERIMENT_IDS_PER_REQUEST));\n }\n return chunks;\n}\n\nfunction settledValues<T>(results: PromiseSettledResult<T>[]): T[] {\n return results.flatMap(result => (result.status === 'fulfilled' ? [result.value] : []));\n}\n\n/**\n * Builds each requested experiment's metrics. Groups for other experiments are skipped,\n * in case a store ignores the `experimentIds` filter.\n */\nfunction toListMetrics(\n experimentIds: readonly string[],\n tokenResponses: GetMetricBreakdownResponse[],\n durationResponses: GetMetricBreakdownResponse[],\n): Map<string, ExperimentListMetrics> {\n const requested = new Set(experimentIds);\n const accumulators = new Map<string, UsageAccumulator>();\n for (const group of tokenResponses.flatMap(response => response.groups)) {\n const experimentId = group.dimensions.experimentId;\n const tokenField = tokenFieldOf(group);\n if (!experimentId || !requested.has(experimentId) || !tokenField) continue;\n accumulate(getOrCreate(accumulators, experimentId, createAccumulator), tokenField, group);\n }\n\n const metrics = new Map<string, ExperimentListMetrics>();\n for (const [experimentId, accumulator] of accumulators) {\n metrics.set(experimentId, toUsage(accumulator));\n }\n for (const group of durationResponses.flatMap(response => response.groups)) {\n const experimentId = group.dimensions.experimentId;\n if (!experimentId || !requested.has(experimentId)) continue;\n getOrCreate(metrics, experimentId, (): ExperimentListMetrics => ({})).avgDurationMs = group.value;\n }\n return metrics;\n}\n\n/**\n * Token usage, estimated cost and average agent duration of many experiments, keyed by experiment id.\n * Sends two metric breakdowns per 500 experiments (tokens grouped by experiment and metric name,\n * durations grouped by experiment) instead of one request per experiment.\n * Experiments without any metric are absent from the map. Polls while an experiment is running\n * or pending. No request is made when the observability store cannot serve metrics.\n */\nexport const useExperimentsListMetrics = ({\n experiments,\n supportsMetrics,\n}: {\n experiments: ExperimentRef[];\n supportsMetrics: boolean;\n}): UseQueryResult<Map<string, ExperimentListMetrics>, Error> => {\n const client = useMastraClient();\n const experimentIds = [...new Set(experiments.map(experiment => experiment.id))].toSorted();\n const hasActiveExperiment = experiments.some(({ status }) => status === 'running' || status === 'pending');\n\n return useQuery({\n queryKey: ['experiments-list-metrics', experimentIds],\n enabled: supportsMetrics && experimentIds.length > 0,\n queryFn: async () => {\n const batches = chunk(experimentIds);\n const [tokenResults, durationResults] = await Promise.all([\n Promise.allSettled(\n batches.map(batch =>\n client.getMetricBreakdown({\n name: [INPUT_TOKEN_METRIC, OUTPUT_TOKEN_METRIC],\n aggregation: 'sum',\n groupBy: ['experimentId', 'name'],\n filters: { experimentIds: batch },\n limit: batch.length * 2,\n }),\n ),\n ),\n Promise.allSettled(\n batches.map(batch =>\n client.getMetricBreakdown({\n name: [AGENT_DURATION_METRIC],\n aggregation: 'avg',\n groupBy: ['experimentId'],\n filters: { experimentIds: batch },\n limit: batch.length,\n }),\n ),\n ),\n ]);\n return toListMetrics(experimentIds, settledValues(tokenResults), settledValues(durationResults));\n },\n // Loading another page changes the ids; keep the rows already shown filled meanwhile.\n placeholderData: keepPreviousData,\n refetchInterval: hasActiveExperiment ? POLL_INTERVAL_MS : false,\n });\n};\n","import type { DatasetExperiment, ListExperimentsParams, MastraClient } from '@mastra/client-js';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport type { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query';\nimport { useEffect } from 'react';\nimport { useMastraClient } from '../../mastra-client-context';\nimport type { MastraInfiniteQueryOptions } from '../shared/query-options';\nimport { useInView } from '../shared/use-in-view';\nimport type { ExperimentTargetFilter } from './use-experiments-for-dataset-filter';\n\nexport const EXPERIMENTS_PER_PAGE = 100;\n\n/**\n * Infinite-scroll experiments for the list page: the global list, or the dataset-scoped list when a\n * dataset filter is active (a dataset's runs may not be in the first pages of the global list). The\n * optional target filter is applied server-side for the same reason.\n */\nexport type ExperimentsOrderBy = NonNullable<ListExperimentsParams['orderBy']>;\n\ntype ListExperimentsResponse = Awaited<ReturnType<MastraClient['listExperiments']>>;\n\nexport function useInfiniteExperiments<TData = DatasetExperiment[]>({\n datasetId,\n target,\n orderBy,\n queryOptions,\n}: {\n datasetId: string | undefined;\n target?: ExperimentTargetFilter;\n orderBy?: ExperimentsOrderBy;\n queryOptions?: MastraInfiniteQueryOptions<ListExperimentsResponse, TData, QueryKey, number>;\n}): UseInfiniteQueryResult<TData, Error> & {\n setEndOfListElement: (element: HTMLDivElement | null) => void;\n} {\n const client = useMastraClient();\n const { inView: isEndOfListInView, setRef: setEndOfListElement } = useInView();\n const targetType = target?.targetType || undefined;\n const targetId = target?.targetId || undefined;\n\n const query = useInfiniteQuery({\n // Prefixes match the keys invalidated by dataset/experiment mutations.\n queryKey: datasetId\n ? ['dataset-experiments', datasetId, 'infinite', { targetType, targetId, orderBy }]\n : ['experiments', 'infinite', { targetType, targetId, orderBy }],\n queryFn: ({ pageParam }) => {\n const params: ListExperimentsParams = { page: pageParam, perPage: EXPERIMENTS_PER_PAGE };\n if (targetType) params.targetType = targetType;\n if (targetId) params.targetId = targetId;\n if (orderBy) params.orderBy = orderBy;\n return datasetId ? client.listDatasetExperiments(datasetId, params) : client.listExperiments(params);\n },\n initialPageParam: 0,\n getNextPageParam: (lastPage, _, lastPageParam) => {\n if (!lastPage?.experiments?.length || !lastPage.pagination?.hasMore) {\n return undefined;\n }\n return lastPageParam + 1;\n },\n select: (data: InfiniteData<ListExperimentsResponse, number>) =>\n data.pages.flatMap(page => page?.experiments ?? []) as TData,\n ...queryOptions,\n });\n\n useEffect(() => {\n if (isEndOfListInView && query.hasNextPage && !query.isFetchingNextPage) {\n void query.fetchNextPage();\n }\n }, [isEndOfListInView, query]);\n\n return { ...query, setEndOfListElement };\n}\n"],"mappings":";;;;;;;AAEA,MAAa,qBAAqB;AAClC,MAAa,sBAAsB;AAkBnC,MAAa,2BAA6C;CACxD,eAAe;CACf,2BAAW,IAAI,IAAI;CACnB,oBAAoB;AACtB;;AAKA,SAAgB,aAAa,OAAmE;CAC9F,MAAM,aAAa,MAAM,WAAW;CACpC,IAAI,eAAA,mCAAmC,OAAO;CAC9C,IAAI,eAAA,oCAAoC,OAAO;AAEjD;AAEA,SAAgB,WACd,aACA,YACA,OACA;CACA,YAAY,eAAe,YAAY,eAAe,KAAK,MAAM;CAEjE,IAAI,MAAM,iBAAiB,MAAM;CACjC,YAAY,iBAAiB,MAAM;CACnC,IAAI,MAAM,UACR,YAAY,UAAU,IAAI,MAAM,SAAS,YAAY,CAAC;MAEtD,YAAY,qBAAqB;AAErC;AAEA,SAAgB,QAAQ,aAAoD;CAE1E,MAAM,WAAW,CAAC,YAAY,sBAAsB,YAAY,UAAU,SAAS;CACnF,OAAO;EACL,aAAa,YAAY;EACzB,cAAc,YAAY;EAC1B,eAAe,WAAW,YAAY,gBAAgB,KAAA;EACtD,UAAU,WAAW,CAAC,GAAG,YAAY,SAAS,CAAC,CAAC,KAAK,KAAA;CACvD;AACF;;;AC1CA,MAAMA,qBAAmB;;AAUzB,SAAS,kBAAkB,UAAuD;CAChF,MAAM,UAAU,kBAAkB;CAClC,MAAM,+BAAe,IAAI,IAA8B;CAEvD,KAAK,MAAM,SAAS,SAAS,QAAQ;EACnC,MAAM,aAAa,aAAa,KAAK;EACrC,IAAI,CAAC,YAAY;EAEjB,WAAW,SAAS,YAAY,KAAK;EAErC,MAAM,UAAU,MAAM,WAAW;EACjC,IAAI,SAAS,WAAW,YAAY,cAAc,SAAS,iBAAiB,GAAG,YAAY,KAAK;CAClG;CAEA,MAAM,SAA8C,CAAC;CACrD,KAAK,MAAM,CAAC,SAAS,gBAAgB,cACnC,OAAO,WAAW,QAAQ,WAAW;CAEvC,OAAO;EAAE,SAAS,QAAQ,OAAO;EAAG;CAAO;AAC7C;;AAGA,MAAM,qBAAqB;AAE3B,MAAM,gBAAgB,UACpB,CAAC,SACA,OAAO,KAAK,MAAM,MAAM,CAAC,CAAC,WAAW,KACpC,MAAM,QAAQ,gBAAgB,KAAA,KAC9B,MAAM,QAAQ,iBAAiB,KAAA;;;;;;;;;AAUnC,MAAa,2BAA2B,EACtC,aACA,sBAI8C;CAC9C,MAAM,SAAS,gBAAgB;CAC/B,MAAM,UAAU,gBAAiC,YAAY,KAAI,eAAc,WAAW,EAAE,CAAC;CAE7F,OAAO,WAAW;EAChB,SAAS,YAAY,KAAK,EAAE,IAAI,aAAa;GAC3C,MAAM,WAAW,WAAW,aAAa,WAAW;GACpD,OAAO;IACL,UAAU;KAAC;KAAyB;KAAI;IAAQ;IAChD,SAAS,YACP,kBACE,MAAM,OAAO,mBAAmB;KAC9B,MAAM,CAAC,oBAAoB,mBAAmB;KAC9C,aAAa;KACb,SAAS,CAAC,WAAW,MAAM;KAC3B,SAAS,EAAE,cAAc,GAAG;IAC9B,CAAC,CACH;IACF,SAAS,mBAAmB;IAC5B,kBAAkB,UAChB,aAAa,MAAM,MAAM,IAAI,KAAK,MAAM,MAAM,kBAAkB,qBAAqBA,qBAAmB;GAC5G;EACF,CAAC;EACD;CACF,CAAC;AACH;;;;;;;;;;ACvEA,MAAa,wBAAwB,EACnC,cACA,kBACA,iBACA,mBAC8B;CAC9B,MAAM,SAAS,gBAAgB;CAC/B,MAAM,YAAY,QAAQ,YAAY,KAAK;CAG3C,MAAM,QAAQ,SAAS;EACrB,UAAU,CAAC,sBAAsB,YAAY;EAC7C,iBAJe,qBAAqB,aAAa,qBAAqB,YAI1C,MAAO;EACnC,SAAS,YAAwC;GAE/C,IAAI,CAAC,cAAc,MAAM,IAAI,MAAM,0BAA0B;GAC7D,MAAM,UAAU,EAAE,aAAa;GAE/B,MAAM,CAAC,QAAQ,aAAa,QAAQ,MAAM,QAAQ,IAAI;IACpD,OAAO,mBAAmB;KACxB,MAAM,CAAC,mCAAmC,kCAAkC;KAC5E,aAAa;KACb;IACF,CAAC;IACD,OAAO,mBAAmB;KAAE,MAAM,CAAC,0BAA0B;KAAG,aAAa;KAAO;IAAQ,CAAC;IAC7F,OAAO,mBAAmB;KAAE,MAAM,CAAC,0BAA0B;KAAG,aAAa;KAAS;IAAQ,CAAC;GACjG,CAAC;GAED,OAAO;IACL,aAAa,OAAO,SAAS;IAC7B,eAAe,OAAO,iBAAiB;IACvC,UAAU,OAAO,YAAY;IAC7B,oBAAoB,YAAY,SAAS;IACzC,WAAW,KAAK,SAAS;GAC3B;EACF;EACA,GAAG;CACL,CAAC;CAED,OAAO;EAAE,MAAM,MAAM;EAAM,WAAW,MAAM;EAAW;CAAU;AACnE;;;;;;ACzDA,MAAa,sBAAkD,EAC7D,SACA,mBAIkC;CAClC,MAAM,SAAS,gBAAgB;CAE/B,OAAO,SAAS;EACd,UAAU,CAAC,0BAA0B,OAAO;EAC5C,SAAS,YAAY;GACnB,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,sBAAsB;GAExC,OAAO,OAAO,cAAc,OAAO;EACrC;EACA,GAAG;CACL,CAAC;AACH;;;;ACvBA,MAAa,wBAAwB;AAcrC,SAAgB,+BAAgE,EAC9E,WACA,QACA,gBAK+B;CAC/B,MAAM,SAAS,gBAAgB;CAC/B,MAAM,SAAgC,EAAE,SAAA,IAA+B;CACvE,IAAI,QAAQ,YAAY,OAAO,aAAa,OAAO;CACnD,IAAI,QAAQ,UAAU,OAAO,WAAW,OAAO;CAE/C,OAAO,SAAS;EAEd,UAAU,YAAY;GAAC;GAAuB;GAAW;EAAM,IAAI,CAAC,eAAe,MAAM;EACzF,eAAgB,YAAY,OAAO,uBAAuB,WAAW,MAAM,IAAI,OAAO,gBAAgB,MAAM;EAC5G,GAAG;CACL,CAAC;AACH;;;ACzBA,MAAM,wBAAwB;AAE9B,MAAM,6BAA6B;AACnC,MAAM,mBAAmB;AAOzB,SAAS,MAAM,KAAoC;CACjD,MAAM,SAAqB,CAAC;CAC5B,KAAK,IAAI,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS,4BAC/C,OAAO,KAAK,IAAI,MAAM,OAAO,QAAQ,0BAA0B,CAAC;CAElE,OAAO;AACT;AAEA,SAAS,cAAiB,SAAyC;CACjE,OAAO,QAAQ,SAAQ,WAAW,OAAO,WAAW,cAAc,CAAC,OAAO,KAAK,IAAI,CAAC,CAAE;AACxF;;;;;AAMA,SAAS,cACP,eACA,gBACA,mBACoC;CACpC,MAAM,YAAY,IAAI,IAAI,aAAa;CACvC,MAAM,+BAAe,IAAI,IAA8B;CACvD,KAAK,MAAM,SAAS,eAAe,SAAQ,aAAY,SAAS,MAAM,GAAG;EACvE,MAAM,eAAe,MAAM,WAAW;EACtC,MAAM,aAAa,aAAa,KAAK;EACrC,IAAI,CAAC,gBAAgB,CAAC,UAAU,IAAI,YAAY,KAAK,CAAC,YAAY;EAClE,WAAW,YAAY,cAAc,cAAc,iBAAiB,GAAG,YAAY,KAAK;CAC1F;CAEA,MAAM,0BAAU,IAAI,IAAmC;CACvD,KAAK,MAAM,CAAC,cAAc,gBAAgB,cACxC,QAAQ,IAAI,cAAc,QAAQ,WAAW,CAAC;CAEhD,KAAK,MAAM,SAAS,kBAAkB,SAAQ,aAAY,SAAS,MAAM,GAAG;EAC1E,MAAM,eAAe,MAAM,WAAW;EACtC,IAAI,CAAC,gBAAgB,CAAC,UAAU,IAAI,YAAY,GAAG;EACnD,YAAY,SAAS,qBAA4C,CAAC,EAAE,CAAC,CAAC,gBAAgB,MAAM;CAC9F;CACA,OAAO;AACT;;;;;;;;AASA,MAAa,6BAA6B,EACxC,aACA,sBAI+D;CAC/D,MAAM,SAAS,gBAAgB;CAC/B,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,YAAY,KAAI,eAAc,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;CAC1F,MAAM,sBAAsB,YAAY,MAAM,EAAE,aAAa,WAAW,aAAa,WAAW,SAAS;CAEzG,OAAO,SAAS;EACd,UAAU,CAAC,4BAA4B,aAAa;EACpD,SAAS,mBAAmB,cAAc,SAAS;EACnD,SAAS,YAAY;GACnB,MAAM,UAAU,MAAM,aAAa;GACnC,MAAM,CAAC,cAAc,mBAAmB,MAAM,QAAQ,IAAI,CACxD,QAAQ,WACN,QAAQ,KAAI,UACV,OAAO,mBAAmB;IACxB,MAAM,CAAC,oBAAoB,mBAAmB;IAC9C,aAAa;IACb,SAAS,CAAC,gBAAgB,MAAM;IAChC,SAAS,EAAE,eAAe,MAAM;IAChC,OAAO,MAAM,SAAS;GACxB,CAAC,CACH,CACF,GACA,QAAQ,WACN,QAAQ,KAAI,UACV,OAAO,mBAAmB;IACxB,MAAM,CAAC,qBAAqB;IAC5B,aAAa;IACb,SAAS,CAAC,cAAc;IACxB,SAAS,EAAE,eAAe,MAAM;IAChC,OAAO,MAAM;GACf,CAAC,CACH,CACF,CACF,CAAC;GACD,OAAO,cAAc,eAAe,cAAc,YAAY,GAAG,cAAc,eAAe,CAAC;EACjG;EAEA,iBAAiB;EACjB,iBAAiB,sBAAsB,mBAAmB;CAC5D,CAAC;AACH;;;AChHA,MAAa,uBAAuB;AAWpC,SAAgB,uBAAoD,EAClE,WACA,QACA,SACA,gBAQA;CACA,MAAM,SAAS,gBAAgB;CAC/B,MAAM,EAAE,QAAQ,mBAAmB,QAAQ,wBAAwB,UAAU;CAC7E,MAAM,aAAa,QAAQ,cAAc,KAAA;CACzC,MAAM,WAAW,QAAQ,YAAY,KAAA;CAErC,MAAM,QAAQ,iBAAiB;EAE7B,UAAU,YACN;GAAC;GAAuB;GAAW;GAAY;IAAE;IAAY;IAAU;GAAQ;EAAC,IAChF;GAAC;GAAe;GAAY;IAAE;IAAY;IAAU;GAAQ;EAAC;EACjE,UAAU,EAAE,gBAAgB;GAC1B,MAAM,SAAgC;IAAE,MAAM;IAAW,SAAA;GAA8B;GACvF,IAAI,YAAY,OAAO,aAAa;GACpC,IAAI,UAAU,OAAO,WAAW;GAChC,IAAI,SAAS,OAAO,UAAU;GAC9B,OAAO,YAAY,OAAO,uBAAuB,WAAW,MAAM,IAAI,OAAO,gBAAgB,MAAM;EACrG;EACA,kBAAkB;EAClB,mBAAmB,UAAU,GAAG,kBAAkB;GAChD,IAAI,CAAC,UAAU,aAAa,UAAU,CAAC,SAAS,YAAY,SAC1D;GAEF,OAAO,gBAAgB;EACzB;EACA,SAAS,SACP,KAAK,MAAM,SAAQ,SAAQ,MAAM,eAAe,CAAC,CAAC;EACpD,GAAG;CACL,CAAC;CAED,gBAAgB;EACd,IAAI,qBAAqB,MAAM,eAAe,CAAC,MAAM,oBACnD,MAAW,cAAc;CAE7B,GAAG,CAAC,mBAAmB,KAAK,CAAC;CAE7B,OAAO;EAAE,GAAG;EAAO;CAAoB;AACzC"}