@elizaos/client 1.6.1-alpha.5 → 1.6.1-alpha.7
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/dist/assets/{main-Bbs84AcL.js → main-BM2lpId8.js} +60 -60
- package/dist/assets/main-BM2lpId8.js.map +1 -0
- package/dist/assets/{main-4tyUgNqd.js → main-CQAV8tyh.js} +4 -4
- package/dist/assets/main-CQAV8tyh.js.map +1 -0
- package/dist/assets/react-vendor-C1OK-nqm.js +611 -0
- package/dist/assets/react-vendor-C1OK-nqm.js.map +1 -0
- package/dist/index.html +1 -1
- package/package.json +25 -25
- package/src/components/agent-prism/Avatar.tsx +56 -68
- package/src/components/agent-prism/Badge.tsx +22 -29
- package/src/components/agent-prism/Button.tsx +39 -51
- package/src/components/agent-prism/CollapseAndExpandControls.tsx +9 -25
- package/src/components/agent-prism/CollapsibleSection.tsx +18 -18
- package/src/components/agent-prism/DetailsView/DetailsView.tsx +25 -30
- package/src/components/agent-prism/DetailsView/DetailsViewAttributesTab.tsx +6 -12
- package/src/components/agent-prism/DetailsView/DetailsViewHeader.tsx +9 -13
- package/src/components/agent-prism/DetailsView/DetailsViewHeaderActions.tsx +2 -2
- package/src/components/agent-prism/DetailsView/DetailsViewInputOutputTab.tsx +30 -60
- package/src/components/agent-prism/DetailsView/DetailsViewMetrics.tsx +10 -18
- package/src/components/agent-prism/DetailsView/DetailsViewRawDataTab.tsx +3 -3
- package/src/components/agent-prism/IconButton.tsx +25 -28
- package/src/components/agent-prism/PriceBadge.tsx +4 -4
- package/src/components/agent-prism/SearchInput.tsx +3 -9
- package/src/components/agent-prism/SpanCard/SpanCard.tsx +74 -104
- package/src/components/agent-prism/SpanCard/SpanCardBadges.tsx +7 -13
- package/src/components/agent-prism/SpanCard/SpanCardConnector.tsx +9 -9
- package/src/components/agent-prism/SpanCard/SpanCardTimeline.tsx +15 -20
- package/src/components/agent-prism/SpanCard/SpanCardToggle.tsx +5 -9
- package/src/components/agent-prism/SpanStatus.tsx +24 -30
- package/src/components/agent-prism/Tabs.tsx +16 -19
- package/src/components/agent-prism/TextInput.tsx +18 -21
- package/src/components/agent-prism/TimestampBadge.tsx +5 -9
- package/src/components/agent-prism/TokensBadge.tsx +6 -10
- package/src/components/agent-prism/TraceList/TraceList.tsx +11 -17
- package/src/components/agent-prism/TraceList/TraceListItem.tsx +18 -24
- package/src/components/agent-prism/TraceList/TraceListItemHeader.tsx +8 -20
- package/src/components/agent-prism/TraceViewer.tsx +36 -53
- package/src/components/agent-prism/TreeView.tsx +7 -7
- package/src/components/agent-prism/shared.ts +81 -93
- package/src/components/agent-runs/AgentRunTimeline.tsx +3 -5
- package/src/components/chat.tsx +7 -7
- package/src/lib/agent-prism-utils.ts +29 -32
- package/src/lib/eliza-span-adapter.ts +438 -440
- package/dist/assets/main-4tyUgNqd.js.map +0 -1
- package/dist/assets/main-Bbs84AcL.js.map +0 -1
- package/dist/assets/react-vendor-DxnAFk-d.js +0 -611
- package/dist/assets/react-vendor-DxnAFk-d.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
TraceSpan,
|
|
3
|
+
TraceSpanCategory,
|
|
4
|
+
TraceSpanStatus,
|
|
5
|
+
TraceSpanAttribute,
|
|
6
|
+
InputOutputData,
|
|
7
|
+
TraceRecord,
|
|
8
8
|
} from '@evilmartians/agent-prism-types';
|
|
9
9
|
import type { RunDetail, RunEvent, RunSummary } from '@elizaos/api-client';
|
|
10
10
|
|
|
@@ -12,475 +12,473 @@ import type { RunDetail, RunEvent, RunSummary } from '@elizaos/api-client';
|
|
|
12
12
|
* Adapter to convert ElizaOS RunDetail data to Agent Prism TraceSpan format
|
|
13
13
|
*/
|
|
14
14
|
export class ElizaSpanAdapter {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Sort events by timestamp
|
|
25
|
-
const sortedEvents = [...events].sort((a, b) => a.timestamp - b.timestamp);
|
|
26
|
-
|
|
27
|
-
// Track actions and their attempts
|
|
28
|
-
const actionMap = new Map<string, TraceSpan>();
|
|
29
|
-
const attemptMap = new Map<string, TraceSpan>();
|
|
30
|
-
const rootSpans: TraceSpan[] = [];
|
|
31
|
-
|
|
32
|
-
sortedEvents.forEach((event, index) => {
|
|
33
|
-
switch (event.type) {
|
|
34
|
-
case 'RUN_STARTED': {
|
|
35
|
-
// Create root run span
|
|
36
|
-
const runSpan = this.createRunSpan(runDetail, event);
|
|
37
|
-
rootSpans.push(runSpan);
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
case 'ACTION_STARTED': {
|
|
42
|
-
const actionName =
|
|
43
|
-
(event.data.actionName as string) ||
|
|
44
|
-
(event.data.actionId as string) ||
|
|
45
|
-
`Action ${index}`;
|
|
46
|
-
const actionKey = (event.data.actionId as string) || actionName;
|
|
47
|
-
|
|
48
|
-
let actionSpan = actionMap.get(actionKey);
|
|
49
|
-
if (!actionSpan) {
|
|
50
|
-
// Create new action span
|
|
51
|
-
actionSpan = {
|
|
52
|
-
id: `action-${actionKey}`,
|
|
53
|
-
title: actionName,
|
|
54
|
-
type: 'agent_invocation' as TraceSpanCategory,
|
|
55
|
-
status: 'pending' as TraceSpanStatus,
|
|
56
|
-
startTime: new Date(event.timestamp),
|
|
57
|
-
endTime: new Date(event.timestamp), // Will be updated on completion
|
|
58
|
-
duration: 0,
|
|
59
|
-
raw: JSON.stringify(event, null, 2),
|
|
60
|
-
attributes: this.convertEventDataToAttributes(event.data),
|
|
61
|
-
children: [],
|
|
62
|
-
};
|
|
63
|
-
actionMap.set(actionKey, actionSpan);
|
|
64
|
-
rootSpans.push(actionSpan);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Create attempt span
|
|
68
|
-
const attemptIndex = (actionSpan.children?.length || 0) + 1;
|
|
69
|
-
const attemptSpan: TraceSpan = {
|
|
70
|
-
id: `attempt-${actionKey}-${attemptIndex}`,
|
|
71
|
-
title: `Attempt ${attemptIndex}`,
|
|
72
|
-
type: 'span' as TraceSpanCategory,
|
|
73
|
-
status: 'pending' as TraceSpanStatus,
|
|
74
|
-
startTime: new Date(event.timestamp),
|
|
75
|
-
endTime: new Date(event.timestamp),
|
|
76
|
-
duration: 0,
|
|
77
|
-
raw: JSON.stringify(event, null, 2),
|
|
78
|
-
attributes: this.convertEventDataToAttributes(event.data),
|
|
79
|
-
children: [],
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
actionSpan.children = [...(actionSpan.children || []), attemptSpan];
|
|
83
|
-
attemptMap.set(actionKey, attemptSpan);
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
case 'ACTION_COMPLETED': {
|
|
88
|
-
const actionName =
|
|
89
|
-
(event.data.actionName as string) ||
|
|
90
|
-
(event.data.actionId as string) ||
|
|
91
|
-
`Action ${index}`;
|
|
92
|
-
const actionKey = (event.data.actionId as string) || actionName;
|
|
93
|
-
const actionSpan = actionMap.get(actionKey);
|
|
94
|
-
const attemptSpan = attemptMap.get(actionKey);
|
|
95
|
-
|
|
96
|
-
// Extract input/output if available
|
|
97
|
-
const prompt = this.extractPrompt(event.data);
|
|
98
|
-
const response = this.extractResponse(event.data);
|
|
99
|
-
|
|
100
|
-
if (attemptSpan) {
|
|
101
|
-
const success = (event.data.success as boolean | undefined) !== false;
|
|
102
|
-
attemptSpan.status = success ? 'success' : 'error';
|
|
103
|
-
attemptSpan.endTime = new Date(event.timestamp);
|
|
104
|
-
attemptSpan.duration = event.timestamp - attemptSpan.startTime.getTime();
|
|
105
|
-
if (prompt) attemptSpan.input = prompt;
|
|
106
|
-
if (response) attemptSpan.output = response;
|
|
107
|
-
attemptMap.delete(actionKey);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (actionSpan) {
|
|
111
|
-
const success = (event.data.success as boolean | undefined) !== false;
|
|
112
|
-
actionSpan.status = success ? 'success' : 'error';
|
|
113
|
-
actionSpan.endTime = new Date(event.timestamp);
|
|
114
|
-
actionSpan.duration =
|
|
115
|
-
event.timestamp - actionSpan.startTime.getTime();
|
|
116
|
-
if (prompt && !actionSpan.input) actionSpan.input = prompt;
|
|
117
|
-
if (response && !actionSpan.output) actionSpan.output = response;
|
|
118
|
-
}
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
case 'MODEL_USED': {
|
|
123
|
-
const modelType = (event.data.modelType as string) || 'Model Call';
|
|
124
|
-
|
|
125
|
-
// Extract prompt and response from event data
|
|
126
|
-
const prompt = this.extractPrompt(event.data);
|
|
127
|
-
const response = this.extractResponse(event.data);
|
|
128
|
-
|
|
129
|
-
const modelSpan: TraceSpan = {
|
|
130
|
-
id: `model-${index}`,
|
|
131
|
-
title: modelType,
|
|
132
|
-
type: 'llm_call' as TraceSpanCategory,
|
|
133
|
-
status: 'success' as TraceSpanStatus,
|
|
134
|
-
startTime: new Date(event.timestamp),
|
|
135
|
-
endTime: new Date(
|
|
136
|
-
event.timestamp + ((event.data.executionTime as number) || 0)
|
|
137
|
-
),
|
|
138
|
-
duration: (event.data.executionTime as number) || 0,
|
|
139
|
-
raw: JSON.stringify(event, null, 2),
|
|
140
|
-
attributes: this.convertEventDataToAttributes(event.data),
|
|
141
|
-
input: prompt,
|
|
142
|
-
output: response,
|
|
143
|
-
tokensCount: this.extractTokensCount(event.data),
|
|
144
|
-
cost: this.extractCost(event.data),
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// Attach to current attempt or action
|
|
148
|
-
const actionContext = (event.data.actionContext as string | undefined) || undefined;
|
|
149
|
-
const targetKey = actionContext || Array.from(attemptMap.keys()).pop();
|
|
150
|
-
|
|
151
|
-
if (targetKey) {
|
|
152
|
-
const attemptSpan = attemptMap.get(targetKey);
|
|
153
|
-
if (attemptSpan) {
|
|
154
|
-
attemptSpan.children = [...(attemptSpan.children || []), modelSpan];
|
|
155
|
-
} else {
|
|
156
|
-
// Fallback to action
|
|
157
|
-
const actionSpan = actionMap.get(targetKey);
|
|
158
|
-
if (actionSpan && actionSpan.children && actionSpan.children.length > 0) {
|
|
159
|
-
const lastAttempt = actionSpan.children[actionSpan.children.length - 1];
|
|
160
|
-
lastAttempt.children = [...(lastAttempt.children || []), modelSpan];
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
rootSpans.push(modelSpan);
|
|
165
|
-
}
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
case 'EVALUATOR_COMPLETED': {
|
|
170
|
-
const evaluatorName = (event.data.evaluatorName as string) || `Evaluator ${index}`;
|
|
171
|
-
const evaluatorSpan: TraceSpan = {
|
|
172
|
-
id: `evaluator-${index}`,
|
|
173
|
-
title: evaluatorName,
|
|
174
|
-
type: 'chain_operation' as TraceSpanCategory,
|
|
175
|
-
status: 'success' as TraceSpanStatus,
|
|
176
|
-
startTime: new Date(event.timestamp),
|
|
177
|
-
endTime: new Date(event.timestamp),
|
|
178
|
-
duration: 0,
|
|
179
|
-
raw: JSON.stringify(event, null, 2),
|
|
180
|
-
attributes: this.convertEventDataToAttributes(event.data),
|
|
181
|
-
};
|
|
182
|
-
rootSpans.push(evaluatorSpan);
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
case 'EMBEDDING_EVENT': {
|
|
187
|
-
const status = (event.data.status as string) || 'completed';
|
|
188
|
-
const embeddingSpan: TraceSpan = {
|
|
189
|
-
id: `embedding-${index}`,
|
|
190
|
-
title: `Embedding ${status}`,
|
|
191
|
-
type: 'embedding' as TraceSpanCategory,
|
|
192
|
-
status: status === 'failed' ? 'error' : 'success',
|
|
193
|
-
startTime: new Date(event.timestamp),
|
|
194
|
-
endTime: new Date(
|
|
195
|
-
event.timestamp + ((event.data.durationMs as number) || 0)
|
|
196
|
-
),
|
|
197
|
-
duration: (event.data.durationMs as number) || 0,
|
|
198
|
-
raw: JSON.stringify(event, null, 2),
|
|
199
|
-
attributes: this.convertEventDataToAttributes(event.data),
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Attach to current attempt or action
|
|
203
|
-
const targetKey = Array.from(attemptMap.keys()).pop();
|
|
204
|
-
if (targetKey) {
|
|
205
|
-
const attemptSpan = attemptMap.get(targetKey);
|
|
206
|
-
if (attemptSpan) {
|
|
207
|
-
attemptSpan.children = [...(attemptSpan.children || []), embeddingSpan];
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
rootSpans.push(embeddingSpan);
|
|
211
|
-
}
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
default:
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
return rootSpans;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Create a root run span from RunDetail
|
|
225
|
-
*/
|
|
226
|
-
private createRunSpan(runDetail: RunDetail, startEvent: RunEvent): TraceSpan {
|
|
227
|
-
const summary = runDetail.summary;
|
|
228
|
-
const duration = summary.durationMs || 0;
|
|
229
|
-
const startTime = new Date(startEvent.timestamp);
|
|
230
|
-
const endTime = new Date(startEvent.timestamp + duration);
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
id: summary.runId,
|
|
234
|
-
title: `Run ${new Date(summary.startedAt || Date.now()).toLocaleTimeString()}`,
|
|
235
|
-
type: 'agent_invocation' as TraceSpanCategory,
|
|
236
|
-
status: this.convertRunStatus(summary.status),
|
|
237
|
-
startTime,
|
|
238
|
-
endTime,
|
|
239
|
-
duration,
|
|
240
|
-
raw: JSON.stringify(runDetail, null, 2),
|
|
241
|
-
attributes: [
|
|
242
|
-
{ key: 'run.id', value: { stringValue: summary.runId } },
|
|
243
|
-
{ key: 'run.status', value: { stringValue: summary.status } },
|
|
244
|
-
...(summary.messageId
|
|
245
|
-
? [{ key: 'message.id', value: { stringValue: summary.messageId } }]
|
|
246
|
-
: []),
|
|
247
|
-
...(summary.roomId ? [{ key: 'room.id', value: { stringValue: summary.roomId } }] : []),
|
|
248
|
-
] as TraceSpanAttribute[],
|
|
249
|
-
children: [],
|
|
250
|
-
};
|
|
15
|
+
/**
|
|
16
|
+
* Convert ElizaOS RunDetail to Agent Prism TraceSpans with hierarchical structure
|
|
17
|
+
*/
|
|
18
|
+
convertRunDetailToTraceSpans(runDetail: RunDetail): TraceSpan[] {
|
|
19
|
+
const events = runDetail.events;
|
|
20
|
+
if (!events || events.length === 0) {
|
|
21
|
+
return [];
|
|
251
22
|
}
|
|
252
23
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
24
|
+
// Sort events by timestamp
|
|
25
|
+
const sortedEvents = [...events].sort((a, b) => a.timestamp - b.timestamp);
|
|
26
|
+
|
|
27
|
+
// Track actions and their attempts
|
|
28
|
+
const actionMap = new Map<string, TraceSpan>();
|
|
29
|
+
const attemptMap = new Map<string, TraceSpan>();
|
|
30
|
+
const rootSpans: TraceSpan[] = [];
|
|
31
|
+
|
|
32
|
+
sortedEvents.forEach((event, index) => {
|
|
33
|
+
switch (event.type) {
|
|
34
|
+
case 'RUN_STARTED': {
|
|
35
|
+
// Create root run span
|
|
36
|
+
const runSpan = this.createRunSpan(runDetail, event);
|
|
37
|
+
rootSpans.push(runSpan);
|
|
38
|
+
break;
|
|
268
39
|
}
|
|
269
|
-
}
|
|
270
40
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
41
|
+
case 'ACTION_STARTED': {
|
|
42
|
+
const actionName =
|
|
43
|
+
(event.data.actionName as string) ||
|
|
44
|
+
(event.data.actionId as string) ||
|
|
45
|
+
`Action ${index}`;
|
|
46
|
+
const actionKey = (event.data.actionId as string) || actionName;
|
|
47
|
+
|
|
48
|
+
let actionSpan = actionMap.get(actionKey);
|
|
49
|
+
if (!actionSpan) {
|
|
50
|
+
// Create new action span
|
|
51
|
+
actionSpan = {
|
|
52
|
+
id: `action-${actionKey}`,
|
|
53
|
+
title: actionName,
|
|
54
|
+
type: 'agent_invocation' as TraceSpanCategory,
|
|
55
|
+
status: 'pending' as TraceSpanStatus,
|
|
56
|
+
startTime: new Date(event.timestamp),
|
|
57
|
+
endTime: new Date(event.timestamp), // Will be updated on completion
|
|
58
|
+
duration: 0,
|
|
59
|
+
raw: JSON.stringify(event, null, 2),
|
|
60
|
+
attributes: this.convertEventDataToAttributes(event.data),
|
|
61
|
+
children: [],
|
|
62
|
+
};
|
|
63
|
+
actionMap.set(actionKey, actionSpan);
|
|
64
|
+
rootSpans.push(actionSpan);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create attempt span
|
|
68
|
+
const attemptIndex = (actionSpan.children?.length || 0) + 1;
|
|
69
|
+
const attemptSpan: TraceSpan = {
|
|
70
|
+
id: `attempt-${actionKey}-${attemptIndex}`,
|
|
71
|
+
title: `Attempt ${attemptIndex}`,
|
|
72
|
+
type: 'span' as TraceSpanCategory,
|
|
73
|
+
status: 'pending' as TraceSpanStatus,
|
|
74
|
+
startTime: new Date(event.timestamp),
|
|
75
|
+
endTime: new Date(event.timestamp),
|
|
76
|
+
duration: 0,
|
|
77
|
+
raw: JSON.stringify(event, null, 2),
|
|
78
|
+
attributes: this.convertEventDataToAttributes(event.data),
|
|
79
|
+
children: [],
|
|
80
|
+
};
|
|
280
81
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
private convertValueToAttributeValue(value: unknown): {
|
|
285
|
-
stringValue?: string;
|
|
286
|
-
intValue?: string;
|
|
287
|
-
boolValue?: boolean;
|
|
288
|
-
} {
|
|
289
|
-
if (typeof value === 'string') {
|
|
290
|
-
return { stringValue: value };
|
|
291
|
-
}
|
|
292
|
-
if (typeof value === 'number') {
|
|
293
|
-
return { intValue: value.toString() };
|
|
294
|
-
}
|
|
295
|
-
if (typeof value === 'boolean') {
|
|
296
|
-
return { boolValue: value };
|
|
82
|
+
actionSpan.children = [...(actionSpan.children || []), attemptSpan];
|
|
83
|
+
attemptMap.set(actionKey, attemptSpan);
|
|
84
|
+
break;
|
|
297
85
|
}
|
|
298
|
-
return { stringValue: JSON.stringify(value) };
|
|
299
|
-
}
|
|
300
86
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
87
|
+
case 'ACTION_COMPLETED': {
|
|
88
|
+
const actionName =
|
|
89
|
+
(event.data.actionName as string) ||
|
|
90
|
+
(event.data.actionId as string) ||
|
|
91
|
+
`Action ${index}`;
|
|
92
|
+
const actionKey = (event.data.actionId as string) || actionName;
|
|
93
|
+
const actionSpan = actionMap.get(actionKey);
|
|
94
|
+
const attemptSpan = attemptMap.get(actionKey);
|
|
95
|
+
|
|
96
|
+
// Extract input/output if available
|
|
97
|
+
const prompt = this.extractPrompt(event.data);
|
|
98
|
+
const response = this.extractResponse(event.data);
|
|
99
|
+
|
|
100
|
+
if (attemptSpan) {
|
|
101
|
+
const success = (event.data.success as boolean | undefined) !== false;
|
|
102
|
+
attemptSpan.status = success ? 'success' : 'error';
|
|
103
|
+
attemptSpan.endTime = new Date(event.timestamp);
|
|
104
|
+
attemptSpan.duration = event.timestamp - attemptSpan.startTime.getTime();
|
|
105
|
+
if (prompt) attemptSpan.input = prompt;
|
|
106
|
+
if (response) attemptSpan.output = response;
|
|
107
|
+
attemptMap.delete(actionKey);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (actionSpan) {
|
|
111
|
+
const success = (event.data.success as boolean | undefined) !== false;
|
|
112
|
+
actionSpan.status = success ? 'success' : 'error';
|
|
113
|
+
actionSpan.endTime = new Date(event.timestamp);
|
|
114
|
+
actionSpan.duration = event.timestamp - actionSpan.startTime.getTime();
|
|
115
|
+
if (prompt && !actionSpan.input) actionSpan.input = prompt;
|
|
116
|
+
if (response && !actionSpan.output) actionSpan.output = response;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
307
119
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
120
|
+
|
|
121
|
+
case 'MODEL_USED': {
|
|
122
|
+
const modelType = (event.data.modelType as string) || 'Model Call';
|
|
123
|
+
|
|
124
|
+
// Extract prompt and response from event data
|
|
125
|
+
const prompt = this.extractPrompt(event.data);
|
|
126
|
+
const response = this.extractResponse(event.data);
|
|
127
|
+
|
|
128
|
+
const modelSpan: TraceSpan = {
|
|
129
|
+
id: `model-${index}`,
|
|
130
|
+
title: modelType,
|
|
131
|
+
type: 'llm_call' as TraceSpanCategory,
|
|
132
|
+
status: 'success' as TraceSpanStatus,
|
|
133
|
+
startTime: new Date(event.timestamp),
|
|
134
|
+
endTime: new Date(event.timestamp + ((event.data.executionTime as number) || 0)),
|
|
135
|
+
duration: (event.data.executionTime as number) || 0,
|
|
136
|
+
raw: JSON.stringify(event, null, 2),
|
|
137
|
+
attributes: this.convertEventDataToAttributes(event.data),
|
|
138
|
+
input: prompt,
|
|
139
|
+
output: response,
|
|
140
|
+
tokensCount: this.extractTokensCount(event.data),
|
|
141
|
+
cost: this.extractCost(event.data),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Attach to current attempt or action
|
|
145
|
+
const actionContext = (event.data.actionContext as string | undefined) || undefined;
|
|
146
|
+
const targetKey = actionContext || Array.from(attemptMap.keys()).pop();
|
|
147
|
+
|
|
148
|
+
if (targetKey) {
|
|
149
|
+
const attemptSpan = attemptMap.get(targetKey);
|
|
150
|
+
if (attemptSpan) {
|
|
151
|
+
attemptSpan.children = [...(attemptSpan.children || []), modelSpan];
|
|
152
|
+
} else {
|
|
153
|
+
// Fallback to action
|
|
154
|
+
const actionSpan = actionMap.get(targetKey);
|
|
155
|
+
if (actionSpan && actionSpan.children && actionSpan.children.length > 0) {
|
|
156
|
+
const lastAttempt = actionSpan.children[actionSpan.children.length - 1];
|
|
157
|
+
lastAttempt.children = [...(lastAttempt.children || []), modelSpan];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
rootSpans.push(modelSpan);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
311
164
|
}
|
|
312
|
-
return undefined;
|
|
313
|
-
}
|
|
314
165
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
166
|
+
case 'EVALUATOR_COMPLETED': {
|
|
167
|
+
const evaluatorName = (event.data.evaluatorName as string) || `Evaluator ${index}`;
|
|
168
|
+
const evaluatorSpan: TraceSpan = {
|
|
169
|
+
id: `evaluator-${index}`,
|
|
170
|
+
title: evaluatorName,
|
|
171
|
+
type: 'chain_operation' as TraceSpanCategory,
|
|
172
|
+
status: 'success' as TraceSpanStatus,
|
|
173
|
+
startTime: new Date(event.timestamp),
|
|
174
|
+
endTime: new Date(event.timestamp),
|
|
175
|
+
duration: 0,
|
|
176
|
+
raw: JSON.stringify(event, null, 2),
|
|
177
|
+
attributes: this.convertEventDataToAttributes(event.data),
|
|
178
|
+
};
|
|
179
|
+
rootSpans.push(evaluatorSpan);
|
|
180
|
+
break;
|
|
326
181
|
}
|
|
327
182
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
183
|
+
case 'EMBEDDING_EVENT': {
|
|
184
|
+
const status = (event.data.status as string) || 'completed';
|
|
185
|
+
const embeddingSpan: TraceSpan = {
|
|
186
|
+
id: `embedding-${index}`,
|
|
187
|
+
title: `Embedding ${status}`,
|
|
188
|
+
type: 'embedding' as TraceSpanCategory,
|
|
189
|
+
status: status === 'failed' ? 'error' : 'success',
|
|
190
|
+
startTime: new Date(event.timestamp),
|
|
191
|
+
endTime: new Date(event.timestamp + ((event.data.durationMs as number) || 0)),
|
|
192
|
+
duration: (event.data.durationMs as number) || 0,
|
|
193
|
+
raw: JSON.stringify(event, null, 2),
|
|
194
|
+
attributes: this.convertEventDataToAttributes(event.data),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Attach to current attempt or action
|
|
198
|
+
const targetKey = Array.from(attemptMap.keys()).pop();
|
|
199
|
+
if (targetKey) {
|
|
200
|
+
const attemptSpan = attemptMap.get(targetKey);
|
|
201
|
+
if (attemptSpan) {
|
|
202
|
+
attemptSpan.children = [...(attemptSpan.children || []), embeddingSpan];
|
|
340
203
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (data.response && typeof data.response === 'object') {
|
|
346
|
-
const response = data.response as Record<string, unknown>;
|
|
347
|
-
const fromResponseUsage = extractFromUsage(response['usage']);
|
|
348
|
-
if (fromResponseUsage !== undefined) return fromResponseUsage;
|
|
204
|
+
} else {
|
|
205
|
+
rootSpans.push(embeddingSpan);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
349
208
|
}
|
|
350
209
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
210
|
+
default:
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return rootSpans;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create a root run span from RunDetail
|
|
220
|
+
*/
|
|
221
|
+
private createRunSpan(runDetail: RunDetail, startEvent: RunEvent): TraceSpan {
|
|
222
|
+
const summary = runDetail.summary;
|
|
223
|
+
const duration = summary.durationMs || 0;
|
|
224
|
+
const startTime = new Date(startEvent.timestamp);
|
|
225
|
+
const endTime = new Date(startEvent.timestamp + duration);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
id: summary.runId,
|
|
229
|
+
title: `Run ${new Date(summary.startedAt || Date.now()).toLocaleTimeString()}`,
|
|
230
|
+
type: 'agent_invocation' as TraceSpanCategory,
|
|
231
|
+
status: this.convertRunStatus(summary.status),
|
|
232
|
+
startTime,
|
|
233
|
+
endTime,
|
|
234
|
+
duration,
|
|
235
|
+
raw: JSON.stringify(runDetail, null, 2),
|
|
236
|
+
attributes: [
|
|
237
|
+
{ key: 'run.id', value: { stringValue: summary.runId } },
|
|
238
|
+
{ key: 'run.status', value: { stringValue: summary.status } },
|
|
239
|
+
...(summary.messageId
|
|
240
|
+
? [{ key: 'message.id', value: { stringValue: summary.messageId } }]
|
|
241
|
+
: []),
|
|
242
|
+
...(summary.roomId ? [{ key: 'room.id', value: { stringValue: summary.roomId } }] : []),
|
|
243
|
+
] as TraceSpanAttribute[],
|
|
244
|
+
children: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Convert RunStatus to TraceSpanStatus
|
|
250
|
+
*/
|
|
251
|
+
private convertRunStatus(status: string): TraceSpanStatus {
|
|
252
|
+
switch (status) {
|
|
253
|
+
case 'completed':
|
|
254
|
+
return 'success';
|
|
255
|
+
case 'error':
|
|
256
|
+
return 'error';
|
|
257
|
+
case 'timeout':
|
|
258
|
+
return 'warning';
|
|
259
|
+
case 'started':
|
|
260
|
+
return 'pending';
|
|
261
|
+
default:
|
|
262
|
+
return 'pending';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Convert event data to TraceSpanAttribute array
|
|
268
|
+
*/
|
|
269
|
+
private convertEventDataToAttributes(data: Record<string, unknown>): TraceSpanAttribute[] {
|
|
270
|
+
return Object.entries(data).map(([key, value]) => ({
|
|
271
|
+
key,
|
|
272
|
+
value: this.convertValueToAttributeValue(value),
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Convert a value to TraceSpanAttributeValue
|
|
278
|
+
*/
|
|
279
|
+
private convertValueToAttributeValue(value: unknown): {
|
|
280
|
+
stringValue?: string;
|
|
281
|
+
intValue?: string;
|
|
282
|
+
boolValue?: boolean;
|
|
283
|
+
} {
|
|
284
|
+
if (typeof value === 'string') {
|
|
285
|
+
return { stringValue: value };
|
|
286
|
+
}
|
|
287
|
+
if (typeof value === 'number') {
|
|
288
|
+
return { intValue: value.toString() };
|
|
289
|
+
}
|
|
290
|
+
if (typeof value === 'boolean') {
|
|
291
|
+
return { boolValue: value };
|
|
292
|
+
}
|
|
293
|
+
return { stringValue: JSON.stringify(value) };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Safely coerce a possibly numeric value (number or numeric string) to number
|
|
298
|
+
*/
|
|
299
|
+
private coerceToNumber(value: unknown): number | undefined {
|
|
300
|
+
if (typeof value === 'number') {
|
|
301
|
+
return Number.isFinite(value) ? value : undefined;
|
|
302
|
+
}
|
|
303
|
+
if (typeof value === 'string') {
|
|
304
|
+
const parsed = Number(value);
|
|
305
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Extract tokens count from event data
|
|
312
|
+
*/
|
|
313
|
+
private extractTokensCount(data: Record<string, unknown>): number | undefined {
|
|
314
|
+
// Prefer explicit direct fields if present, even if they sum to 0
|
|
315
|
+
const hasInputTokens = Object.prototype.hasOwnProperty.call(data, 'inputTokens');
|
|
316
|
+
const hasOutputTokens = Object.prototype.hasOwnProperty.call(data, 'outputTokens');
|
|
317
|
+
if (hasInputTokens || hasOutputTokens) {
|
|
318
|
+
const input = this.coerceToNumber((data as Record<string, unknown>)['inputTokens']) ?? 0;
|
|
319
|
+
const output = this.coerceToNumber((data as Record<string, unknown>)['outputTokens']) ?? 0;
|
|
320
|
+
return input + output;
|
|
321
|
+
}
|
|
354
322
|
|
|
355
|
-
|
|
323
|
+
// Helper to extract from a usage-like object
|
|
324
|
+
const extractFromUsage = (usageContainer: unknown): number | undefined => {
|
|
325
|
+
if (!usageContainer || typeof usageContainer !== 'object') return undefined;
|
|
326
|
+
const container = usageContainer as Record<string, unknown>;
|
|
327
|
+
const totalTokens = this.coerceToNumber(container['total_tokens']);
|
|
328
|
+
if (totalTokens !== undefined) return totalTokens;
|
|
329
|
+
const hasPrompt = Object.prototype.hasOwnProperty.call(container, 'prompt_tokens');
|
|
330
|
+
const hasCompletion = Object.prototype.hasOwnProperty.call(container, 'completion_tokens');
|
|
331
|
+
if (hasPrompt || hasCompletion) {
|
|
332
|
+
const prompt = this.coerceToNumber(container['prompt_tokens']) ?? 0;
|
|
333
|
+
const completion = this.coerceToNumber(container['completion_tokens']) ?? 0;
|
|
334
|
+
return prompt + completion;
|
|
335
|
+
}
|
|
336
|
+
return undefined;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Try response.usage object (common in LLM responses)
|
|
340
|
+
if (data.response && typeof data.response === 'object') {
|
|
341
|
+
const response = data.response as Record<string, unknown>;
|
|
342
|
+
const fromResponseUsage = extractFromUsage(response['usage']);
|
|
343
|
+
if (fromResponseUsage !== undefined) return fromResponseUsage;
|
|
356
344
|
}
|
|
357
345
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
private extractCost(data: Record<string, unknown>): number | undefined {
|
|
362
|
-
// Try direct cost field
|
|
363
|
-
if (data.cost && typeof data.cost === 'number') {
|
|
364
|
-
return data.cost;
|
|
365
|
-
}
|
|
346
|
+
// Try top-level usage object
|
|
347
|
+
const fromTopLevelUsage = extractFromUsage(data['usage']);
|
|
348
|
+
if (fromTopLevelUsage !== undefined) return fromTopLevelUsage;
|
|
366
349
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const response = data.response as Record<string, unknown>;
|
|
370
|
-
if (response.cost && typeof response.cost === 'number') {
|
|
371
|
-
return response.cost;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
374
352
|
|
|
375
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Extract cost from event data
|
|
355
|
+
*/
|
|
356
|
+
private extractCost(data: Record<string, unknown>): number | undefined {
|
|
357
|
+
// Try direct cost field
|
|
358
|
+
if (data.cost && typeof data.cost === 'number') {
|
|
359
|
+
return data.cost;
|
|
376
360
|
}
|
|
377
361
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (prompts.length > 0) {
|
|
386
|
-
return prompts
|
|
387
|
-
.map((p, idx) => {
|
|
388
|
-
const header = prompts.length > 1 ? `[Prompt ${idx + 1}${p.modelType ? ` - ${p.modelType}` : ''}]\n` : '';
|
|
389
|
-
return header + (p.prompt || '');
|
|
390
|
-
})
|
|
391
|
-
.join('\n\n---\n\n');
|
|
392
|
-
}
|
|
393
|
-
}
|
|
362
|
+
// Try response.cost
|
|
363
|
+
if (data.response && typeof data.response === 'object') {
|
|
364
|
+
const response = data.response as Record<string, unknown>;
|
|
365
|
+
if (response.cost && typeof response.cost === 'number') {
|
|
366
|
+
return response.cost;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
394
369
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Extract prompt/input from event data
|
|
375
|
+
*/
|
|
376
|
+
private extractPrompt(data: Record<string, unknown>): string | undefined {
|
|
377
|
+
// Handle multiple prompts array (from actions)
|
|
378
|
+
if (data.prompts && Array.isArray(data.prompts)) {
|
|
379
|
+
const prompts = data.prompts as Array<{ prompt?: string; modelType?: string }>;
|
|
380
|
+
if (prompts.length > 0) {
|
|
381
|
+
return prompts
|
|
382
|
+
.map((p, idx) => {
|
|
383
|
+
const header =
|
|
384
|
+
prompts.length > 1
|
|
385
|
+
? `[Prompt ${idx + 1}${p.modelType ? ` - ${p.modelType}` : ''}]\n`
|
|
386
|
+
: '';
|
|
387
|
+
return header + (p.prompt || '');
|
|
388
|
+
})
|
|
389
|
+
.join('\n\n---\n\n');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
399
392
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return params.prompt;
|
|
405
|
-
}
|
|
406
|
-
// Return formatted params if no specific prompt
|
|
407
|
-
const { prompt: _, ...otherParams } = params;
|
|
408
|
-
if (Object.keys(otherParams).length > 0) {
|
|
409
|
-
return JSON.stringify(otherParams, null, 2);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
393
|
+
// Try direct prompt field
|
|
394
|
+
if (data.prompt && typeof data.prompt === 'string') {
|
|
395
|
+
return data.prompt;
|
|
396
|
+
}
|
|
412
397
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
398
|
+
// Try params.prompt
|
|
399
|
+
if (data.params && typeof data.params === 'object') {
|
|
400
|
+
const params = data.params as Record<string, unknown>;
|
|
401
|
+
if (params.prompt && typeof params.prompt === 'string') {
|
|
402
|
+
return params.prompt;
|
|
403
|
+
}
|
|
404
|
+
// Return formatted params if no specific prompt
|
|
405
|
+
const { prompt: _, ...otherParams } = params;
|
|
406
|
+
if (Object.keys(otherParams).length > 0) {
|
|
407
|
+
return JSON.stringify(otherParams, null, 2);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
417
410
|
|
|
418
|
-
|
|
411
|
+
// Try input field
|
|
412
|
+
if (data.input && typeof data.input === 'string') {
|
|
413
|
+
return data.input;
|
|
419
414
|
}
|
|
420
415
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
if (response.message && typeof response.message === 'string') {
|
|
441
|
-
return response.message;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Format the full response
|
|
445
|
-
return JSON.stringify(response, null, 2);
|
|
446
|
-
}
|
|
447
|
-
return String(data.response);
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Extract response/output from event data
|
|
421
|
+
*/
|
|
422
|
+
private extractResponse(data: Record<string, unknown>): string | undefined {
|
|
423
|
+
// Handle response object
|
|
424
|
+
if (data.response) {
|
|
425
|
+
if (typeof data.response === 'string') {
|
|
426
|
+
return data.response;
|
|
427
|
+
}
|
|
428
|
+
if (typeof data.response === 'object') {
|
|
429
|
+
const response = data.response as Record<string, unknown>;
|
|
430
|
+
|
|
431
|
+
// Extract text content from common response structures
|
|
432
|
+
if (response.content && typeof response.content === 'string') {
|
|
433
|
+
return response.content;
|
|
448
434
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (data.output) {
|
|
452
|
-
if (typeof data.output === 'string') {
|
|
453
|
-
return data.output;
|
|
454
|
-
}
|
|
455
|
-
return JSON.stringify(data.output, null, 2);
|
|
435
|
+
if (response.text && typeof response.text === 'string') {
|
|
436
|
+
return response.text;
|
|
456
437
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (data.result) {
|
|
460
|
-
if (typeof data.result === 'string') {
|
|
461
|
-
return data.result;
|
|
462
|
-
}
|
|
463
|
-
return JSON.stringify(data.result, null, 2);
|
|
438
|
+
if (response.message && typeof response.message === 'string') {
|
|
439
|
+
return response.message;
|
|
464
440
|
}
|
|
465
441
|
|
|
466
|
-
|
|
442
|
+
// Format the full response
|
|
443
|
+
return JSON.stringify(response, null, 2);
|
|
444
|
+
}
|
|
445
|
+
return String(data.response);
|
|
467
446
|
}
|
|
468
447
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return {
|
|
476
|
-
id: summary.runId,
|
|
477
|
-
name: `Run ${shortId}`,
|
|
478
|
-
spansCount: Object.values(summary.counts || {}).reduce((a, b) => a + b, 0),
|
|
479
|
-
durationMs: summary.durationMs || 0,
|
|
480
|
-
agentDescription: `Status: ${summary.status}`,
|
|
481
|
-
startTime: summary.startedAt || undefined,
|
|
482
|
-
};
|
|
448
|
+
// Try output field
|
|
449
|
+
if (data.output) {
|
|
450
|
+
if (typeof data.output === 'string') {
|
|
451
|
+
return data.output;
|
|
452
|
+
}
|
|
453
|
+
return JSON.stringify(data.output, null, 2);
|
|
483
454
|
}
|
|
455
|
+
|
|
456
|
+
// Try result field (for action results)
|
|
457
|
+
if (data.result) {
|
|
458
|
+
if (typeof data.result === 'string') {
|
|
459
|
+
return data.result;
|
|
460
|
+
}
|
|
461
|
+
return JSON.stringify(data.result, null, 2);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Convert RunSummary to TraceRecord for TraceList component
|
|
469
|
+
*/
|
|
470
|
+
convertRunSummaryToTraceRecord(summary: RunSummary): TraceRecord {
|
|
471
|
+
// Use first 8 characters of runId for compact display
|
|
472
|
+
const shortId = summary.runId.slice(0, 8);
|
|
473
|
+
return {
|
|
474
|
+
id: summary.runId,
|
|
475
|
+
name: `Run ${shortId}`,
|
|
476
|
+
spansCount: Object.values(summary.counts || {}).reduce((a, b) => a + b, 0),
|
|
477
|
+
durationMs: summary.durationMs || 0,
|
|
478
|
+
agentDescription: `Status: ${summary.status}`,
|
|
479
|
+
startTime: summary.startedAt || undefined,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
484
482
|
}
|
|
485
483
|
|
|
486
484
|
// Export a singleton instance
|