/** * Shared text smoother for display-layer streaming. * * Provider chunk boundaries are arbitrary and vary across OpenAI, OpenRouter, * Anthropic, etc. Fred's raw stream (`fullStream`) is the canonical semantic * stream and must not be altered. This module provides a display-layer * smoother that normalises the *visual cadence* of assistant text output, * independent of upstream chunk sizes. * * Design: * - Only operates on assistant text chunks. Non-text events (tool-call, * tool-result, usage, run-end, errors) must be handled by the caller * and are not touched by this module. * - Callers should flush pending text before processing non-text events * when ordering matters. * - The smoother is frontend-agnostic: CLI, web, SSE adapters, etc. can * all use the same utility. * * Modelled after Vercel's `smoothStream()` behaviour, adapted to Fred's * event model. * * @module */ /** * Chunking strategy for splitting buffered text into display units. * * - `'word'` Split on whitespace boundaries (default). Good for Latin scripts. * - `'line'` Split on newline boundaries. Useful for code or structured output. * - `RegExp` Custom regex whose first match is emitted as a chunk. * - `Intl.Segmenter` Unicode-aware word segmentation (ideal for CJK, Thai, etc.). * - `(buffer: string) => string | null | undefined` Fully custom extractor. * Return the next chunk to emit, or null/undefined to wait for more input. */ export type ChunkingStrategy = 'word' | 'line' | RegExp | Intl.Segmenter | ((buffer: string) => string | null | undefined); /** * Options for {@link createTextSmoother}. */ export interface TextSmootherOptions { /** * Called for each display chunk emitted by the smoother. * * `tokenCount` is an accounting hint: 1 for the first visual segment of * each logical AI token, 0 for subsequent sub-word segments. This allows * consumers to track token throughput without double-counting. */ onChunk: (chunk: string, tokenCount?: number) => void; /** * Delay in milliseconds between emitted display chunks. * * - `null` or `0` disables the timer and flushes chunks synchronously on push. * - Defaults to `12`. */ delayMs?: number | null; /** * How incoming text is split into display-sized chunks. * Defaults to `'word'`. */ chunking?: ChunkingStrategy; } /** * A text smoother instance returned by {@link createTextSmoother}. */ export interface TextSmoother { /** * Feed a text chunk from the AI stream into the smoother. * The text is buffered and drip-fed to `onChunk` according to the * configured chunking strategy and delay. */ push(text: string): void; /** * Synchronously emit any buffered text that matches the chunking strategy * without waiting for the timer. Leaves partial/unmatched text in the * buffer. */ flush(): void; /** * Synchronously drain *all* buffered text, including any partial remainder * that has not yet matched the chunking boundary. Stops the timer. */ flushAll(): void; /** * Drop all queued text and stop the timer. * Use on handoff, error, or when abandoning the current response. */ clear(): void; /** * Stop the internal timer without draining the buffer. * Queued text remains available for a subsequent `flush()` / `flushAll()`. */ stop(): void; } /** * Create a text smoother that normalises the visual cadence of streamed * assistant text. * * ```ts * const smoother = createTextSmoother({ * onChunk: (chunk) => process.stdout.write(chunk), * delayMs: 12, * chunking: 'word', * }); * * for await (const event of fullStream) { * if (event.type === 'token') { * smoother.push(event.delta); * } else { * smoother.flush(); // drain pending text before non-text events * handleEvent(event); // tool-call, run-end, etc. * } * } * smoother.flushAll(); // drain any remainder * ``` */ export declare function createTextSmoother(options: TextSmootherOptions): TextSmoother; /** * Options for {@link smoothStream}. */ export interface SmoothStreamOptions { /** * Delay in milliseconds between emitted display chunks. * * - `null` or `0` disables delays (chunks still split by strategy but emitted synchronously). * - Defaults to `10`. */ delayMs?: number | null; /** * How incoming text is split into display-sized chunks. * Defaults to `'word'`. */ chunking?: ChunkingStrategy; /** * Internal. For test use only. May change without notice. * Overrides the delay function for deterministic testing. */ _delay?: (ms: number) => Promise; } /** * Transform an `AsyncIterable` to smooth text output. * * This is the architecturally correct approach to text smoothing for * Fred's streaming pipeline. Unlike the timer-based {@link createTextSmoother}, * this function transforms the async iterable itself, inserting real `await` * delays between word-level chunks. This ensures the event loop yields * between chunks regardless of how fast the upstream produces events. * * **How it works:** * - `token` events are buffered and split into display segments using the * configured chunking strategy. * - Each segment is re-emitted as a synthetic `token` event with an * `await delay()` between emissions. * - Non-token events flush the buffer immediately (no delay) and pass through. * - When the stream ends, any remaining buffer is flushed. * * Modelled after Vercel AI SDK's `smoothStream()` TransformStream approach, * adapted to Fred's `AsyncIterable` model. * * @example * ```ts * import { smoothStream } from '@fancyrobot/fred/stream'; * * const smooth = smoothStream({ delayMs: 10, chunking: 'word' }); * const smoothed = smooth(streamResult.fullStream); * * for await (const event of smoothed) { * if (event.type === 'token') { * process.stdout.write(event.delta); * } * } * ``` */ export declare function smoothStream(options?: SmoothStreamOptions): (source: AsyncIterable) => AsyncIterable; //# sourceMappingURL=smooth-text.d.ts.map