import { ENCODE_METHOD, DECODE_METHOD } from "../utils/encoder_symbols"; import type { AdkEncodableSnapshot } from "./encodable"; import type { DispatchContext } from "../contracts/dispatch_context"; /** * A DYNAMIC Tokenizable value: a function evaluated at prompt-ASSEMBLY time with the live dispatch * context, so the wrapped content computes itself coherent with the dispatch it ships in (e.g. a * citation instruction that adapts to whether the answer tool survived the subtractive-pass shed). * * @remarks * The context is OPTIONAL: the same Tokenizable is also read outside any dispatch (token measurement, * serialization, plain string coercion). The evaluator MUST therefore handle `ctx === undefined` and * return a string for EVERY input — that no-ctx branch IS its fallback. It must never throw and must * never return a non-string; either violation raises {@link @nhtio/adk!E_TOKENIZABLE_EVALUATOR_INVALID} * (loud — a mis-authored evaluator must not silently coerce garbage into the prompt). Keep evaluators * serializer-friendly (capture only module-level refs or values passed as explicit bindings, NOT live * per-turn state) so a dynamic Tokenizable round-trips its evaluator and stays dynamic across encode/decode. */ export type TokenizableEvaluator = (ctx?: DispatchContext) => string; /** * The set of supported token encoding identifiers. * * @remarks * Each value maps to a specific estimation backend: * - `gpt2`, `r50k_base`, `p50k_base`, `p50k_edit`, `cl100k_base`, `o200k_base` — exact counts * via `js-tiktoken` (OpenAI / tiktoken-compatible models). * - `gemini` — exact counts via `@lenml/tokenizer-gemini`, which embeds Gemini's actual * SentencePiece vocabulary locally with no API call required. * - `gemma` — exact counts for Google's Gemma models (Gemma 2/3/4, incl. the on-device * `.litertlm` / ONNX builds). Backed by the SAME `@lenml/tokenizer-gemini` package, whose bundled * `tokenizer_config.json` declares `"tokenizer_class": "GemmaTokenizer"` over the shared 256k-vocab * SentencePiece tokenizer — Gemini and Gemma share it, and it encodes Gemma's control tokens * (``, ``, ``, …) as single ids. Deliberate reuse, not a proxy: no * extra dependency. Distinct identifier so callers can say what model they mean. * - `llama2` — exact counts via `llama-tokenizer-js` (Llama 1 and 2). Llama 3+ uses a * different vocabulary and should use the `llama3` identifier once a suitable sync backend * is available. * - `claude` — heuristic approximation using Anthropic's published ~3.5 chars/token ratio. * No local tokenizer is available for Claude 3+ models; the Anthropic SDK's * `messages.countTokens()` API is the only exact path but requires a network call. * * This array is the CANONICAL, closed set of backends built into core — adding one of these * requires editing core (add a case to {@link Tokenizable.estimateTokens}'s internal switch). For * every OTHER encoding a battery or consumer wants to measure (a model-specific tokenizer core has * no business knowing about), call {@link registerTokenEstimator} instead — no core edit required. * See {@link TokenEncodingId} for the widened identifier type that accepts both. */ export declare const TokenEncoding: readonly [ "gpt2", "r50k_base", "p50k_base", "p50k_edit", "cl100k_base", "o200k_base", "gemini", "gemma", "llama2", "claude" ]; /** * Union of all recognised token encoding identifier strings. * * @remarks * Derived from {@link TokenEncoding} so the type and the runtime array stay in sync * automatically when new encodings are added. */ export type TokenEncoding = (typeof TokenEncoding)[number]; /** * A recognised token-encoding identifier: one of the closed {@link TokenEncoding} built-ins, OR any * other string registered via {@link registerTokenEstimator}. * * @remarks * The `(string & {})` half of the union is a widening trick, not a real intersection — `string & {}` * has no members beyond `string`, so it accepts any string value while `TokenEncoding` still * contributes its literal members to editor autocomplete (a bare `string` union member would erase * that autocomplete entirely, since TypeScript collapses `TokenEncoding | string` to `string`). Use * this type wherever an API accepts "a built-in encoding, or a custom one" — including the `tokenEncoding` * property on battery adapter options, which treat the built-ins as suggestions but permit any string. */ export type TokenEncodingId = TokenEncoding | (string & {}); /** * A custom token-count function registered for a non-built-in {@link TokenEncodingId} via * {@link registerTokenEstimator}. * * @remarks * Synchronous, matching the built-in estimation backends: every backend behind the * {@link TokenEncoding} switch (`js-tiktoken`, `@lenml/tokenizer-gemini`, `llama-tokenizer-js`, the * Claude heuristic) resolves synchronously, and {@link Tokenizable.estimateTokens} itself returns a * plain `number` — not a `Promise`. A custom estimator therefore must not be async either, so * a registered encoding stays a drop-in peer of the built-ins (same call shape, no `await` threaded * through `estimateTokens` for some encodings and not others). Wrap an inherently-async tokenizer in a * synchronous cache (measure ahead of time / memoize a warmed instance) before registering it. * * @param value - The already-RESOLVED text to count (the same string `countFor` measures for the * built-in backends — i.e. `render(ctx)`'s output, never the raw evaluator or ctx). * @returns The estimated token count for `value`. */ export type TokenEstimatorFn = (value: string) => number; /** * Thrown by {@link registerTokenEstimator} when the given encoding identifier names a built-in * {@link TokenEncoding} rather than a genuinely custom one. * * @remarks * The built-in encodings are canonical: `'gemma'`, `'cl100k_base'`, and friends must always resolve * to their core backend (`js-tiktoken`, `@lenml/tokenizer-gemini`, `llama-tokenizer-js`, the Claude * heuristic), never to a consumer-supplied override. Allowing a shadow registration would let one * battery silently change another's token counts for a shared built-in name — a correctness hazard * that is cheap to prevent at registration time. Fatal: this is a programming error in the caller * (pick a distinct identifier), not a runtime condition to recover from. */ export declare const E_TOKEN_ESTIMATOR_SHADOWS_BUILTIN: import("../utils/exceptions").CreatedException<[ string ]>; /** * Register (or replace) a {@link TokenEstimatorFn} for a custom {@link TokenEncodingId}, so * {@link Tokenizable.estimateTokens} can measure it without a core code change. * * @remarks * This is the additive escape hatch for the closed {@link TokenEncoding} set. Say a context-management * battery needs to measure tokens for a model that core has no built-in backend for — it simply calls * this once, at startup, for that model's identifier. From then on the encoding is measurable, with no * core code change (a new case in {@link Tokenizable.estimateTokens}'s switch) ever required. Mirrors the * {@link @nhtio/adk!registerMediaReaderResolver} / {@link @nhtio/adk!registerSpoolReaderResolver} registry * idiom used for reader handles — register a factory once, core (or the primitive) consults it, nobody * imports a battery from core. * * Resolution order inside {@link Tokenizable.estimateTokens}: the built-in switch is tried FIRST (so * the closed set's fast path — including its degrade-to-heuristic-on-encoder-failure contract — is * completely unaffected by the registry existing), and only an encoding the switch doesn't recognise * falls through to this registry. * * Idempotent-by-overwrite: registering the same `encoding` twice replaces the previous estimator — * useful for hot-swapping a warmed tokenizer instance without restarting. Registering a name that * shadows a BUILT-IN {@link TokenEncoding} (e.g. `'gemma'`, `'cl100k_base'`) is rejected: the built-ins * are canonical and must always resolve to their core backend, never to a consumer override. * * @param encoding - The custom encoding identifier this estimator handles. Must not be one of the * built-in {@link TokenEncoding} values. * @param estimator - The synchronous token-count function to call for `encoding`. * @throws {@link @nhtio/adk!E_TOKEN_ESTIMATOR_SHADOWS_BUILTIN} when `encoding` names a built-in. */ export declare function registerTokenEstimator(encoding: string, estimator: TokenEstimatorFn): void; /** * A mutable string with a built-in token counter. * * @remarks * The wrapped string can be read via the standard coercion protocol and updated at any time via * {@link Tokenizable.set}. Token counts are computed lazily on first access per encoding and * cached until the value changes, avoiding redundant encoder invocations when the same content * is measured multiple times across a pipeline. * * Estimation is dispatched by encoding identifier — see {@link TokenEncoding} for the full list of * built-in backends and their accuracy characteristics, and {@link registerTokenEstimator} for adding * more without a core change. An encoding that is neither a built-in nor registered resolves to * `undefined` (see {@link Tokenizable.estimateTokens}) — this pre-dates the registry and is unchanged * by it. Separately, a built-in encoder that THROWS while measuring (as opposed to an unrecognised * name) degrades to a `ceil(length / 3.5)` character heuristic inside a runner execution — see * `degradeOrThrow` and `utils/estimation_context`. * * The class implements the standard JS value-coercion protocol (`toString`, `valueOf`, * `toJSON`, `toLocaleString`, `Symbol.for('nodejs.util.inspect.custom')`) so instances behave * transparently as strings in most contexts. */ export declare class Tokenizable { #private; /** The set of supported token-encoding identifiers, re-exposed as a static for convenience. */ static TokenEncoding: readonly [ "gpt2", "r50k_base", "p50k_base", "p50k_edit", "cl100k_base", "o200k_base", "gemini", "gemma", "llama2", "claude" ]; /** * Validator schema that accepts a plain `string` or a {@link Tokenizable} instance. * * @remarks * Reusable fragment for any schema that wants to accept either form — for example, * `systemPrompt` and each item in `standingInstructions` in `turnContextSchema`. */ static schema: import("@nhtio/validation").AlternativesSchema; /** * Variant of {@link Tokenizable.schema} that additionally accepts the EMPTY string. * * @remarks * For fields where "present but empty" is a legitimate state rather than a mistake — e.g. * {@link @nhtio/adk!Thought.content} in opaque-replay mode, where the meaning lives in the vendor * `payload` and the prose is only kept for token-accounting and observer inspection. * * Do NOT reach for this by default. {@link Tokenizable.schema} stays strict precisely because an * empty system prompt or a blank standing instruction is a bug worth failing on. */ static emptyableSchema: import("@nhtio/validation").AlternativesSchema; toJSON: () => string; toString: () => string; valueOf: () => string; toLocaleString: () => string; /** Replace the wrapped value (string or evaluator) and invalidate the cached token estimates. */ set: (value: string | TokenizableEvaluator) => void; /** * Resolve the wrapped content against an OPTIONAL dispatch context and return the string. For a static * value the context is ignored. For a dynamic (evaluatable) value the evaluator is invoked with `ctx` * ({@link TokenizableEvaluator}); assembly passes the live context so the content matches the dispatch * it ships in, while a no-context call returns the evaluator's `undefined`-branch fallback. */ /** Whether the current wrapped value is evaluator-backed rather than a static string. */ readonly dynamic: boolean; /** Resolve the value against an optional dispatch context. */ render: (ctx?: DispatchContext) => string; /** * Estimate the token count under the given {@link TokenEncodingId} of the string this Tokenizable * resolves to for the OPTIONAL context — i.e. of `render(ctx)`. Passing the same `ctx` assembly uses * keeps the budget count honest for dynamic content (it measures exactly what will ship). Accepts * both a built-in {@link TokenEncoding} and any encoding registered via {@link registerTokenEstimator}. */ estimateTokens: (encoding: TokenEncodingId, ctx?: DispatchContext) => number; /** * @param value - The initial value to wrap: a plain `string` (static) or a {@link TokenizableEvaluator} * evaluated at assembly time (dynamic). */ constructor(value: string | TokenizableEvaluator); /** * Convenience overload for one-off token counting without managing a {@link Tokenizable} instance. * * @remarks * Creates a temporary instance and immediately discards it — no caching benefit. Use the * instance method when you need to count the same value under multiple encodings or when the * value may change over time. * * @param value - The string (or {@link TokenizableEvaluator}) to count tokens for. * @param encoding - The encoding identifier to use for counting — a built-in {@link TokenEncoding} or * any encoding registered via {@link registerTokenEstimator}. * @param ctx - Optional dispatch context; for a dynamic value it selects which resolved string is * counted (so the count matches what assembly ships). Ignored for a static string. * @returns The estimated number of tokens. */ static estimateTokens(value: string | TokenizableEvaluator, encoding: TokenEncodingId, ctx?: DispatchContext): number; /** * Returns `true` if `value` is a {@link Tokenizable} instance. * * @remarks * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety — `instanceof` would fail for instances * created in a different module copy or VM context. * * @param value - The value to test. * @returns `true` when `value` is a {@link Tokenizable} instance. */ static isTokenizable(value: unknown): value is Tokenizable; /** * Serialise this Tokenizable into an `@nhtio/encoder` snapshot. * * @remarks * The wrapped VALUE is the entire state; the token-count caches are derived and deliberately not encoded * (they rebuild lazily after decode). For a STATIC value the snapshot is the string. For a DYNAMIC value * the snapshot is the EVALUATOR FUNCTION itself — `@nhtio/encoder` serialises functions (source + * explicit bindings), so a dynamic Tokenizable round-trips its evaluator and stays dynamic, re-evaluating * live on the next assembly (it does NOT downgrade to a frozen string). Evaluators must therefore stay * serializer-friendly: capture only module-level refs / explicit bindings, not live per-turn state. * Round-trips via {@link Tokenizable.[DECODE_METHOD]}. * * @returns The wrapped string, or the evaluator function for a dynamic value. */ [ENCODE_METHOD](): AdkEncodableSnapshot; /** * Reconstruct a {@link Tokenizable} from an {@link Tokenizable.[ENCODE_METHOD]} snapshot. * * @param data - The wrapped string (static) or evaluator function (dynamic) produced by * {@link Tokenizable.[ENCODE_METHOD]}. * @returns A fresh {@link Tokenizable} over the same value. */ static [DECODE_METHOD](data: AdkEncodableSnapshot): Tokenizable; } /** * Returns `true` if `value` is a {@link Tokenizable} instance. * * @remarks * Module-level convenience alias for {@link Tokenizable.isTokenizable}. Prefer this form when * you need a standalone type guard without importing the full class. * * Uses {@link @nhtio/adk!isInstanceOf} for cross-realm safety — `instanceof` would fail for instances * created in a different module copy or VM context. * * @param value - The value to test. * @returns `true` when `value` is a {@link Tokenizable} instance. */ export declare const isTokenizable: (value: unknown) => value is Tokenizable;