@ai-sdk/devtools 1.0.0-beta.9 → 1.0.0-canary.20
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/client/assets/index-B1quREL_.css +1 -0
- package/dist/client/assets/index-DYruJ8X0.js +181 -0
- package/dist/client/index.html +2 -2
- package/dist/index.d.ts +19 -1
- package/dist/index.js +342 -3
- package/dist/viewer/server.js +42 -5
- package/package.json +28 -4
- package/src/db.ts +32 -5
- package/src/index.ts +1 -0
- package/src/integration.ts +509 -0
- package/src/middleware.ts +5 -5
- package/src/viewer/client/app.tsx +97 -1878
- package/src/viewer/client/components/message-components.tsx +342 -0
- package/src/viewer/client/components/output-components.tsx +145 -0
- package/src/viewer/client/components/shared-components.tsx +691 -0
- package/src/viewer/client/components/step-card.tsx +472 -0
- package/src/viewer/client/components/trace-timeline.tsx +529 -0
- package/src/viewer/client/components/ui/badge.tsx +0 -1
- package/src/viewer/client/components/ui/button.tsx +0 -1
- package/src/viewer/client/types.ts +183 -0
- package/src/viewer/client/utils.ts +708 -0
- package/src/viewer/server.ts +49 -5
- package/dist/client/assets/index-BkyOIjbt.css +0 -1
- package/dist/client/assets/index-BwqXGbSV.js +0 -176
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StepSummary,
|
|
3
|
+
StepInputSummary,
|
|
4
|
+
InputTokenBreakdown,
|
|
5
|
+
OutputTokenBreakdown,
|
|
6
|
+
TraceSpan,
|
|
7
|
+
RunDetail,
|
|
8
|
+
ChildRun,
|
|
9
|
+
Step,
|
|
10
|
+
SpanKind,
|
|
11
|
+
ParsedInput,
|
|
12
|
+
ParsedOutput,
|
|
13
|
+
ParsedUsage,
|
|
14
|
+
ContentPart,
|
|
15
|
+
ToolCallContentPart,
|
|
16
|
+
PromptMessage,
|
|
17
|
+
ParseJson,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
export function summarizeToolCalls(toolCalls: ToolCallContentPart[]): {
|
|
21
|
+
label: string;
|
|
22
|
+
details: string;
|
|
23
|
+
} {
|
|
24
|
+
const counts = toolCalls.reduce((acc: Record<string, number>, call) => {
|
|
25
|
+
acc[call.toolName] = (acc[call.toolName] || 0) + 1;
|
|
26
|
+
return acc;
|
|
27
|
+
}, {});
|
|
28
|
+
|
|
29
|
+
const uniqueTools = Object.keys(counts);
|
|
30
|
+
|
|
31
|
+
const formatTool = (name: string) => {
|
|
32
|
+
const count = counts[name] ?? 0;
|
|
33
|
+
return count > 1 ? `${name} (x${count})` : name;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const allToolsFormatted = uniqueTools.map(formatTool);
|
|
37
|
+
|
|
38
|
+
if (uniqueTools.length === 1 && uniqueTools[0]) {
|
|
39
|
+
return {
|
|
40
|
+
label: formatTool(uniqueTools[0]),
|
|
41
|
+
details: '',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (uniqueTools.length === 2) {
|
|
46
|
+
return {
|
|
47
|
+
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}`,
|
|
48
|
+
details: allToolsFormatted.join(', '),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}, ...`,
|
|
54
|
+
details: allToolsFormatted.join(', '),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function truncateText(text: string, maxLength: number = 30): string {
|
|
59
|
+
if (text.length <= maxLength) return text;
|
|
60
|
+
return text.slice(0, maxLength).trim() + '…';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractTextFromMessage(message: PromptMessage): string | null {
|
|
64
|
+
const { content } = message;
|
|
65
|
+
if (typeof content === 'string') return content;
|
|
66
|
+
if (Array.isArray(content)) {
|
|
67
|
+
const textPart = content.find(
|
|
68
|
+
(part): part is { type: 'text'; text: string } => part.type === 'text',
|
|
69
|
+
);
|
|
70
|
+
return textPart?.text ?? null;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getStepInputSummary(
|
|
76
|
+
input: ParsedInput | null,
|
|
77
|
+
isFirstStep: boolean,
|
|
78
|
+
): StepInputSummary | null {
|
|
79
|
+
const prompt = input?.prompt;
|
|
80
|
+
if (!Array.isArray(prompt)) return null;
|
|
81
|
+
|
|
82
|
+
if (isFirstStep) {
|
|
83
|
+
const userMessages = prompt.filter(msg => msg.role === 'user');
|
|
84
|
+
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
85
|
+
|
|
86
|
+
if (lastUserMessage) {
|
|
87
|
+
const text = extractTextFromMessage(lastUserMessage);
|
|
88
|
+
if (text) {
|
|
89
|
+
return {
|
|
90
|
+
type: 'user',
|
|
91
|
+
label: `"${truncateText(text)}"`,
|
|
92
|
+
fullText: text,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const toolMessages = prompt.filter(msg => msg.role === 'tool');
|
|
100
|
+
if (toolMessages.length > 0) {
|
|
101
|
+
const lastToolMessage = toolMessages[toolMessages.length - 1];
|
|
102
|
+
const toolCounts: Record<string, number> = {};
|
|
103
|
+
|
|
104
|
+
const content = lastToolMessage?.content;
|
|
105
|
+
if (Array.isArray(content)) {
|
|
106
|
+
for (const part of content) {
|
|
107
|
+
if (
|
|
108
|
+
part.type === 'tool-result' &&
|
|
109
|
+
'toolName' in part &&
|
|
110
|
+
part.toolName
|
|
111
|
+
) {
|
|
112
|
+
toolCounts[part.toolName] = (toolCounts[part.toolName] || 0) + 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const uniqueTools = Object.keys(toolCounts);
|
|
118
|
+
if (uniqueTools.length === 0) {
|
|
119
|
+
return { type: 'tool', label: 'tool result' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const formatTool = (name: string) => {
|
|
123
|
+
const count = toolCounts[name];
|
|
124
|
+
return count != null && count > 1 ? `${name} (x${count})` : name;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const allToolsFormatted = uniqueTools.map(formatTool);
|
|
128
|
+
|
|
129
|
+
if (uniqueTools.length === 1) {
|
|
130
|
+
return { type: 'tool', label: formatTool(uniqueTools[0]) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (uniqueTools.length === 2) {
|
|
134
|
+
return {
|
|
135
|
+
type: 'tool',
|
|
136
|
+
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}`,
|
|
137
|
+
toolDetails: allToolsFormatted.join(', '),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
type: 'tool',
|
|
143
|
+
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}, ...`,
|
|
144
|
+
toolDetails: allToolsFormatted.join(', '),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const userMessages = prompt.filter(msg => msg.role === 'user');
|
|
149
|
+
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
150
|
+
|
|
151
|
+
if (!lastUserMessage) return null;
|
|
152
|
+
|
|
153
|
+
const text = extractTextFromMessage(lastUserMessage);
|
|
154
|
+
if (!text) return null;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
type: 'user',
|
|
158
|
+
label: `"${truncateText(text)}"`,
|
|
159
|
+
fullText: text,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getStepSummary(
|
|
164
|
+
output: ParsedOutput | null,
|
|
165
|
+
error: string | null,
|
|
166
|
+
): StepSummary {
|
|
167
|
+
if (error) {
|
|
168
|
+
return { type: 'error', icon: 'alert', label: 'Error' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const finishReason =
|
|
172
|
+
typeof output?.finishReason === 'string'
|
|
173
|
+
? output.finishReason
|
|
174
|
+
: typeof output?.finishReason === 'object'
|
|
175
|
+
? output.finishReason?.unified
|
|
176
|
+
: undefined;
|
|
177
|
+
|
|
178
|
+
if (finishReason === 'tool-calls') {
|
|
179
|
+
const toolCalls: ToolCallContentPart[] =
|
|
180
|
+
output?.toolCalls ||
|
|
181
|
+
(output?.content?.filter(
|
|
182
|
+
(p): p is ToolCallContentPart => p.type === 'tool-call',
|
|
183
|
+
) ??
|
|
184
|
+
[]);
|
|
185
|
+
const { label, details } = summarizeToolCalls(toolCalls);
|
|
186
|
+
return {
|
|
187
|
+
type: 'tool-calls',
|
|
188
|
+
icon: 'wrench',
|
|
189
|
+
label,
|
|
190
|
+
toolDetails: details || undefined,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { type: 'response', icon: 'message', label: 'Response' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function safeParseJson(value: unknown): unknown {
|
|
198
|
+
if (typeof value === 'string') {
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(value);
|
|
201
|
+
} catch {
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Token usage can be either a number (old format) or an object with breakdown (ai@6.0.0-beta.139+).
|
|
210
|
+
* Old format: inputTokens: 4456
|
|
211
|
+
* New format for input: { total: 4456, noCache: 9, cacheRead: 4100, cacheWrite: 347 }
|
|
212
|
+
* New format for output: { total: 1262, text: 1262, reasoning: 0 }
|
|
213
|
+
*/
|
|
214
|
+
export function getInputTokenBreakdown(
|
|
215
|
+
tokens: number | InputTokenBreakdown | null | undefined,
|
|
216
|
+
): InputTokenBreakdown {
|
|
217
|
+
if (tokens == null) return { total: 0 };
|
|
218
|
+
if (typeof tokens === 'number') return { total: tokens };
|
|
219
|
+
if (typeof tokens === 'object') {
|
|
220
|
+
const total =
|
|
221
|
+
'total' in tokens && typeof tokens.total === 'number' ? tokens.total : 0;
|
|
222
|
+
return {
|
|
223
|
+
total,
|
|
224
|
+
...(typeof tokens.noCache === 'number' && { noCache: tokens.noCache }),
|
|
225
|
+
...(typeof tokens.cacheRead === 'number' && {
|
|
226
|
+
cacheRead: tokens.cacheRead,
|
|
227
|
+
}),
|
|
228
|
+
...(typeof tokens.cacheWrite === 'number' && {
|
|
229
|
+
cacheWrite: tokens.cacheWrite,
|
|
230
|
+
}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { total: 0 };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getOutputTokenBreakdown(
|
|
237
|
+
tokens: number | OutputTokenBreakdown | null | undefined,
|
|
238
|
+
): OutputTokenBreakdown {
|
|
239
|
+
if (tokens == null) return { total: 0 };
|
|
240
|
+
if (typeof tokens === 'number') return { total: tokens };
|
|
241
|
+
if (typeof tokens === 'object') {
|
|
242
|
+
const total =
|
|
243
|
+
'total' in tokens && typeof tokens.total === 'number' ? tokens.total : 0;
|
|
244
|
+
return {
|
|
245
|
+
total,
|
|
246
|
+
...(typeof tokens.text === 'number' && { text: tokens.text }),
|
|
247
|
+
...(typeof tokens.reasoning === 'number' && {
|
|
248
|
+
reasoning: tokens.reasoning,
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return { total: 0 };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function formatInputTokens(breakdown: InputTokenBreakdown): string {
|
|
256
|
+
const { total, cacheRead } = breakdown;
|
|
257
|
+
if (cacheRead && cacheRead > 0) {
|
|
258
|
+
return `${total} (${cacheRead} cached)`;
|
|
259
|
+
}
|
|
260
|
+
return String(total);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function formatOutputTokens(breakdown: OutputTokenBreakdown): string {
|
|
264
|
+
const { total, reasoning } = breakdown;
|
|
265
|
+
if (reasoning && reasoning > 0) {
|
|
266
|
+
return `${total} (${reasoning} reasoning)`;
|
|
267
|
+
}
|
|
268
|
+
return String(total);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatToolParams(
|
|
272
|
+
args: Record<string, unknown>,
|
|
273
|
+
_maxLength = 40,
|
|
274
|
+
): string {
|
|
275
|
+
if (!args || typeof args !== 'object') return '';
|
|
276
|
+
|
|
277
|
+
const entries = Object.entries(args);
|
|
278
|
+
if (entries.length === 0) return '';
|
|
279
|
+
|
|
280
|
+
const formatValue = (value: unknown, maxLen = 20): string => {
|
|
281
|
+
if (value === null) return 'null';
|
|
282
|
+
if (value === undefined) return 'undefined';
|
|
283
|
+
if (typeof value === 'object') {
|
|
284
|
+
if (Array.isArray(value)) {
|
|
285
|
+
return `[${value.length}]`;
|
|
286
|
+
}
|
|
287
|
+
return '{…}';
|
|
288
|
+
}
|
|
289
|
+
if (typeof value === 'string') {
|
|
290
|
+
if (value.length > maxLen) {
|
|
291
|
+
return `"${value.slice(0, maxLen)}…"`;
|
|
292
|
+
}
|
|
293
|
+
return `"${value}"`;
|
|
294
|
+
}
|
|
295
|
+
return String(value);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const firstEntry = entries[0];
|
|
299
|
+
if (!firstEntry) return '';
|
|
300
|
+
|
|
301
|
+
const [firstKey, firstValue] = firstEntry;
|
|
302
|
+
const firstFormatted = `${firstKey}: ${formatValue(firstValue)}`;
|
|
303
|
+
|
|
304
|
+
if (entries.length === 1) {
|
|
305
|
+
return `{ ${firstFormatted} }`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return `{ ${firstFormatted}, … }`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function formatToolParamsInline(args: Record<string, unknown>): string {
|
|
312
|
+
if (!args || typeof args !== 'object') return '';
|
|
313
|
+
|
|
314
|
+
const entries = Object.entries(args);
|
|
315
|
+
if (entries.length === 0) return '';
|
|
316
|
+
|
|
317
|
+
const formatValue = (value: unknown, maxLen = 20): string => {
|
|
318
|
+
if (value === null) return 'null';
|
|
319
|
+
if (value === undefined) return 'undefined';
|
|
320
|
+
if (typeof value === 'object') {
|
|
321
|
+
if (Array.isArray(value)) {
|
|
322
|
+
return `[${value.length}]`;
|
|
323
|
+
}
|
|
324
|
+
return '{…}';
|
|
325
|
+
}
|
|
326
|
+
if (typeof value === 'string') {
|
|
327
|
+
if (value.length > maxLen) {
|
|
328
|
+
return `"${value.slice(0, maxLen)}…"`;
|
|
329
|
+
}
|
|
330
|
+
return `"${value}"`;
|
|
331
|
+
}
|
|
332
|
+
return String(value);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const firstEntry = entries[0];
|
|
336
|
+
if (!firstEntry) return '';
|
|
337
|
+
|
|
338
|
+
const [firstKey, firstValue] = firstEntry;
|
|
339
|
+
const firstFormatted = `${firstKey}: ${formatValue(firstValue)}`;
|
|
340
|
+
|
|
341
|
+
if (entries.length === 1) {
|
|
342
|
+
return `{ ${firstFormatted} }`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return `{ ${firstFormatted}, … }`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function formatResultPreview(result: unknown): string {
|
|
349
|
+
if (result === null) return 'null';
|
|
350
|
+
if (result === undefined) return 'undefined';
|
|
351
|
+
|
|
352
|
+
if (typeof result === 'string') {
|
|
353
|
+
if (result.length > 30) {
|
|
354
|
+
return `"${result.slice(0, 30)}…"`;
|
|
355
|
+
}
|
|
356
|
+
return `"${result}"`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (Array.isArray(result)) {
|
|
360
|
+
return `[${result.length}]`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (typeof result === 'object') {
|
|
364
|
+
const entries = Object.entries(result as Record<string, unknown>);
|
|
365
|
+
if (entries.length === 0) return '{}';
|
|
366
|
+
|
|
367
|
+
const firstEntry = entries[0];
|
|
368
|
+
if (!firstEntry) return '{…}';
|
|
369
|
+
|
|
370
|
+
const [firstKey, firstValue] = firstEntry;
|
|
371
|
+
let valueStr: string;
|
|
372
|
+
if (typeof firstValue === 'string' && firstValue.length > 15) {
|
|
373
|
+
valueStr = `"${firstValue.slice(0, 15)}…"`;
|
|
374
|
+
} else if (typeof firstValue === 'string') {
|
|
375
|
+
valueStr = `"${firstValue}"`;
|
|
376
|
+
} else if (typeof firstValue === 'object') {
|
|
377
|
+
valueStr = Array.isArray(firstValue) ? `[${firstValue.length}]` : '{…}';
|
|
378
|
+
} else {
|
|
379
|
+
valueStr = String(firstValue);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (entries.length === 1) {
|
|
383
|
+
return `{ ${firstKey}: ${valueStr} }`;
|
|
384
|
+
}
|
|
385
|
+
return `{ ${firstKey}: ${valueStr}, … }`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return String(result);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export const SPAN_COLORS: Record<SpanKind, string> = {
|
|
392
|
+
step: 'bg-blue-500',
|
|
393
|
+
'child-run': 'bg-cyan-500',
|
|
394
|
+
thinking: 'bg-amber-500',
|
|
395
|
+
'tool-call': 'bg-purple-500',
|
|
396
|
+
text: 'bg-emerald-500',
|
|
397
|
+
error: 'bg-red-500',
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const SPAN_COLORS_MUTED: Record<SpanKind, string> = {
|
|
401
|
+
step: 'bg-blue-500/20',
|
|
402
|
+
'child-run': 'bg-cyan-500/20',
|
|
403
|
+
thinking: 'bg-amber-500/20',
|
|
404
|
+
'tool-call': 'bg-purple-500/20',
|
|
405
|
+
text: 'bg-emerald-500/20',
|
|
406
|
+
error: 'bg-red-500/20',
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
export function buildTraceSpans(
|
|
410
|
+
runDetail: RunDetail,
|
|
411
|
+
parseJson: ParseJson,
|
|
412
|
+
): TraceSpan[] {
|
|
413
|
+
const spans: TraceSpan[] = [];
|
|
414
|
+
const traceStart = new Date(runDetail.run.started_at).getTime();
|
|
415
|
+
|
|
416
|
+
const addStepSpans = (
|
|
417
|
+
steps: Step[],
|
|
418
|
+
depth: number,
|
|
419
|
+
childRuns: ChildRun[],
|
|
420
|
+
functionId?: string | null,
|
|
421
|
+
) => {
|
|
422
|
+
for (const step of steps) {
|
|
423
|
+
const stepStartAbs = new Date(step.started_at).getTime();
|
|
424
|
+
const stepStartMs = stepStartAbs - traceStart;
|
|
425
|
+
const stepDuration = step.duration_ms ?? 0;
|
|
426
|
+
const stepEndMs = stepStartMs + stepDuration;
|
|
427
|
+
const usage = parseJson(step.usage) as ParsedUsage | null;
|
|
428
|
+
const output = parseJson(step.output) as ParsedOutput | null;
|
|
429
|
+
|
|
430
|
+
const stepLabel = functionId || step.model_id || 'LLM call';
|
|
431
|
+
const stepSublabel = functionId ? step.model_id : undefined;
|
|
432
|
+
|
|
433
|
+
const contentParts: ContentPart[] =
|
|
434
|
+
(output?.content as ContentPart[] | undefined) ?? [];
|
|
435
|
+
const hasSubParts = contentParts.length > 0 || step.error;
|
|
436
|
+
|
|
437
|
+
const tokenInfo = usage
|
|
438
|
+
? {
|
|
439
|
+
input:
|
|
440
|
+
typeof usage.inputTokens === 'number'
|
|
441
|
+
? usage.inputTokens
|
|
442
|
+
: ((usage.inputTokens as InputTokenBreakdown | undefined)
|
|
443
|
+
?.total ?? 0),
|
|
444
|
+
output:
|
|
445
|
+
typeof usage.outputTokens === 'number'
|
|
446
|
+
? usage.outputTokens
|
|
447
|
+
: ((usage.outputTokens as OutputTokenBreakdown | undefined)
|
|
448
|
+
?.total ?? 0),
|
|
449
|
+
}
|
|
450
|
+
: undefined;
|
|
451
|
+
|
|
452
|
+
spans.push({
|
|
453
|
+
id: step.id,
|
|
454
|
+
stepId: step.id,
|
|
455
|
+
label: stepLabel,
|
|
456
|
+
sublabel: !hasSubParts
|
|
457
|
+
? step.duration_ms === null && !step.error
|
|
458
|
+
? 'streaming...'
|
|
459
|
+
: stepSublabel || 'Response'
|
|
460
|
+
: stepSublabel,
|
|
461
|
+
startMs: stepStartMs,
|
|
462
|
+
durationMs: stepDuration,
|
|
463
|
+
depth,
|
|
464
|
+
kind: 'step',
|
|
465
|
+
tokens: tokenInfo,
|
|
466
|
+
modelId: step.model_id,
|
|
467
|
+
isInProgress: step.duration_ms === null && !step.error,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (!hasSubParts) {
|
|
471
|
+
const stepChildRuns = childRuns.filter(
|
|
472
|
+
cr => cr.run.parent_step_id === step.id,
|
|
473
|
+
);
|
|
474
|
+
for (const cr of stepChildRuns) {
|
|
475
|
+
addStepSpans(
|
|
476
|
+
cr.steps,
|
|
477
|
+
depth + 1,
|
|
478
|
+
cr.childRuns ?? [],
|
|
479
|
+
cr.run.function_id,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const stepChildRuns = childRuns.filter(
|
|
486
|
+
cr => cr.run.parent_step_id === step.id,
|
|
487
|
+
);
|
|
488
|
+
const sortedChildRuns = [...stepChildRuns].sort(
|
|
489
|
+
(a, b) =>
|
|
490
|
+
new Date(a.run.started_at).getTime() -
|
|
491
|
+
new Date(b.run.started_at).getTime(),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const toolCallParts = contentParts.filter(
|
|
495
|
+
(p): p is ToolCallContentPart => p.type === 'tool-call',
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const toolCallToChildRuns = new Map<string, ChildRun[]>();
|
|
499
|
+
const unmatchedChildRuns: ChildRun[] = [];
|
|
500
|
+
|
|
501
|
+
if (toolCallParts.length > 0 && sortedChildRuns.length > 0) {
|
|
502
|
+
let crIdx = 0;
|
|
503
|
+
for (const tc of toolCallParts) {
|
|
504
|
+
const tcId = tc.toolCallId;
|
|
505
|
+
if (!tcId) continue;
|
|
506
|
+
|
|
507
|
+
const tcResult = contentParts.find(
|
|
508
|
+
p =>
|
|
509
|
+
p.type === 'tool-result' &&
|
|
510
|
+
'toolCallId' in p &&
|
|
511
|
+
p.toolCallId === tcId,
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
if (tcResult && crIdx < sortedChildRuns.length) {
|
|
515
|
+
const cr = sortedChildRuns[crIdx];
|
|
516
|
+
if (cr) {
|
|
517
|
+
const existing = toolCallToChildRuns.get(tcId) ?? [];
|
|
518
|
+
existing.push(cr);
|
|
519
|
+
toolCallToChildRuns.set(tcId, existing);
|
|
520
|
+
crIdx++;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (let i = crIdx; i < sortedChildRuns.length; i++) {
|
|
525
|
+
const cr = sortedChildRuns[i];
|
|
526
|
+
if (cr) unmatchedChildRuns.push(cr);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
unmatchedChildRuns.push(...sortedChildRuns);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const toolTimeRanges: Array<{
|
|
533
|
+
toolCallId: string;
|
|
534
|
+
startMs: number;
|
|
535
|
+
endMs: number;
|
|
536
|
+
}> = [];
|
|
537
|
+
for (const tc of toolCallParts) {
|
|
538
|
+
const tcId = tc.toolCallId;
|
|
539
|
+
if (!tcId) continue;
|
|
540
|
+
const tcChildRuns = toolCallToChildRuns.get(tcId) ?? [];
|
|
541
|
+
if (tcChildRuns.length > 0) {
|
|
542
|
+
const earliest = Math.min(
|
|
543
|
+
...tcChildRuns.map(
|
|
544
|
+
cr => new Date(cr.run.started_at).getTime() - traceStart,
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
const latest = Math.max(
|
|
548
|
+
...tcChildRuns.map(cr => {
|
|
549
|
+
const crStart =
|
|
550
|
+
new Date(cr.run.started_at).getTime() - traceStart;
|
|
551
|
+
const crDuration = cr.steps.reduce(
|
|
552
|
+
(a, s) => a + (s.duration_ms || 0),
|
|
553
|
+
0,
|
|
554
|
+
);
|
|
555
|
+
return crStart + crDuration;
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
toolTimeRanges.push({
|
|
559
|
+
toolCallId: tcId,
|
|
560
|
+
startMs: earliest,
|
|
561
|
+
endMs: latest,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let cursor = stepStartMs;
|
|
567
|
+
|
|
568
|
+
for (const part of contentParts) {
|
|
569
|
+
if (part.type === 'reasoning' || part.type === 'thinking') {
|
|
570
|
+
const thinkingText =
|
|
571
|
+
part.text || part.thinking || part.reasoning || '';
|
|
572
|
+
|
|
573
|
+
const nextToolStart =
|
|
574
|
+
toolTimeRanges.length > 0
|
|
575
|
+
? Math.min(...toolTimeRanges.map(t => t.startMs))
|
|
576
|
+
: stepEndMs;
|
|
577
|
+
const thinkingEnd = Math.max(cursor, nextToolStart);
|
|
578
|
+
const thinkingDuration = Math.max(0, thinkingEnd - cursor);
|
|
579
|
+
|
|
580
|
+
spans.push({
|
|
581
|
+
id: `${step.id}-thinking-${part.toolCallId || cursor}`,
|
|
582
|
+
stepId: step.id,
|
|
583
|
+
label: 'Thinking',
|
|
584
|
+
sublabel:
|
|
585
|
+
thinkingText.length > 50
|
|
586
|
+
? thinkingText.slice(0, 50) + '...'
|
|
587
|
+
: thinkingText,
|
|
588
|
+
startMs: cursor,
|
|
589
|
+
durationMs: thinkingDuration,
|
|
590
|
+
depth: depth + 1,
|
|
591
|
+
kind: 'thinking',
|
|
592
|
+
thinkingText,
|
|
593
|
+
});
|
|
594
|
+
cursor = cursor + thinkingDuration;
|
|
595
|
+
} else if (part.type === 'tool-call') {
|
|
596
|
+
const toolName = part.toolName || 'unknown';
|
|
597
|
+
const args = part.input ?? part.args;
|
|
598
|
+
const argPreview =
|
|
599
|
+
args && typeof args === 'object'
|
|
600
|
+
? Object.entries(args as Record<string, unknown>)
|
|
601
|
+
.slice(0, 3)
|
|
602
|
+
.map(([, v]) => {
|
|
603
|
+
const s =
|
|
604
|
+
typeof v === 'string'
|
|
605
|
+
? v
|
|
606
|
+
: (JSON.stringify(v) ?? String(v));
|
|
607
|
+
return s.length > 30 ? s.slice(0, 30) + '…' : s;
|
|
608
|
+
})
|
|
609
|
+
.join(', ')
|
|
610
|
+
: typeof args === 'string'
|
|
611
|
+
? args.slice(0, 60)
|
|
612
|
+
: '';
|
|
613
|
+
|
|
614
|
+
const tcChildRuns =
|
|
615
|
+
toolCallToChildRuns.get(part.toolCallId ?? '') ?? [];
|
|
616
|
+
const timeRange = toolTimeRanges.find(
|
|
617
|
+
t => t.toolCallId === part.toolCallId,
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
let toolStartMs: number;
|
|
621
|
+
let toolDuration: number;
|
|
622
|
+
|
|
623
|
+
if (timeRange) {
|
|
624
|
+
toolStartMs = timeRange.startMs;
|
|
625
|
+
toolDuration = timeRange.endMs - timeRange.startMs;
|
|
626
|
+
} else {
|
|
627
|
+
toolStartMs = cursor;
|
|
628
|
+
toolDuration = 0;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
spans.push({
|
|
632
|
+
id: `${step.id}-tool-${part.toolCallId || cursor}`,
|
|
633
|
+
stepId: step.id,
|
|
634
|
+
label: toolName,
|
|
635
|
+
sublabel: argPreview ? `(${argPreview})` : undefined,
|
|
636
|
+
startMs: toolStartMs,
|
|
637
|
+
durationMs: toolDuration,
|
|
638
|
+
depth: depth + 1,
|
|
639
|
+
kind: 'tool-call',
|
|
640
|
+
toolCallId: part.toolCallId,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
for (const cr of tcChildRuns) {
|
|
644
|
+
addStepSpans(
|
|
645
|
+
cr.steps,
|
|
646
|
+
depth + 2,
|
|
647
|
+
cr.childRuns ?? [],
|
|
648
|
+
cr.run.function_id,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
cursor = Math.max(cursor, toolStartMs + toolDuration);
|
|
653
|
+
} else if (part.type === 'text') {
|
|
654
|
+
const text = part.text || '';
|
|
655
|
+
|
|
656
|
+
const textDuration = Math.max(0, stepEndMs - cursor);
|
|
657
|
+
|
|
658
|
+
spans.push({
|
|
659
|
+
id: `${step.id}-text-${cursor}`,
|
|
660
|
+
stepId: step.id,
|
|
661
|
+
label: 'Text',
|
|
662
|
+
sublabel: text.length > 60 ? text.slice(0, 60) + '...' : text,
|
|
663
|
+
startMs: cursor,
|
|
664
|
+
durationMs: textDuration,
|
|
665
|
+
depth: depth + 1,
|
|
666
|
+
kind: 'text',
|
|
667
|
+
textContent: text,
|
|
668
|
+
});
|
|
669
|
+
cursor = cursor + textDuration;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (step.error) {
|
|
674
|
+
const errorDuration = Math.max(0, stepEndMs - cursor);
|
|
675
|
+
spans.push({
|
|
676
|
+
id: `${step.id}-error`,
|
|
677
|
+
stepId: step.id,
|
|
678
|
+
label: 'Error',
|
|
679
|
+
sublabel:
|
|
680
|
+
step.error.length > 60
|
|
681
|
+
? step.error.slice(0, 60) + '...'
|
|
682
|
+
: step.error,
|
|
683
|
+
startMs: cursor,
|
|
684
|
+
durationMs: errorDuration,
|
|
685
|
+
depth: depth + 1,
|
|
686
|
+
kind: 'error',
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
for (const cr of unmatchedChildRuns) {
|
|
691
|
+
addStepSpans(
|
|
692
|
+
cr.steps,
|
|
693
|
+
depth + 1,
|
|
694
|
+
cr.childRuns ?? [],
|
|
695
|
+
cr.run.function_id,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
addStepSpans(
|
|
702
|
+
runDetail.steps,
|
|
703
|
+
0,
|
|
704
|
+
runDetail.childRuns ?? [],
|
|
705
|
+
runDetail.run.function_id,
|
|
706
|
+
);
|
|
707
|
+
return spans;
|
|
708
|
+
}
|