UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

97 lines (90 loc) • 3.16 kB
import type { ContentPart } from '../types' const CONTENT_PART_TYPES = new Set([ 'text', 'image', 'audio', 'video', 'document', ]) /** * Structural check for a single `ContentPart`. A text part must carry a string * `content`. Every other part carries a source with a string `value`; a file * source's `value` is a non-empty opaque handle, and its optional `provider` * is a string. */ export function isContentPart(value: unknown): value is ContentPart { if (typeof value !== 'object' || value === null) return false const part = value as Record<string, unknown> if (typeof part.type !== 'string' || !CONTENT_PART_TYPES.has(part.type)) { return false } if (part.type === 'text') { return typeof part.content === 'string' } const source = part.source if (typeof source !== 'object' || source === null) return false const src = source as Record<string, unknown> if (typeof src.value !== 'string') return false // `file` sources carry an opaque handle in `value`; `provider`, when set, // names the issuer. if (src.type === 'file') { return ( src.value.length > 0 && (src.provider === undefined || typeof src.provider === 'string') ) } // `data` sources require a mimeType (matches ContentPartDataSource); `url` // sources don't. Requiring it here keeps the runtime guard consistent with // the type and avoids emitting `data:undefined;base64,...` downstream. if (src.type === 'data') return typeof src.mimeType === 'string' return src.type === 'url' } /** * True iff `value` is a NON-EMPTY array whose every element is a valid * `ContentPart`. Empty arrays and mixed arrays return false so they continue * to be treated as ordinary (stringified) data — this keeps the auto-detection * footgun narrow. */ export function isContentPartArray( value: unknown, ): value is Array<ContentPart> { return Array.isArray(value) && value.length > 0 && value.every(isContentPart) } /** * Error text for a failed tool result: `output.error` when it is a string, * else the output itself when it is a string, else a generic message. * `StreamProcessor` and `chat()` history share it, so a reload shows the * same text as the live stream. */ export function toolResultErrorText(output: unknown): string { if ( output && typeof output === 'object' && 'error' in output && typeof output.error === 'string' ) { return output.error } return typeof output === 'string' ? output : 'Tool execution failed' } /** Parse tool result content as JSON. Plain text stays a string. */ export function parseToolOutput(content: string): unknown { try { return JSON.parse(content) } catch { return content } } /** * Normalize a tool's return value for transport: * - string → unchanged * - ContentPart array → unchanged (multimodal, passed through to the adapter) * - anything else → `JSON.stringify` */ export function normalizeToolResult( result: unknown, ): string | Array<ContentPart> { if (typeof result === 'string') return result if (isContentPartArray(result)) return result return JSON.stringify(result) }