UNPKG

@tanstack/ai

Version:

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

145 lines (135 loc) 5 kB
import type { Logger } from './types' /** * `util.inspect` options used with `console.dir` on Node so deeply nested * structures (e.g. provider chunk payloads with `usage`, `output`, * `reasoning`, `tools`) render in full instead of truncating to * `[Object]` / `[Array]`. */ const DIR_OPTIONS = { depth: null, colors: true } as const /** * How `meta` should be rendered on the current runtime: * * - `dir` — Node. `console.dir(meta, { depth: null, colors: true })` gives a * depth-unlimited, colored inspect dump. * - `json` — Cloudflare Workers / workerd. workerd never forwards * `console.dir` output to the terminal (with or without options), and its * own inspect of extra console arguments truncates nested objects, so the * payload is appended as circular-safe pretty-printed JSON instead. * - `arg` — everything else (browsers, Deno, Bun). `meta` is passed as an * extra console argument: devtools keep collapsible object trees and the * runtime's inspect handles circular references natively. */ type MetaStrategy = 'dir' | 'json' | 'arg' function resolveMetaStrategy(): MetaStrategy { // workerd must be detected before the Node check: under the `nodejs_compat` // flag it emulates `process.versions.node`, but still drops `console.dir`. try { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- navigator is missing on Node < 21 despite the DOM lib typing it as always present if (globalThis.navigator?.userAgent === 'Cloudflare-Workers') return 'json' } catch { // A locked-down runtime with a throwing `userAgent` getter is not workerd; // fall through to the remaining checks rather than crash the log call. } if ( typeof process !== 'undefined' && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- a partial process global (bundler shims) may lack versions typeof process.versions?.node === 'string' ) { return 'dir' } return 'arg' } /** * `JSON.stringify` hardened for debug payloads: circular references collapse * to `"[Circular]"`, `Error` instances expand to `name`/`message`/`stack` * (they would otherwise stringify to `{}`), and `bigint` values become * strings (they would otherwise throw). Never throws — falls back to * `String(value)` and, if even that coercion throws, a placeholder. */ function stringifyMetaSafely(value: unknown): string { const seen = new WeakSet<object>() try { return JSON.stringify( value, (_key, entry: unknown) => { if (typeof entry === 'bigint') return entry.toString() if (entry instanceof Error) { return { name: entry.name, message: entry.message, stack: entry.stack, } } if (typeof entry === 'object' && entry !== null) { if (seen.has(entry)) return '[Circular]' seen.add(entry) } return entry }, 2, ) } catch { try { return String(value) } catch { return '[Unserializable meta]' } } } /** * Default `Logger` implementation that routes each level to the matching * `console` method: * * - `debug` → `console.debug` * - `info` → `console.info` * - `warn` → `console.warn` * - `error` → `console.error` * * When a `meta` object is supplied it is rendered with the strategy that * actually surfaces it on the current runtime (see {@link MetaStrategy}): * depth-unlimited `console.dir` on Node, circular-safe JSON on Cloudflare * Workers, and an extra console argument everywhere else. * * This is the logger used when `debug` is enabled on any activity and no * custom `logger` is supplied via `debug: { logger }`. */ export class ConsoleLogger implements Logger { /** Log a debug-level message; forwards to `console.debug`. */ debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta) } /** Log an info-level message; forwards to `console.info`. */ info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta) } /** Log a warning-level message; forwards to `console.warn`. */ warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta) } /** Log an error-level message; forwards to `console.error`. */ error(message: string, meta?: Record<string, unknown>): void { this.emit('error', message, meta) } private emit( level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: Record<string, unknown>, ): void { if (meta === undefined) { console[level](message) return } switch (resolveMetaStrategy()) { case 'dir': console[level](message) console.dir(meta, DIR_OPTIONS) break case 'json': console[level](`${message}\n${stringifyMetaSafely(meta)}`) break case 'arg': console[level](message, meta) break } } }