/**
* 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