@elizaos/client 1.6.1-alpha.4 → 1.6.1-alpha.5
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-C4q5_rtN.js → main-4tyUgNqd.js} +3 -3
- package/dist/assets/{main-C4q5_rtN.js.map → main-4tyUgNqd.js.map} +1 -1
- package/dist/assets/{main-BNtEiK3o.js → main-Bbs84AcL.js} +77 -63
- package/dist/assets/main-Bbs84AcL.js.map +1 -0
- package/dist/assets/{main-BOBWcKWW.css → main-CNv6B3RZ.css} +597 -71
- package/dist/assets/react-vendor-DxnAFk-d.js +611 -0
- package/dist/assets/react-vendor-DxnAFk-d.js.map +1 -0
- package/dist/index.html +1 -1
- package/package.json +8 -4
- package/src/components/agent-prism/Avatar.tsx +164 -0
- package/src/components/agent-prism/Badge.tsx +109 -0
- package/src/components/agent-prism/Button.tsx +138 -0
- package/src/components/agent-prism/CollapseAndExpandControls.tsx +45 -0
- package/src/components/agent-prism/CollapsibleSection.tsx +121 -0
- package/src/components/agent-prism/DetailsView/DetailsView.tsx +141 -0
- package/src/components/agent-prism/DetailsView/DetailsViewAttributesTab.tsx +45 -0
- package/src/components/agent-prism/DetailsView/DetailsViewHeader.tsx +77 -0
- package/src/components/agent-prism/DetailsView/DetailsViewHeaderActions.tsx +21 -0
- package/src/components/agent-prism/DetailsView/DetailsViewInputOutputTab.tsx +210 -0
- package/src/components/agent-prism/DetailsView/DetailsViewMetrics.tsx +53 -0
- package/src/components/agent-prism/DetailsView/DetailsViewRawDataTab.tsx +24 -0
- package/src/components/agent-prism/IconButton.tsx +75 -0
- package/src/components/agent-prism/PriceBadge.tsx +12 -0
- package/src/components/agent-prism/SearchInput.tsx +17 -0
- package/src/components/agent-prism/SpanCard/SpanCard.tsx +467 -0
- package/src/components/agent-prism/SpanCard/SpanCardBadges.tsx +35 -0
- package/src/components/agent-prism/SpanCard/SpanCardConnector.tsx +36 -0
- package/src/components/agent-prism/SpanCard/SpanCardTimeline.tsx +60 -0
- package/src/components/agent-prism/SpanCard/SpanCardToggle.tsx +32 -0
- package/src/components/agent-prism/SpanStatus.tsx +79 -0
- package/src/components/agent-prism/Tabs.tsx +141 -0
- package/src/components/agent-prism/TextInput.tsx +142 -0
- package/src/components/agent-prism/TimestampBadge.tsx +28 -0
- package/src/components/agent-prism/TokensBadge.tsx +26 -0
- package/src/components/agent-prism/TraceList/TraceList.tsx +80 -0
- package/src/components/agent-prism/TraceList/TraceListItem.tsx +79 -0
- package/src/components/agent-prism/TraceList/TraceListItemHeader.tsx +46 -0
- package/src/components/agent-prism/TraceViewer.tsx +476 -0
- package/src/components/agent-prism/TreeView.tsx +57 -0
- package/src/components/agent-prism/shared.ts +210 -0
- package/src/components/agent-runs/AgentRunTimeline.tsx +64 -673
- package/src/components/agent-sidebar.tsx +2 -2
- package/src/components/chat.tsx +8 -8
- package/src/lib/agent-prism-utils.ts +46 -0
- package/src/lib/eliza-span-adapter.ts +487 -0
- package/dist/assets/main-BNtEiK3o.js.map +0 -1
- package/dist/assets/react-vendor-pe76PXQl.js +0 -546
- package/dist/assets/react-vendor-pe76PXQl.js.map +0 -1
|
@@ -1,700 +1,91 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cn } from '@/lib/utils';
|
|
1
|
+
import { useAgentRuns } from '@/hooks/use-query-hooks';
|
|
3
2
|
import type { UUID } from '@elizaos/core';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Database,
|
|
12
|
-
Eye,
|
|
13
|
-
XCircle,
|
|
14
|
-
Zap,
|
|
15
|
-
} from 'lucide-react';
|
|
16
|
-
import React, { useMemo, useState } from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import { elizaSpanAdapter } from '@/lib/eliza-span-adapter';
|
|
5
|
+
import { Loader2 } from 'lucide-react';
|
|
6
|
+
import { useQueries } from '@tanstack/react-query';
|
|
7
|
+
import { createElizaClient } from '@/lib/api-client-config';
|
|
8
|
+
import type { RunDetail } from '@elizaos/api-client';
|
|
9
|
+
import { TraceViewer, type TraceViewerData } from '../agent-prism/TraceViewer';
|
|
17
10
|
|
|
18
11
|
type AgentRunTimelineProps = {
|
|
19
12
|
agentId: UUID;
|
|
20
13
|
};
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
interface TimelineEvent {
|
|
25
|
-
id: string;
|
|
26
|
-
type:
|
|
27
|
-
| 'RUN_STARTED'
|
|
28
|
-
| 'RUN_ENDED'
|
|
29
|
-
| 'ACTION_STARTED'
|
|
30
|
-
| 'ACTION_COMPLETED'
|
|
31
|
-
| 'MODEL_USED'
|
|
32
|
-
| 'EVALUATOR_COMPLETED'
|
|
33
|
-
| 'EMBEDDING_EVENT';
|
|
34
|
-
timestamp: number;
|
|
35
|
-
duration?: number;
|
|
36
|
-
data: Record<string, unknown>;
|
|
37
|
-
parentId?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface ProcessedRun {
|
|
41
|
-
id: string;
|
|
42
|
-
name: string;
|
|
43
|
-
status: RunStatus;
|
|
44
|
-
startTime: number;
|
|
45
|
-
endTime?: number;
|
|
46
|
-
duration?: number;
|
|
47
|
-
children: ProcessedEvent[];
|
|
48
|
-
counts: {
|
|
49
|
-
actions: number;
|
|
50
|
-
modelCalls: number;
|
|
51
|
-
errors: number;
|
|
52
|
-
evaluators: number;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface ProcessedEvent {
|
|
57
|
-
id: string;
|
|
58
|
-
name: string;
|
|
59
|
-
type: 'action' | 'attempt' | 'model' | 'evaluator' | 'embedding';
|
|
60
|
-
status: 'completed' | 'failed' | 'running';
|
|
61
|
-
startTime: number;
|
|
62
|
-
duration?: number;
|
|
63
|
-
icon: React.ComponentType<{ className?: string }>;
|
|
64
|
-
attempts?: ProcessedEvent[];
|
|
65
|
-
children?: ProcessedEvent[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function formatDuration(durationMs?: number | null): string {
|
|
69
|
-
if (!durationMs || durationMs < 0) return '—';
|
|
70
|
-
if (durationMs < 1000) return `${durationMs.toFixed(2)}ms`;
|
|
71
|
-
const seconds = (durationMs / 1000).toFixed(2);
|
|
72
|
-
return `${seconds}s`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatTime(timestamp: number): string {
|
|
76
|
-
return new Date(timestamp).toLocaleTimeString('en-US', {
|
|
77
|
-
hour12: false,
|
|
78
|
-
hour: '2-digit',
|
|
79
|
-
minute: '2-digit',
|
|
80
|
-
second: '2-digit',
|
|
81
|
-
fractionalSecondDigits: 3,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function getStatusIcon(status: string) {
|
|
86
|
-
switch (status) {
|
|
87
|
-
case 'completed':
|
|
88
|
-
return CheckCircle;
|
|
89
|
-
case 'error':
|
|
90
|
-
case 'failed':
|
|
91
|
-
return XCircle;
|
|
92
|
-
case 'timeout':
|
|
93
|
-
return AlertCircle;
|
|
94
|
-
case 'running':
|
|
95
|
-
case 'started':
|
|
96
|
-
return Clock;
|
|
97
|
-
default:
|
|
98
|
-
return Activity;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function getStatusColor(status: string): string {
|
|
103
|
-
switch (status) {
|
|
104
|
-
case 'completed':
|
|
105
|
-
return 'text-green-700 bg-green-100 border-green-300 dark:text-green-400 dark:bg-green-900/30 dark:border-green-800';
|
|
106
|
-
case 'error':
|
|
107
|
-
case 'failed':
|
|
108
|
-
return 'text-red-700 bg-red-100 border-red-300 dark:text-red-400 dark:bg-red-900/30 dark:border-red-800';
|
|
109
|
-
case 'timeout':
|
|
110
|
-
return 'text-yellow-700 bg-yellow-100 border-yellow-300 dark:text-yellow-400 dark:bg-yellow-900/30 dark:border-yellow-800';
|
|
111
|
-
case 'running':
|
|
112
|
-
case 'started':
|
|
113
|
-
return 'text-blue-700 bg-blue-100 border-blue-300 dark:text-blue-400 dark:bg-blue-900/30 dark:border-blue-800';
|
|
114
|
-
default:
|
|
115
|
-
return 'text-muted-foreground bg-muted border-border';
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function getEventIcon(type: string) {
|
|
120
|
-
switch (type) {
|
|
121
|
-
case 'action':
|
|
122
|
-
return Activity;
|
|
123
|
-
case 'attempt':
|
|
124
|
-
return Clock;
|
|
125
|
-
case 'model':
|
|
126
|
-
return Eye;
|
|
127
|
-
case 'evaluator':
|
|
128
|
-
return Database;
|
|
129
|
-
case 'embedding':
|
|
130
|
-
return Zap;
|
|
131
|
-
default:
|
|
132
|
-
return Activity;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Removed page-level time scale to avoid confusing global durations across runs
|
|
15
|
+
const elizaClient = createElizaClient();
|
|
137
16
|
|
|
138
17
|
export const AgentRunTimeline: React.FC<AgentRunTimelineProps> = ({ agentId }) => {
|
|
139
|
-
const [selectedRunId, setSelectedRunId] = useState<UUID | null>(null);
|
|
140
|
-
const [expandedRuns, setExpandedRuns] = useState<Set<string>>(new Set());
|
|
141
|
-
|
|
142
18
|
const runsQuery = useAgentRuns(agentId);
|
|
143
19
|
const runs = runsQuery.data?.runs ?? [];
|
|
144
20
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const selectedRunSummary = useMemo(
|
|
155
|
-
() => runs.find((run) => run.runId === selectedRunId) ?? runs[0] ?? null,
|
|
156
|
-
[runs, selectedRunId]
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// Process run data into hierarchical structure
|
|
160
|
-
const processedRuns = useMemo((): ProcessedRun[] => {
|
|
161
|
-
if (runs.length === 0) {
|
|
162
|
-
return [];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return runs.map((runSummary) => {
|
|
166
|
-
// For now, create a processed run from the summary data
|
|
167
|
-
// We'll only show detailed events for the selected run
|
|
168
|
-
const isSelected = runSummary.runId === selectedRunId;
|
|
169
|
-
const runDetail = isSelected ? runDetailQuery.data : null;
|
|
170
|
-
|
|
171
|
-
let processedEvents: ProcessedEvent[] = [];
|
|
172
|
-
|
|
173
|
-
if (runDetail && runDetail.events) {
|
|
174
|
-
// Build grouped actions with attempts and nested events
|
|
175
|
-
const orderedEvents: ProcessedEvent[] = [];
|
|
176
|
-
const actionMap = new Map<string, ProcessedEvent>();
|
|
177
|
-
const inFlightAttempt = new Map<string, ProcessedEvent>();
|
|
178
|
-
|
|
179
|
-
const eventsSorted = [...runDetail.events].sort((a, b) => a.timestamp - b.timestamp);
|
|
180
|
-
|
|
181
|
-
eventsSorted.forEach((event, index) => {
|
|
182
|
-
switch (event.type) {
|
|
183
|
-
case 'ACTION_STARTED': {
|
|
184
|
-
const actionName =
|
|
185
|
-
(event.data.actionName as string) ||
|
|
186
|
-
(event.data.actionId as string) ||
|
|
187
|
-
`Action ${index}`;
|
|
188
|
-
const actionKey = (event.data.actionId as string) || actionName;
|
|
189
|
-
|
|
190
|
-
let actionEvent = actionMap.get(actionKey);
|
|
191
|
-
if (!actionEvent) {
|
|
192
|
-
actionEvent = {
|
|
193
|
-
id: `action-${actionKey}`,
|
|
194
|
-
name: actionName,
|
|
195
|
-
type: 'action',
|
|
196
|
-
status: 'running',
|
|
197
|
-
startTime: event.timestamp,
|
|
198
|
-
icon: Activity,
|
|
199
|
-
attempts: [],
|
|
200
|
-
};
|
|
201
|
-
actionMap.set(actionKey, actionEvent);
|
|
202
|
-
orderedEvents.push(actionEvent);
|
|
203
|
-
} else {
|
|
204
|
-
// For multiple attempts, keep earliest start
|
|
205
|
-
actionEvent.startTime = Math.min(actionEvent.startTime, event.timestamp);
|
|
206
|
-
actionEvent.status = 'running';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const attemptIndex = (actionEvent.attempts?.length || 0) + 1;
|
|
210
|
-
const attemptEvent: ProcessedEvent = {
|
|
211
|
-
id: `attempt-${actionKey}-${attemptIndex}`,
|
|
212
|
-
name: `Attempt ${attemptIndex}`,
|
|
213
|
-
type: 'attempt',
|
|
214
|
-
status: 'running',
|
|
215
|
-
startTime: event.timestamp,
|
|
216
|
-
icon: Clock,
|
|
217
|
-
attempts: [], // will hold nested child events like model calls
|
|
218
|
-
};
|
|
219
|
-
actionEvent.attempts = [...(actionEvent.attempts || []), attemptEvent];
|
|
220
|
-
inFlightAttempt.set(actionKey, attemptEvent);
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
case 'ACTION_COMPLETED': {
|
|
224
|
-
const actionName =
|
|
225
|
-
(event.data.actionName as string) ||
|
|
226
|
-
(event.data.actionId as string) ||
|
|
227
|
-
`Action ${index}`;
|
|
228
|
-
const actionKey = (event.data.actionId as string) || actionName;
|
|
229
|
-
let actionEvent = actionMap.get(actionKey);
|
|
230
|
-
if (!actionEvent) {
|
|
231
|
-
// If we missed the start, create a placeholder action with a single attempt
|
|
232
|
-
actionEvent = {
|
|
233
|
-
id: `action-${actionKey}`,
|
|
234
|
-
name: actionName,
|
|
235
|
-
type: 'action',
|
|
236
|
-
status: 'running',
|
|
237
|
-
startTime: event.timestamp,
|
|
238
|
-
icon: Activity,
|
|
239
|
-
attempts: [],
|
|
240
|
-
};
|
|
241
|
-
actionMap.set(actionKey, actionEvent);
|
|
242
|
-
orderedEvents.push(actionEvent);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
let attempt = inFlightAttempt.get(actionKey);
|
|
246
|
-
if (!attempt) {
|
|
247
|
-
// Missing start; synthesize an attempt starting at completion time
|
|
248
|
-
attempt = {
|
|
249
|
-
id: `attempt-${actionKey}-1`,
|
|
250
|
-
name: 'Attempt 1',
|
|
251
|
-
type: 'attempt',
|
|
252
|
-
status: 'running',
|
|
253
|
-
startTime: event.timestamp,
|
|
254
|
-
icon: Clock,
|
|
255
|
-
attempts: [],
|
|
256
|
-
};
|
|
257
|
-
actionEvent.attempts = [...(actionEvent.attempts || []), attempt];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const success = (event.data.success as boolean | undefined) !== false;
|
|
261
|
-
attempt.duration = Math.max(0, event.timestamp - attempt.startTime);
|
|
262
|
-
attempt.status = success ? 'completed' : 'failed';
|
|
263
|
-
inFlightAttempt.delete(actionKey);
|
|
264
|
-
|
|
265
|
-
actionEvent.status = success ? 'completed' : 'failed';
|
|
266
|
-
const firstAttemptStart = (actionEvent.attempts || [attempt])[0].startTime;
|
|
267
|
-
actionEvent.duration = Math.max(0, event.timestamp - firstAttemptStart);
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
case 'MODEL_USED': {
|
|
271
|
-
const modelType = (event.data.modelType as string) || 'Model Call';
|
|
272
|
-
const modelEvent: ProcessedEvent = {
|
|
273
|
-
id: `model-${index}`,
|
|
274
|
-
name: modelType,
|
|
275
|
-
type: 'model',
|
|
276
|
-
status: 'completed',
|
|
277
|
-
startTime: event.timestamp,
|
|
278
|
-
duration: (event.data.executionTime as number) || undefined,
|
|
279
|
-
icon: Eye,
|
|
280
|
-
};
|
|
281
|
-
const actionContext = (event.data.actionContext as string | undefined) || undefined;
|
|
282
|
-
const targetKey = actionContext || Array.from(inFlightAttempt.keys()).pop();
|
|
283
|
-
if (targetKey) {
|
|
284
|
-
const attempt = inFlightAttempt.get(targetKey);
|
|
285
|
-
if (attempt) {
|
|
286
|
-
attempt.attempts = [...(attempt.attempts || []), modelEvent];
|
|
287
|
-
} else {
|
|
288
|
-
// If no running attempt, attach to the last attempt of the action
|
|
289
|
-
const actionEvent = actionMap.get(targetKey);
|
|
290
|
-
const lastAttempt = actionEvent?.attempts && actionEvent.attempts[actionEvent.attempts.length - 1];
|
|
291
|
-
if (lastAttempt) {
|
|
292
|
-
lastAttempt.attempts = [...(lastAttempt.attempts || []), modelEvent];
|
|
293
|
-
} else {
|
|
294
|
-
orderedEvents.push(modelEvent);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
orderedEvents.push(modelEvent);
|
|
299
|
-
}
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
case 'EVALUATOR_COMPLETED': {
|
|
303
|
-
const evaluatorName = (event.data.evaluatorName as string) || `Evaluator ${index}`;
|
|
304
|
-
orderedEvents.push({
|
|
305
|
-
id: `evaluator-${index}`,
|
|
306
|
-
name: evaluatorName,
|
|
307
|
-
type: 'evaluator',
|
|
308
|
-
status: 'completed',
|
|
309
|
-
startTime: event.timestamp,
|
|
310
|
-
icon: Database,
|
|
311
|
-
});
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
case 'EMBEDDING_EVENT': {
|
|
315
|
-
const status = (event.data.status as string) || 'completed';
|
|
316
|
-
const embeddingEvent: ProcessedEvent = {
|
|
317
|
-
id: `embedding-${index}`,
|
|
318
|
-
name: `Embedding ${status}`,
|
|
319
|
-
type: 'embedding',
|
|
320
|
-
status: status === 'failed' ? 'failed' : 'completed',
|
|
321
|
-
startTime: event.timestamp,
|
|
322
|
-
duration: (event.data.durationMs as number) || undefined,
|
|
323
|
-
icon: Zap,
|
|
324
|
-
};
|
|
325
|
-
const key = Array.from(inFlightAttempt.keys()).pop();
|
|
326
|
-
if (key) {
|
|
327
|
-
const attempt = inFlightAttempt.get(key);
|
|
328
|
-
if (attempt) {
|
|
329
|
-
attempt.attempts = [...(attempt.attempts || []), embeddingEvent];
|
|
330
|
-
} else {
|
|
331
|
-
orderedEvents.push(embeddingEvent);
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
orderedEvents.push(embeddingEvent);
|
|
335
|
-
}
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
default:
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
processedEvents = orderedEvents;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
id: runSummary.runId,
|
|
348
|
-
name: `Run ${formatTime(runSummary.startedAt || Date.now())}`,
|
|
349
|
-
status: runSummary.status as RunStatus,
|
|
350
|
-
startTime: runSummary.startedAt || Date.now(),
|
|
351
|
-
endTime: runSummary.endedAt || undefined,
|
|
352
|
-
duration:
|
|
353
|
-
runSummary.durationMs ??
|
|
354
|
-
(runSummary.endedAt != null && runSummary.startedAt != null
|
|
355
|
-
? runSummary.endedAt - runSummary.startedAt
|
|
356
|
-
: undefined),
|
|
357
|
-
children: processedEvents.sort((a, b) => a.startTime - b.startTime),
|
|
358
|
-
counts: runSummary.counts || { actions: 0, modelCalls: 0, errors: 0, evaluators: 0 },
|
|
359
|
-
};
|
|
360
|
-
});
|
|
361
|
-
}, [runs, runDetailQuery.data, selectedRunId]);
|
|
362
|
-
|
|
363
|
-
// Helper function to calculate timeline bounds for a single run
|
|
364
|
-
const calculateRunTimelineBounds = (run: ProcessedRun) => {
|
|
365
|
-
let earliestStart = run.startTime;
|
|
366
|
-
let latestEnd = run.endTime || run.startTime + (run.duration || 0);
|
|
367
|
-
|
|
368
|
-
const scanEvent = (ev: ProcessedEvent) => {
|
|
369
|
-
earliestStart = Math.min(earliestStart, ev.startTime);
|
|
370
|
-
const end = ev.startTime + (ev.duration || 0);
|
|
371
|
-
latestEnd = Math.max(latestEnd, end);
|
|
372
|
-
(ev.attempts || []).forEach(scanEvent);
|
|
373
|
-
(ev.children || []).forEach(scanEvent);
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
run.children.forEach(scanEvent);
|
|
377
|
-
|
|
378
|
-
const totalDuration = latestEnd - earliestStart;
|
|
379
|
-
|
|
380
|
-
// Ensure we have at least a minimal duration for visualization
|
|
381
|
-
const minDuration = 100; // 100ms minimum for visualization
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
startTime: earliestStart,
|
|
385
|
-
endTime: latestEnd,
|
|
386
|
-
totalDuration: Math.max(totalDuration, minDuration),
|
|
387
|
-
};
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// Toggle expansion of runs
|
|
391
|
-
const toggleRunExpansion = (runId: string) => {
|
|
392
|
-
const newExpanded = new Set(expandedRuns);
|
|
393
|
-
if (newExpanded.has(runId)) {
|
|
394
|
-
newExpanded.delete(runId);
|
|
395
|
-
} else {
|
|
396
|
-
newExpanded.add(runId);
|
|
397
|
-
}
|
|
398
|
-
setExpandedRuns(newExpanded);
|
|
399
|
-
};
|
|
21
|
+
// Fetch details for all runs using useQueries to avoid hook rule violations
|
|
22
|
+
const runDetailQueries = useQueries({
|
|
23
|
+
queries: runs.map((run) => ({
|
|
24
|
+
queryKey: ['agent', agentId, 'runs', 'detail', run.runId, null],
|
|
25
|
+
queryFn: async () => elizaClient.runs.getRun(agentId, run.runId),
|
|
26
|
+
enabled: Boolean(agentId && run.runId),
|
|
27
|
+
staleTime: 30000,
|
|
28
|
+
})),
|
|
29
|
+
});
|
|
400
30
|
|
|
401
|
-
|
|
31
|
+
// Convert ElizaOS runs to Agent Prism format
|
|
32
|
+
const traceViewerData: TraceViewerData[] = useMemo(() => {
|
|
33
|
+
return runs
|
|
34
|
+
.map((run, index) => {
|
|
35
|
+
const detailQuery = runDetailQueries[index];
|
|
36
|
+
if (!detailQuery?.data) return null;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
traceRecord: elizaSpanAdapter.convertRunSummaryToTraceRecord(run),
|
|
40
|
+
spans: elizaSpanAdapter.convertRunDetailToTraceSpans(detailQuery.data as RunDetail),
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
.filter((item): item is TraceViewerData => item !== null);
|
|
44
|
+
}, [runs, ...runDetailQueries.map(q => q.data)]);
|
|
45
|
+
|
|
46
|
+
const isLoading = runsQuery.isLoading;
|
|
402
47
|
const errorMessage = runsQuery.error ? (runsQuery.error as Error).message : undefined;
|
|
403
48
|
const hasRuns = runs.length > 0;
|
|
49
|
+
const hasData = traceViewerData.length > 0;
|
|
404
50
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<div className="flex
|
|
408
|
-
<
|
|
409
|
-
<h2 className="text-lg font-semibold">Agent Runs</h2>
|
|
410
|
-
<p className="text-sm text-muted-foreground">
|
|
411
|
-
Hierarchical view of agent execution with timing details.
|
|
412
|
-
</p>
|
|
413
|
-
</div>
|
|
414
|
-
{/* Intentionally no global time scale; each row shows its own proportional bar */}
|
|
51
|
+
if (isLoading) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex items-center justify-center h-32">
|
|
54
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
415
55
|
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
416
58
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
<div className="px-4 py-8 text-sm text-muted-foreground">No runs available yet.</div>
|
|
422
|
-
)}
|
|
423
|
-
|
|
424
|
-
{processedRuns.length > 0 && (
|
|
425
|
-
<div className="space-y-2">
|
|
426
|
-
{processedRuns.map((run) => (
|
|
427
|
-
<div key={run.id}>
|
|
428
|
-
<RunItem
|
|
429
|
-
run={run}
|
|
430
|
-
isExpanded={expandedRuns.has(run.id)}
|
|
431
|
-
isSelected={selectedRunId === run.id}
|
|
432
|
-
onToggle={() => {
|
|
433
|
-
toggleRunExpansion(run.id);
|
|
434
|
-
// Also set this as the selected run when expanded
|
|
435
|
-
if (!expandedRuns.has(run.id)) {
|
|
436
|
-
setSelectedRunId(run.id as UUID);
|
|
437
|
-
}
|
|
438
|
-
}}
|
|
439
|
-
level={0}
|
|
440
|
-
timelineBounds={calculateRunTimelineBounds(run)}
|
|
441
|
-
/>
|
|
442
|
-
</div>
|
|
443
|
-
))}
|
|
444
|
-
</div>
|
|
445
|
-
)}
|
|
446
|
-
</div>
|
|
447
|
-
);
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
// RunItem component for hierarchical display
|
|
451
|
-
interface RunItemProps {
|
|
452
|
-
run: ProcessedRun;
|
|
453
|
-
isExpanded: boolean;
|
|
454
|
-
onToggle: () => void;
|
|
455
|
-
level: number;
|
|
456
|
-
isSelected?: boolean;
|
|
457
|
-
timelineBounds: {
|
|
458
|
-
startTime: number;
|
|
459
|
-
endTime: number;
|
|
460
|
-
totalDuration: number;
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const RunItem: React.FC<RunItemProps> = ({
|
|
465
|
-
run,
|
|
466
|
-
isExpanded,
|
|
467
|
-
onToggle,
|
|
468
|
-
level,
|
|
469
|
-
isSelected,
|
|
470
|
-
timelineBounds,
|
|
471
|
-
}) => {
|
|
472
|
-
const StatusIcon = getStatusIcon(run.status);
|
|
473
|
-
const indent = level * 24;
|
|
474
|
-
|
|
475
|
-
// Calculate timing bar parameters based on timeline bounds
|
|
476
|
-
// For the run itself, we want the bar to span the full width since it represents the entire timeline
|
|
477
|
-
const { startTime: timelineStart, totalDuration: timelineTotal } = timelineBounds;
|
|
478
|
-
const runDuration = run.duration || 0;
|
|
479
|
-
|
|
480
|
-
// For a run at the top level, the bar should show the full duration
|
|
481
|
-
const isRootRun = level === 0;
|
|
482
|
-
const startOffset = isRootRun ? 0 :
|
|
483
|
-
(timelineTotal > 0 ? ((run.startTime - timelineStart) / timelineTotal) * 100 : 0);
|
|
484
|
-
const widthPercent = isRootRun ? 100 :
|
|
485
|
-
(timelineTotal > 0 ? (runDuration / timelineTotal) * 100 : 0);
|
|
486
|
-
|
|
487
|
-
return (
|
|
488
|
-
<div>
|
|
489
|
-
{/* Main run row */}
|
|
490
|
-
<div
|
|
491
|
-
className={cn(
|
|
492
|
-
'flex items-center gap-3 p-3 cursor-pointer transition-colors',
|
|
493
|
-
isRootRun ? 'bg-card border rounded-lg' : '',
|
|
494
|
-
isSelected && !isRootRun && 'bg-primary/10'
|
|
495
|
-
)}
|
|
496
|
-
style={{ paddingLeft: `${12 + indent}px` }}
|
|
497
|
-
onClick={onToggle}
|
|
498
|
-
>
|
|
499
|
-
{/* Expand/collapse button */}
|
|
500
|
-
<button className="flex-shrink-0 w-4 h-4 flex items-center justify-center">
|
|
501
|
-
{run.children.length > 0 &&
|
|
502
|
-
(isExpanded ? (
|
|
503
|
-
<ChevronDown className="w-3 h-3" />
|
|
504
|
-
) : (
|
|
505
|
-
<ChevronRight className="w-3 h-3" />
|
|
506
|
-
))}
|
|
507
|
-
</button>
|
|
508
|
-
|
|
509
|
-
{/* Status icon */}
|
|
510
|
-
<div
|
|
511
|
-
className={cn(
|
|
512
|
-
'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs',
|
|
513
|
-
getStatusColor(run.status)
|
|
514
|
-
)}
|
|
515
|
-
>
|
|
516
|
-
<StatusIcon className="w-3 h-3" />
|
|
517
|
-
</div>
|
|
518
|
-
|
|
519
|
-
{/* Task name and details */}
|
|
520
|
-
<div className="flex-1 min-w-0">
|
|
521
|
-
<div className="flex items-center gap-2">
|
|
522
|
-
<span className="font-medium text-sm">{run.name}</span>
|
|
523
|
-
<span
|
|
524
|
-
className={cn(
|
|
525
|
-
'px-1.5 py-0.5 text-xs rounded-full border',
|
|
526
|
-
getStatusColor(run.status)
|
|
527
|
-
)}
|
|
528
|
-
>
|
|
529
|
-
{run.status === 'completed' ? '✓' : run.status === 'error' ? '✗' : '○'}
|
|
530
|
-
</span>
|
|
531
|
-
</div>
|
|
532
|
-
<div className="text-xs text-muted-foreground mt-1">
|
|
533
|
-
{run.counts.actions} actions • {run.counts.modelCalls} model calls • {run.counts.errors}{' '}
|
|
534
|
-
errors
|
|
535
|
-
</div>
|
|
536
|
-
</div>
|
|
537
|
-
|
|
538
|
-
{/* Timing bar with integrated duration display */}
|
|
539
|
-
<div className="flex-shrink-0 w-40 h-5 relative bg-muted rounded-none overflow-hidden">
|
|
540
|
-
<div
|
|
541
|
-
className={cn(
|
|
542
|
-
'absolute top-0 bottom-0 transition-all flex items-center justify-center',
|
|
543
|
-
run.status === 'completed'
|
|
544
|
-
? 'bg-primary'
|
|
545
|
-
: run.status === 'error'
|
|
546
|
-
? 'bg-destructive'
|
|
547
|
-
: run.status === 'timeout'
|
|
548
|
-
? 'bg-yellow-500 dark:bg-yellow-600'
|
|
549
|
-
: 'bg-primary/70'
|
|
550
|
-
)}
|
|
551
|
-
style={{
|
|
552
|
-
left: `${Math.max(0, Math.min(startOffset, 98))}%`,
|
|
553
|
-
width: `${Math.max(2, Math.min(widthPercent, 100 - Math.max(0, Math.min(startOffset, 98))))}%`,
|
|
554
|
-
}}
|
|
555
|
-
>
|
|
556
|
-
{/* Show duration on the bar for parent runs */}
|
|
557
|
-
{isRootRun && (
|
|
558
|
-
<span className="text-xs font-mono text-primary-foreground px-1 truncate">
|
|
559
|
-
{formatDuration(run.duration)}
|
|
560
|
-
</span>
|
|
561
|
-
)}
|
|
562
|
-
</div>
|
|
563
|
-
{/* Show duration outside for child runs */}
|
|
564
|
-
{!isRootRun && (
|
|
565
|
-
<div className="absolute right-1 top-0 bottom-0 flex items-center">
|
|
566
|
-
<span className="text-xs font-mono text-muted-foreground">
|
|
567
|
-
{formatDuration(run.duration)}
|
|
568
|
-
</span>
|
|
569
|
-
</div>
|
|
570
|
-
)}
|
|
571
|
-
</div>
|
|
59
|
+
if (errorMessage) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="px-4 py-3 text-sm text-destructive">
|
|
62
|
+
Failed to load runs: {errorMessage}
|
|
572
63
|
</div>
|
|
573
|
-
|
|
574
|
-
{/* Children */}
|
|
575
|
-
{isExpanded && run.children.length > 0 && (
|
|
576
|
-
<div className={cn(
|
|
577
|
-
"border-l border-border ml-4",
|
|
578
|
-
isRootRun ? "mt-2 pt-2" : ""
|
|
579
|
-
)}>
|
|
580
|
-
{run.children.map((child) => (
|
|
581
|
-
<EventItem
|
|
582
|
-
key={child.id}
|
|
583
|
-
event={child}
|
|
584
|
-
level={level + 1}
|
|
585
|
-
timelineBounds={timelineBounds}
|
|
586
|
-
/>
|
|
587
|
-
))}
|
|
588
|
-
</div>
|
|
589
|
-
)}
|
|
590
|
-
</div>
|
|
591
|
-
);
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
// EventItem component for individual events
|
|
595
|
-
interface EventItemProps {
|
|
596
|
-
event: ProcessedEvent;
|
|
597
|
-
level: number;
|
|
598
|
-
timelineBounds: {
|
|
599
|
-
startTime: number;
|
|
600
|
-
endTime: number;
|
|
601
|
-
totalDuration: number;
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const EventItem: React.FC<EventItemProps> = ({ event, level, timelineBounds }) => {
|
|
606
|
-
const IconComponent = event.icon;
|
|
607
|
-
const StatusIcon = getStatusIcon(event.status);
|
|
608
|
-
const indent = level * 24;
|
|
609
|
-
const [expanded, setExpanded] = React.useState(true);
|
|
610
|
-
const hasNested = (event.attempts && event.attempts.length > 0) || (event.children && event.children.length > 0);
|
|
611
|
-
|
|
612
|
-
// Calculate timing bar parameters based on timeline bounds
|
|
613
|
-
const { startTime: timelineStart, totalDuration: timelineTotal } = timelineBounds;
|
|
614
|
-
const eventDuration = event.duration || 0;
|
|
615
|
-
const startOffset =
|
|
616
|
-
timelineTotal > 0 ? ((event.startTime - timelineStart) / timelineTotal) * 100 : 0;
|
|
617
|
-
|
|
618
|
-
// Calculate width with a minimum visible size
|
|
619
|
-
// Use logarithmic scaling for very small durations to maintain visibility
|
|
620
|
-
let widthPercent = 0;
|
|
621
|
-
if (timelineTotal > 0 && eventDuration > 0) {
|
|
622
|
-
const rawPercent = (eventDuration / timelineTotal) * 100;
|
|
623
|
-
// Ensure minimum visibility of 5% for any event with duration
|
|
624
|
-
widthPercent = Math.max(5, rawPercent);
|
|
625
|
-
// For very small events (< 2%), apply scaling to make them more visible
|
|
626
|
-
if (rawPercent < 2) {
|
|
627
|
-
widthPercent = 3 + (rawPercent * 2); // Scale up small events
|
|
628
|
-
}
|
|
64
|
+
);
|
|
629
65
|
}
|
|
630
66
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
<div
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
<button className="w-4 h-4 flex items-center justify-center" onClick={() => hasNested && setExpanded(!expanded)}>
|
|
639
|
-
{hasNested ? (expanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />) : null}
|
|
640
|
-
</button>
|
|
641
|
-
|
|
642
|
-
{/* Event icon */}
|
|
643
|
-
<div className="flex-shrink-0 w-4 h-4 flex items-center justify-center">
|
|
644
|
-
<IconComponent className="w-3 h-3 text-muted-foreground" />
|
|
645
|
-
</div>
|
|
646
|
-
|
|
647
|
-
{/* Status icon */}
|
|
648
|
-
<div
|
|
649
|
-
className={cn(
|
|
650
|
-
'flex-shrink-0 w-4 h-4 rounded-sm flex items-center justify-center text-xs',
|
|
651
|
-
getStatusColor(event.status)
|
|
652
|
-
)}
|
|
653
|
-
>
|
|
654
|
-
<StatusIcon className="w-2.5 h-2.5" />
|
|
655
|
-
</div>
|
|
656
|
-
|
|
657
|
-
{/* Event name */}
|
|
658
|
-
<div className="flex-1 min-w-0">
|
|
659
|
-
<span className="text-sm">{event.name}</span>
|
|
660
|
-
</div>
|
|
67
|
+
if (!hasRuns) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="px-4 py-8 text-sm text-center text-muted-foreground">
|
|
70
|
+
No agent runs yet. Runs will appear here after the agent processes messages.
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
661
74
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
event.status === 'completed'
|
|
668
|
-
? 'bg-primary/80'
|
|
669
|
-
: event.status === 'failed'
|
|
670
|
-
? 'bg-destructive/80'
|
|
671
|
-
: 'bg-primary/60'
|
|
672
|
-
)}
|
|
673
|
-
style={{
|
|
674
|
-
left: `${Math.max(0, Math.min(startOffset, 98))}%`,
|
|
675
|
-
width: `${Math.max(3, Math.min(widthPercent, 100 - Math.max(0, Math.min(startOffset, 98))))}%`,
|
|
676
|
-
}}
|
|
677
|
-
/>
|
|
678
|
-
{/* Duration text overlay */}
|
|
679
|
-
<div className="absolute right-1 text-xs font-mono text-muted-foreground z-10">
|
|
680
|
-
{formatDuration(event.duration)}
|
|
681
|
-
</div>
|
|
682
|
-
</div>
|
|
75
|
+
if (!hasData) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex items-center justify-center h-32">
|
|
78
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
79
|
+
<span className="ml-2 text-sm text-muted-foreground">Loading run details...</span>
|
|
683
80
|
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
684
83
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
{(event.attempts || []).map((child) => (
|
|
689
|
-
<EventItem key={child.id} event={child} level={level + 1} timelineBounds={timelineBounds} />
|
|
690
|
-
))}
|
|
691
|
-
{(event.children || []).map((child) => (
|
|
692
|
-
<EventItem key={child.id} event={child} level={level + 1} timelineBounds={timelineBounds} />
|
|
693
|
-
))}
|
|
694
|
-
</div>
|
|
695
|
-
)}
|
|
84
|
+
return (
|
|
85
|
+
<div className="h-full w-full">
|
|
86
|
+
<TraceViewer data={traceViewerData} />
|
|
696
87
|
</div>
|
|
697
88
|
);
|
|
698
89
|
};
|
|
699
90
|
|
|
700
|
-
export default AgentRunTimeline;
|
|
91
|
+
export default AgentRunTimeline;
|