/** * Stateful scrubber for reasoning/thinking blocks in streamed assistant text. * * When a model streams `...` blocks across multiple SSE deltas, * naive per-delta regex destroys open/close tag state, leaking inner content * to the consumer. This module holds a buffer across `feed()` calls so partial * tags at delta boundaries are resolved before any text is emitted. * * Ported from Hermes `agent/think_scrubber.py` (StreamingThinkScrubber). * * @task T9275 * @epic T9261 */ /** * Stateful streaming reasoning-block scrubber. * * Holds a buffer across deltas so partial open/close tags spanning a delta * boundary do not leak inner content to the consumer. * * Usage: * ```ts * const s = new StreamingThinkScrubber(); * for (const delta of stream) { * const visible = s.feed(delta); * if (visible) emit(visible); * } * const tail = s.flush(); * if (tail) emit(tail); * ``` * * Call {@link reset} at the start of every new turn so a hung block from an * interrupted prior stream cannot taint the next turn's output. */ export declare class StreamingThinkScrubber { private buf; private inBlock; /** True when the last emission ended with `\n`, or nothing has been emitted yet. */ private lastEmittedEndedNewline; /** * Feed one delta. Returns the visible portion with reasoning blocks stripped. * * May return an empty string when the entire delta is reasoning content or is * being held back pending resolution of a partial tag at the boundary. */ feed(text: string): string; /** * Flush any remaining buffered text. Call at end-of-stream. * * If still inside an unterminated reasoning block the held-back content is * discarded — leaking partial reasoning is worse than a truncated answer. * Otherwise the held-back partial-tag tail is emitted verbatim (it turned * out not to be a real tag prefix). */ flush(): string; /** Reset all internal state. Call at the start of every new turn. */ reset(): void; /** * Return `[index, tagLength]` of the earliest open tag that sits at a * block boundary in `buf`, or `[-1, 0]` if none found. * * A block boundary is: * - position 0 when the last emission ended with `\n` (or nothing emitted) * - any position preceded only by whitespace since the last `\n` in `buf` * (with the same newline-ended prior-emission requirement when no `\n` is * present in the preceding text) * * Closed pairs (`X`) are handled separately and are NOT * subject to boundary gating. */ private findOpenAtBoundary; /** * Return true if position `idx` in `buf` is a block boundary. * * Rules: * - `idx === 0`: boundary iff the last emitted chunk (this feed call or the * cross-feed flag) ended with `\n`, or nothing has been emitted yet. * - `idx > 0`: boundary iff every character since the last `\n` in * `buf.slice(0, idx)` is whitespace; when there is no `\n` in that slice * the same newline-ended prior-emission requirement applies. */ private isBlockBoundary; } /** * Scrub all reasoning blocks from a complete (non-streaming) string. * * Convenience wrapper around {@link StreamingThinkScrubber} for callers that * already have the full response text. */ export declare function scrubReasoning(text: string): string; //# sourceMappingURL=think-scrubber.d.ts.map