/** * TokenBuffer -- ring buffer that absorbs bursty LLM token arrival * and emits at smooth cadence. * * Backed by a pre-allocated ring (capacity fixed at construction; `new Array` * once, never on the hot path). The ZERO-ALLOCATION hot path is `push` + * {@link TokenBufferShape.drainInto}: `push` writes one slot and bumps three * integers (no allocation); `drainInto(sink, maxCount)` copies the drained * tokens into a CALLER-OWNED array and returns the count written — it allocates * NOTHING, so a host that reuses one scratch array drains at steady state with * zero per-op heap traffic. This is the path a streaming consumer runs. * * The convenience {@link TokenBufferShape.drain} returns a FRESH `T[]` each call * (one array allocation per drain, inherent to handing back an owned array) — it * is the ergonomic accessor, NOT the zero-alloc path. Reach for `drainInto` on a * hot loop; reach for `drain` when a fresh array is what you want. * * EMA (exponential moving average) for rate estimation. * Stall detection: buffer empty + `gen < consume`. * * The zero-allocation of `push`/`drainInto` is MEASURED + PINNED by * `tests/property/token-buffer-zero-alloc.test.ts` (the allocation gate), which * forces GC between batches and asserts the live per-op heap growth is ≈ 0. * * @module */ import { type Clock } from './clock.js'; interface TokenBufferShape { push(token: T): void; /** * Convenience drain — returns a FRESH array of up to `maxCount` tokens (the * whole buffer when omitted). Allocates one array per call; for a zero-alloc * hot loop use {@link TokenBufferShape.drainInto} instead. */ drain(maxCount?: number): T[]; /** * ZERO-ALLOCATION drain — copy up to `maxCount` drained tokens into the * caller-owned `sink` (reused scratch), starting at index 0, and return the * count written. Allocates nothing: the host owns and reuses `sink`. `maxCount` * defaults to the current occupancy; the actual count is clamped to both the * occupancy AND `sink.length` (a sink shorter than the request drains only what * fits, leaving the rest buffered — never an out-of-bounds write). Only indices * `[0, count)` of `sink` are written; the caller reads exactly that prefix. */ drainInto(sink: T[], maxCount?: number): number; reset(): void; readonly occupancy: number; readonly generationRate: number; readonly consumptionRate: number; readonly isStalled: boolean; readonly length: number; readonly capacity: number; } interface TokenBufferConfig { readonly capacity?: number; readonly emaAlpha?: number; /** * Injected time source for rate estimation. Defaults to {@link systemClock} * (the declared entropy boundary); a test passes a {@link manualClock} so the * EMA rates become a deterministic function of the advances it makes. */ readonly clock?: Clock; } declare function _make(config?: TokenBufferConfig): TokenBufferShape; /** * TokenBuffer — ring buffer that absorbs bursty LLM token arrival and hands * tokens out at a smooth cadence. The `push` + `drainInto` path is genuinely * zero-allocation (measured, pinned); `drain` is the allocating convenience. * Reports stall via `isStalled` and rate via an internal EMA. */ export declare const TokenBuffer: { /** Build a new buffer — pass capacity or reuse defaults. */ make: typeof _make; }; export declare namespace TokenBuffer { /** Structural shape of a token buffer: `push`, `drain`, `reset`, stall/rate accessors. */ type Shape = TokenBufferShape; /** Configuration accepted by {@link TokenBuffer.make}. */ type Config = TokenBufferConfig; } export {}; //# sourceMappingURL=token-buffer.d.ts.map