@ai-sdk/devtools 1.0.8 → 1.0.9

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.
@@ -8,16 +8,27 @@ import type {
8
8
  TextContentPart,
9
9
  ReasoningContentPart,
10
10
  } from '../types';
11
- import { safeParseJson, formatToolParams } from '../utils';
12
- import { JsonBlock, ReasoningBlock, TextBlock } from './shared-components';
11
+ import {
12
+ safeParseJson,
13
+ formatToolParams,
14
+ getOutputToolResults,
15
+ } from '../utils';
16
+ import {
17
+ MediaAwareValue,
18
+ ReasoningBlock,
19
+ TextBlock,
20
+ } from './shared-components';
21
+ import { MediaPreviewList } from './media-components';
13
22
 
14
23
  export function OutputDisplay({
15
24
  output,
16
- toolResults = [],
25
+ fallbackToolResults = [],
17
26
  }: {
18
27
  output: ParsedOutput;
19
- toolResults?: ContentPart[];
28
+ fallbackToolResults?: ContentPart[];
20
29
  }) {
30
+ const toolResults = getOutputToolResults(output, fallbackToolResults);
31
+
21
32
  const getToolResult = (toolCallId: string) => {
22
33
  return toolResults.find(
23
34
  (r): r is ToolResultContentPart =>
@@ -62,6 +73,15 @@ export function OutputDisplay({
62
73
  <TextBlock content={textContent} defaultExpanded={!!isTextOnly} />
63
74
  )}
64
75
 
76
+ <MediaPreviewList
77
+ data={output?.content?.filter(
78
+ part =>
79
+ part.type === 'file' ||
80
+ part.type === 'reasoning-file' ||
81
+ part.type === 'image',
82
+ )}
83
+ />
84
+
65
85
  {toolCalls.map((call, i) => {
66
86
  const result = call.toolCallId
67
87
  ? getToolResult(call.toolCallId)
@@ -127,7 +147,7 @@ function ToolCallCard({
127
147
  <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mb-2">
128
148
  Input
129
149
  </div>
130
- <JsonBlock data={parsedArgs} />
150
+ <MediaAwareValue data={parsedArgs} />
131
151
  </div>
132
152
 
133
153
  {parsedResult != null && (
@@ -135,7 +155,7 @@ function ToolCallCard({
135
155
  <div className="text-[10px] font-medium uppercase tracking-wider text-success mb-2">
136
156
  Output
137
157
  </div>
138
- <JsonBlock data={parsedResult} />
158
+ <MediaAwareValue data={parsedResult} />
139
159
  </div>
140
160
  )}
141
161
  </>
@@ -29,21 +29,33 @@ import {
29
29
  getInputTokenBreakdown,
30
30
  getOutputTokenBreakdown,
31
31
  } from '../utils';
32
+ import { MediaPreviewList } from './media-components';
32
33
 
33
34
  export function JsonBlock({
34
35
  data,
35
36
  compact = false,
36
37
  size = 'sm',
38
+ maxStringLength,
37
39
  }: {
38
40
  data: unknown;
39
41
  compact?: boolean;
40
42
  size?: 'sm' | 'base' | 'lg';
43
+ maxStringLength?: number;
41
44
  }) {
42
45
  const [copied, setCopied] = useState(false);
43
46
 
44
- const jsonString = JSON.stringify(data, null, 2);
47
+ const replacer =
48
+ maxStringLength == null
49
+ ? undefined
50
+ : (_key: string, value: unknown) =>
51
+ typeof value === 'string' && value.length > maxStringLength
52
+ ? `${value.slice(0, maxStringLength)}… [${value.length - maxStringLength} characters omitted from the viewer]`
53
+ : value;
54
+ const jsonString = JSON.stringify(data, replacer, 2);
45
55
  const displayString =
46
- compact && jsonString.length > 200 ? JSON.stringify(data) : jsonString;
56
+ compact && jsonString.length > 200
57
+ ? JSON.stringify(data, replacer)
58
+ : jsonString;
47
59
 
48
60
  const sizeClasses = {
49
61
  sm: 'text-xs',
@@ -81,6 +93,15 @@ export function JsonBlock({
81
93
  );
82
94
  }
83
95
 
96
+ export function MediaAwareValue({ data }: { data: unknown }) {
97
+ return (
98
+ <div className="space-y-3">
99
+ <MediaPreviewList data={data} />
100
+ <JsonBlock data={data} maxStringLength={16 * 1024} />
101
+ </div>
102
+ );
103
+ }
104
+
84
105
  export function RawDataSection({
85
106
  rawRequest,
86
107
  rawResponse,
@@ -516,7 +537,7 @@ export function CollapsibleToolCall({
516
537
  </button>
517
538
  {expanded && (
518
539
  <div className="p-3 border-t bg-card/50 border-tool/30">
519
- <JsonBlock data={parsedData} />
540
+ <MediaAwareValue data={parsedData} />
520
541
  </div>
521
542
  )}
522
543
  </div>
@@ -559,7 +580,7 @@ export function CollapsibleToolResult({
559
580
  </button>
560
581
  {expanded && (
561
582
  <div className="p-3 border-t bg-card/50 border-success/30">
562
- <JsonBlock data={data} />
583
+ <MediaAwareValue data={data} />
563
584
  </div>
564
585
  )}
565
586
  </div>
@@ -256,7 +256,7 @@ export function StepDetailContent({
256
256
  const nextInput = nextStep
257
257
  ? (parseJson(nextStep.input) as ParsedInput | null)
258
258
  : null;
259
- const toolResults: ContentPart[] =
259
+ const fallbackToolResults: ContentPart[] =
260
260
  nextInput?.prompt
261
261
  ?.filter((msg: PromptMessage) => msg.role === 'tool')
262
262
  ?.flatMap((msg: PromptMessage) =>
@@ -290,7 +290,10 @@ export function StepDetailContent({
290
290
  {step.error}
291
291
  </div>
292
292
  ) : output ? (
293
- <OutputDisplay output={output} toolResults={toolResults} />
293
+ <OutputDisplay
294
+ output={output}
295
+ fallbackToolResults={fallbackToolResults}
296
+ />
294
297
  ) : isActiveStep ? (
295
298
  <div className="flex items-center gap-2 text-sm text-info">
296
299
  <Loader2 className="size-4 animate-spin" />
@@ -14,6 +14,7 @@ import type {
14
14
  SpanKind,
15
15
  TraceSpan,
16
16
  ParseJson,
17
+ ParsedInput,
17
18
  ParsedOutput,
18
19
  ContentPart,
19
20
  ToolCallContentPart,
@@ -21,11 +22,12 @@ import type {
21
22
  } from '../types';
22
23
  import {
23
24
  buildTraceSpans,
25
+ getOutputToolResults,
24
26
  safeParseJson,
25
27
  SPAN_COLORS,
26
28
  SPAN_COLORS_MUTED,
27
29
  } from '../utils';
28
- import { JsonBlock } from './shared-components';
30
+ import { MediaAwareValue } from './shared-components';
29
31
  import { StepDetailContent } from './step-card';
30
32
 
31
33
  export function TraceTimeline({
@@ -424,7 +426,17 @@ function SpanDetailPanel({
424
426
  'toolCallId' in p &&
425
427
  p.toolCallId === span.toolCallId,
426
428
  );
427
- const toolResult = contentParts.find(
429
+ const nextStep = steps[stepIndex + 1];
430
+ const nextInput = nextStep
431
+ ? (parseJson(nextStep.input) as ParsedInput | null)
432
+ : null;
433
+ const fallbackToolResults =
434
+ nextInput?.prompt
435
+ ?.filter(message => message.role === 'tool')
436
+ .flatMap(message =>
437
+ Array.isArray(message.content) ? message.content : [],
438
+ ) ?? [];
439
+ const toolResult = getOutputToolResults(output, fallbackToolResults).find(
428
440
  (p): p is ToolResultContentPart =>
429
441
  p.type === 'tool-result' &&
430
442
  'toolCallId' in p &&
@@ -450,7 +462,7 @@ function SpanDetailPanel({
450
462
  <h4 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
451
463
  Input
452
464
  </h4>
453
- <JsonBlock data={parsedArgs} />
465
+ <MediaAwareValue data={parsedArgs} />
454
466
  </div>
455
467
  )}
456
468
  {parsedResult != null && (
@@ -458,7 +470,7 @@ function SpanDetailPanel({
458
470
  <h4 className="text-[11px] font-semibold uppercase tracking-wider text-success mb-2">
459
471
  Output
460
472
  </h4>
461
- <JsonBlock data={parsedResult} />
473
+ <MediaAwareValue data={parsedResult} />
462
474
  </div>
463
475
  )}
464
476
  {!parsedArgs && !parsedResult && (
@@ -0,0 +1,440 @@
1
+ export type MediaKind = 'image' | 'audio' | 'video' | 'file';
2
+
3
+ export interface MediaPreviewData {
4
+ filename?: string;
5
+ kind: MediaKind;
6
+ mediaType: string;
7
+ source?: string;
8
+ sourceType?: 'inline' | 'remote';
9
+ unavailableReason?: string;
10
+ }
11
+
12
+ export interface MediaPreviewLimits {
13
+ maxCount: number;
14
+ maxDepth: number;
15
+ maxInlineBytes: number;
16
+ maxNodes: number;
17
+ }
18
+
19
+ export const DEFAULT_MEDIA_PREVIEW_LIMITS: MediaPreviewLimits = {
20
+ maxCount: 8,
21
+ maxDepth: 12,
22
+ maxInlineBytes: 5 * 1024 * 1024,
23
+ maxNodes: 1000,
24
+ };
25
+
26
+ const safeInlineMediaTypes = new Set([
27
+ 'audio/m4a',
28
+ 'audio/mp3',
29
+ 'audio/mp4',
30
+ 'audio/mpeg',
31
+ 'audio/ogg',
32
+ 'audio/wav',
33
+ 'audio/webm',
34
+ 'image/avif',
35
+ 'image/bmp',
36
+ 'image/gif',
37
+ 'image/jpeg',
38
+ 'image/png',
39
+ 'image/webp',
40
+ 'image/x-icon',
41
+ 'video/mp4',
42
+ 'video/ogg',
43
+ 'video/webm',
44
+ ]);
45
+
46
+ const mediaTypesByExtension: Record<string, string> = {
47
+ avif: 'image/avif',
48
+ bmp: 'image/bmp',
49
+ gif: 'image/gif',
50
+ ico: 'image/x-icon',
51
+ jpeg: 'image/jpeg',
52
+ jpg: 'image/jpeg',
53
+ m4a: 'audio/mp4',
54
+ mp3: 'audio/mpeg',
55
+ mp4: 'video/mp4',
56
+ oga: 'audio/ogg',
57
+ ogg: 'audio/ogg',
58
+ ogv: 'video/ogg',
59
+ png: 'image/png',
60
+ wav: 'audio/wav',
61
+ webm: 'video/webm',
62
+ webp: 'image/webp',
63
+ };
64
+
65
+ function isRecord(value: unknown): value is Record<string, unknown> {
66
+ return value != null && typeof value === 'object' && !Array.isArray(value);
67
+ }
68
+
69
+ function getKind(mediaType: string): MediaKind {
70
+ const topLevelType = mediaType.toLowerCase().split('/')[0];
71
+ return topLevelType === 'image' ||
72
+ topLevelType === 'audio' ||
73
+ topLevelType === 'video'
74
+ ? topLevelType
75
+ : 'file';
76
+ }
77
+
78
+ function getBase64Prefix(data: string, maxBytes = 16): number[] {
79
+ const alphabet =
80
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
81
+ const bytes: number[] = [];
82
+ let buffer = 0;
83
+ let bitCount = 0;
84
+
85
+ for (const character of data) {
86
+ if (character === '=') break;
87
+
88
+ const value = alphabet.indexOf(character);
89
+ if (value === -1) return [];
90
+
91
+ buffer = (buffer << 6) | value;
92
+ bitCount += 6;
93
+
94
+ if (bitCount >= 8) {
95
+ bitCount -= 8;
96
+ bytes.push((buffer >> bitCount) & 0xff);
97
+ if (bytes.length >= maxBytes) break;
98
+ }
99
+ }
100
+
101
+ return bytes;
102
+ }
103
+
104
+ function hasBytes(bytes: number[], expected: number[], offset = 0): boolean {
105
+ return expected.every((value, index) => bytes[offset + index] === value);
106
+ }
107
+
108
+ function detectInlineMediaType({
109
+ data,
110
+ mediaType,
111
+ }: {
112
+ data: string;
113
+ mediaType: string;
114
+ }): string | undefined {
115
+ const normalizedMediaType = mediaType.toLowerCase();
116
+ if (safeInlineMediaTypes.has(normalizedMediaType)) {
117
+ return normalizedMediaType;
118
+ }
119
+
120
+ const bytes = getBase64Prefix(data);
121
+
122
+ if (normalizedMediaType === 'image') {
123
+ if (hasBytes(bytes, [0x89, 0x50, 0x4e, 0x47])) return 'image/png';
124
+ if (hasBytes(bytes, [0xff, 0xd8, 0xff])) return 'image/jpeg';
125
+ if (hasBytes(bytes, [0x47, 0x49, 0x46, 0x38])) return 'image/gif';
126
+ if (
127
+ hasBytes(bytes, [0x52, 0x49, 0x46, 0x46]) &&
128
+ hasBytes(bytes, [0x57, 0x45, 0x42, 0x50], 8)
129
+ ) {
130
+ return 'image/webp';
131
+ }
132
+ if (hasBytes(bytes, [0x42, 0x4d])) return 'image/bmp';
133
+ }
134
+
135
+ if (normalizedMediaType === 'audio') {
136
+ if (hasBytes(bytes, [0x49, 0x44, 0x33])) return 'audio/mpeg';
137
+ if (bytes[0] === 0xff && bytes[1] != null && (bytes[1] & 0xe0) === 0xe0) {
138
+ return 'audio/mpeg';
139
+ }
140
+ if (
141
+ hasBytes(bytes, [0x52, 0x49, 0x46, 0x46]) &&
142
+ hasBytes(bytes, [0x57, 0x41, 0x56, 0x45], 8)
143
+ ) {
144
+ return 'audio/wav';
145
+ }
146
+ if (hasBytes(bytes, [0x4f, 0x67, 0x67, 0x53])) return 'audio/ogg';
147
+ if (hasBytes(bytes, [0x1a, 0x45, 0xdf, 0xa3])) return 'audio/webm';
148
+ if (hasBytes(bytes, [0x66, 0x74, 0x79, 0x70], 4)) return 'audio/mp4';
149
+ }
150
+
151
+ if (normalizedMediaType === 'video') {
152
+ if (hasBytes(bytes, [0x4f, 0x67, 0x67, 0x53])) return 'video/ogg';
153
+ if (hasBytes(bytes, [0x1a, 0x45, 0xdf, 0xa3])) return 'video/webm';
154
+ if (hasBytes(bytes, [0x66, 0x74, 0x79, 0x70], 4)) return 'video/mp4';
155
+ }
156
+
157
+ return undefined;
158
+ }
159
+
160
+ function inferMediaTypeFromUrl(source: unknown): string | undefined {
161
+ if (typeof source !== 'string') return undefined;
162
+
163
+ try {
164
+ const extension = new URL(source).pathname.split('.').pop()?.toLowerCase();
165
+ return extension == null ? undefined : mediaTypesByExtension[extension];
166
+ } catch {
167
+ return undefined;
168
+ }
169
+ }
170
+
171
+ function getBase64ByteLength(value: string): number | undefined {
172
+ if (
173
+ value.length === 0 ||
174
+ value.length % 4 !== 0 ||
175
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
176
+ value,
177
+ )
178
+ ) {
179
+ return undefined;
180
+ }
181
+
182
+ const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
183
+ return (value.length / 4) * 3 - padding;
184
+ }
185
+
186
+ function getSafeSource({
187
+ source,
188
+ mediaType,
189
+ maxInlineBytes,
190
+ }: {
191
+ source: unknown;
192
+ mediaType: string;
193
+ maxInlineBytes: number;
194
+ }): Pick<MediaPreviewData, 'source' | 'sourceType' | 'unavailableReason'> {
195
+ if (typeof source !== 'string') {
196
+ return {};
197
+ }
198
+
199
+ if (source.startsWith('data:')) {
200
+ const match = /^data:([^;,]+);base64,(.*)$/i.exec(source);
201
+ const sourceMediaType = match?.[1]?.toLowerCase();
202
+ const byteLength =
203
+ match?.[2] == null ? undefined : getBase64ByteLength(match[2]);
204
+ const declaredTopLevelType = mediaType.toLowerCase().split('/')[0];
205
+ if (
206
+ sourceMediaType == null ||
207
+ byteLength == null ||
208
+ (sourceMediaType !== mediaType.toLowerCase() &&
209
+ sourceMediaType.split('/')[0] !== declaredTopLevelType) ||
210
+ !safeInlineMediaTypes.has(sourceMediaType)
211
+ ) {
212
+ return {};
213
+ }
214
+
215
+ if (byteLength > maxInlineBytes) {
216
+ return {
217
+ unavailableReason: `Inline preview exceeds the ${maxInlineBytes}-byte limit.`,
218
+ };
219
+ }
220
+
221
+ return { source, sourceType: 'inline' };
222
+ }
223
+
224
+ try {
225
+ const url = new URL(source);
226
+ return (url.protocol === 'http:' || url.protocol === 'https:') &&
227
+ url.username === '' &&
228
+ url.password === ''
229
+ ? { source: url.toString(), sourceType: 'remote' }
230
+ : {};
231
+ } catch {
232
+ const inlineMediaType = detectInlineMediaType({
233
+ data: source,
234
+ mediaType,
235
+ });
236
+ const byteLength = getBase64ByteLength(source);
237
+ if (inlineMediaType == null || byteLength == null) {
238
+ return {};
239
+ }
240
+
241
+ if (byteLength > maxInlineBytes) {
242
+ return {
243
+ unavailableReason: `Inline preview exceeds the ${maxInlineBytes}-byte limit.`,
244
+ };
245
+ }
246
+
247
+ return {
248
+ source: `data:${inlineMediaType};base64,${source}`,
249
+ sourceType: 'inline',
250
+ };
251
+ }
252
+ }
253
+
254
+ function getFileData(
255
+ value: Record<string, unknown>,
256
+ maxInlineBytes: number,
257
+ ): Pick<MediaPreviewData, 'source' | 'sourceType' | 'unavailableReason'> {
258
+ const generatedFile = isRecord(value.file) ? value.file : undefined;
259
+ const mediaType =
260
+ typeof value.mediaType === 'string'
261
+ ? value.mediaType
262
+ : typeof generatedFile?.mediaType === 'string'
263
+ ? generatedFile.mediaType
264
+ : undefined;
265
+ if (mediaType == null) {
266
+ return {};
267
+ }
268
+
269
+ const data = value.data;
270
+ if (isRecord(data)) {
271
+ if (data.type === 'data') {
272
+ return getSafeSource({ source: data.data, mediaType, maxInlineBytes });
273
+ }
274
+ if (data.type === 'url') {
275
+ return getSafeSource({ source: data.url, mediaType, maxInlineBytes });
276
+ }
277
+ return {};
278
+ }
279
+
280
+ const generatedFileData =
281
+ generatedFile?.base64 ??
282
+ generatedFile?.base64Data ??
283
+ generatedFile?.uint8Array ??
284
+ generatedFile?.uint8ArrayData;
285
+
286
+ return getSafeSource({
287
+ source: data ?? generatedFileData,
288
+ mediaType,
289
+ maxInlineBytes,
290
+ });
291
+ }
292
+
293
+ function parseMediaPart(
294
+ value: unknown,
295
+ maxInlineBytes: number,
296
+ ): MediaPreviewData | undefined {
297
+ if (!isRecord(value) || typeof value.type !== 'string') {
298
+ return undefined;
299
+ }
300
+
301
+ if (value.type === 'file' || value.type === 'reasoning-file') {
302
+ const generatedFile = isRecord(value.file) ? value.file : undefined;
303
+ const mediaType =
304
+ typeof value.mediaType === 'string'
305
+ ? value.mediaType
306
+ : typeof generatedFile?.mediaType === 'string'
307
+ ? generatedFile.mediaType
308
+ : undefined;
309
+ if (mediaType == null) {
310
+ return undefined;
311
+ }
312
+
313
+ return {
314
+ filename:
315
+ typeof value.filename === 'string'
316
+ ? value.filename
317
+ : typeof generatedFile?.filename === 'string'
318
+ ? generatedFile.filename
319
+ : undefined,
320
+ kind: getKind(mediaType),
321
+ mediaType,
322
+ ...getFileData(value, maxInlineBytes),
323
+ };
324
+ }
325
+
326
+ if (value.type === 'image') {
327
+ const mediaType =
328
+ typeof value.mediaType === 'string' ? value.mediaType : 'image/png';
329
+ const image = value.image;
330
+ const source =
331
+ isRecord(image) && image.type === 'url'
332
+ ? image.url
333
+ : isRecord(image) && image.type === 'data'
334
+ ? image.data
335
+ : image;
336
+
337
+ return {
338
+ kind: 'image',
339
+ mediaType,
340
+ ...getSafeSource({ source, mediaType, maxInlineBytes }),
341
+ };
342
+ }
343
+
344
+ if (
345
+ value.type === 'media' ||
346
+ value.type === 'file-data' ||
347
+ value.type === 'image-data'
348
+ ) {
349
+ const mediaType =
350
+ typeof value.mediaType === 'string'
351
+ ? value.mediaType
352
+ : value.type === 'image-data'
353
+ ? 'image/png'
354
+ : undefined;
355
+ if (mediaType == null) {
356
+ return undefined;
357
+ }
358
+
359
+ return {
360
+ filename: typeof value.filename === 'string' ? value.filename : undefined,
361
+ kind: getKind(mediaType),
362
+ mediaType,
363
+ ...getSafeSource({
364
+ source: value.data,
365
+ mediaType,
366
+ maxInlineBytes,
367
+ }),
368
+ };
369
+ }
370
+
371
+ if (value.type === 'image-url' || value.type === 'file-url') {
372
+ const mediaType =
373
+ typeof value.mediaType === 'string'
374
+ ? value.mediaType
375
+ : value.type === 'image-url'
376
+ ? 'image'
377
+ : (inferMediaTypeFromUrl(value.url) ?? 'application/octet-stream');
378
+
379
+ return {
380
+ filename: typeof value.filename === 'string' ? value.filename : undefined,
381
+ kind: getKind(mediaType),
382
+ mediaType,
383
+ ...getSafeSource({ source: value.url, mediaType, maxInlineBytes }),
384
+ };
385
+ }
386
+
387
+ return undefined;
388
+ }
389
+
390
+ export function findMediaPreviews(
391
+ value: unknown,
392
+ limitOverrides: Partial<MediaPreviewLimits> = {},
393
+ ): MediaPreviewData[] {
394
+ const limits = { ...DEFAULT_MEDIA_PREVIEW_LIMITS, ...limitOverrides };
395
+ const previews: MediaPreviewData[] = [];
396
+ const visited = new WeakSet<object>();
397
+ const pending: Array<{ value: unknown; depth: number }> = [
398
+ { value, depth: 0 },
399
+ ];
400
+ let visitedNodes = 0;
401
+
402
+ while (
403
+ pending.length > 0 &&
404
+ previews.length < limits.maxCount &&
405
+ visitedNodes < limits.maxNodes
406
+ ) {
407
+ const { value: current, depth } = pending.pop()!;
408
+ visitedNodes++;
409
+
410
+ const preview = parseMediaPart(current, limits.maxInlineBytes);
411
+ if (preview != null) {
412
+ previews.push(preview);
413
+ continue;
414
+ }
415
+
416
+ if (
417
+ depth >= limits.maxDepth ||
418
+ current == null ||
419
+ typeof current !== 'object'
420
+ ) {
421
+ continue;
422
+ }
423
+
424
+ if (visited.has(current)) {
425
+ continue;
426
+ }
427
+ visited.add(current);
428
+
429
+ const children = Array.isArray(current)
430
+ ? current
431
+ : isRecord(current)
432
+ ? Object.values(current)
433
+ : [];
434
+ for (let index = children.length - 1; index >= 0; index--) {
435
+ pending.push({ value: children[index], depth: depth + 1 });
436
+ }
437
+ }
438
+
439
+ return previews;
440
+ }
@@ -137,11 +137,25 @@ export interface ReasoningContentPart {
137
137
  toolCallId?: string;
138
138
  }
139
139
 
140
+ export interface MediaContentPart {
141
+ type:
142
+ | 'file'
143
+ | 'reasoning-file'
144
+ | 'image'
145
+ | 'media'
146
+ | 'file-data'
147
+ | 'file-url'
148
+ | 'image-data'
149
+ | 'image-url';
150
+ [key: string]: unknown;
151
+ }
152
+
140
153
  export type ContentPart =
141
154
  | TextContentPart
142
155
  | ToolCallContentPart
143
156
  | ToolResultContentPart
144
- | ReasoningContentPart;
157
+ | ReasoningContentPart
158
+ | MediaContentPart;
145
159
 
146
160
  export type MessageRole = 'user' | 'assistant' | 'system' | 'tool';
147
161
 
@@ -169,9 +183,13 @@ export interface ToolDefinition {
169
183
  export interface ParsedOutput {
170
184
  finishReason?: string | { unified?: string; raw?: string };
171
185
  toolCalls?: ToolCallContentPart[];
186
+ toolResults?: ToolResultContentPart[];
172
187
  textParts?: TextContentPart[];
173
188
  reasoningParts?: ReasoningContentPart[];
174
189
  content?: ContentPart[];
190
+ response?: {
191
+ messages?: PromptMessage[];
192
+ };
175
193
  }
176
194
 
177
195
  export interface ParsedUsage {