import { ToolRegistry } from "./tool_registry"; import { TokenEncoding } from "./tokenizable"; import { ENCODE_METHOD, DECODE_METHOD } from "../utils/encoder_symbols"; import type { ObjectSchema } from '@nhtio/validation'; import type { AdkEncodableSnapshot } from "./encodable"; import type { SpoolReader } from "../contracts/spool_reader"; import type { DispatchContext } from "../contracts/dispatch_context"; import type { ReaderDescriptor } from "../contracts/reader_descriptor"; /** * Constructor signature for {@link SpooledArtifact} and any subclass. * * @remarks * Used by {@link @nhtio/adk!Tool} to declare the artifact subclass the consumer should use when wrapping * the handler's serialised output. The variadic rest parameter accommodates subclass-specific * constructor arguments (e.g. `SpooledJsonArtifact(reader, format?)`). * * @typeParam A - The {@link SpooledArtifact} subtype the constructor produces. */ export type SpooledArtifactConstructor = new (reader: SpoolReader, ...rest: any[]) => A; /** * Metadata table entry for one of the artifact's existing query methods, used by * {@link SpooledArtifact.forgeTools} to surface that method as an {@link @nhtio/adk!ArtifactTool}. * * @remarks * This is a metadata shape, not a general method → tool pipeline. `forgeTools` knows how to * marshal arguments for a fixed, closed set of method names (the base seven on * {@link SpooledArtifact} and the JSON/Markdown methods on the bundled subclasses); a * descriptor is the place to attach a tool name, description, args schema, and optional * serializer to one of those methods. Adding a descriptor for a method whose name is not * in that closed set will produce a tool whose handler invokes the method with no arguments. * * For new methods that require custom argument marshalling, branching, multi-step logic, * cross-artifact joins, or any other behaviour beyond "call this existing method," override * {@link SpooledArtifact.forgeTools} and mint the {@link @nhtio/adk!ArtifactTool} directly — do not try * to express it through a descriptor. * * Zero-arg methods are the exception: a descriptor with no `argsSchema` (or one that adds * only `callId`-adjacent fields you don't consume) works for any method that takes no * arguments, regardless of name. */ export interface ToolMethodDescriptor { /** Absolute tool name as exposed to the LLM (e.g. `'artifact_head'`). */ name: string; /** Method to invoke on the resolved artifact instance (e.g. `'head'`, `'json_get'`). */ method: string; /** Human-readable description passed to the model. Should mention "in this turn" so the model understands the artifact's lifecycle scope. */ description: string; /** Schema for the method's own args, NOT including `callId`. `forgeTools()` injects `callId`. */ argsSchema?: ObjectSchema; /** Optional formatter for non-string return values. Defaults: string → as-is; string[] → newline-join; number → `String(n)`; otherwise `JSON.stringify(value, null, 2)`. */ serialise?: (result: unknown) => string; } /** * Returns the effective artifact tool descriptors from a constructor's static prototype chain. * * Core subclasses declare only their own `toolMethods`; a static property shadows its ancestor * rather than concatenating it. This walks leaf-first and deduplicates by `name`, the absolute * model-facing tool identifier, rather than `method`, which is the instance dispatch name and may * legitimately differ (and is not the vocabulary exposed to callers). The nearest declaration * wins, matching tool collision replacement semantics. * * @param ctor The artifact constructor whose effective descriptors are wanted. * @returns A frozen, leaf-first array of descriptors, or an empty array when none are declared. */ export declare const effectiveToolMethods: (ctor: unknown) => readonly ToolMethodDescriptor[]; /** * Collect artifact IDs from the current turn's tool calls and retrievables that are instances of a specific artifact class. * * @param ctx - The dispatch context containing tool calls and retrievables. * @param requires - The artifact class constructor to filter by. * @returns An array of unique artifact IDs matching the specified class, with collision detection. * @throws {@link @nhtio/adk!E_ARTIFACT_ID_COLLISION} when the same ID appears in both tool calls and retrievables. */ export declare function collectArtifactCompatibleIds(ctx: { turnToolCalls: Iterable<{ id: string; fromArtifactTool?: boolean; results: unknown; }>; turnRetrievables: Iterable<{ id: string; inline: boolean; content: unknown; }>; }, requires: SpooledArtifactConstructor): string[]; /** * Resolve an artifact by ID from the current turn's tool calls or retrievables, with type narrowing. * * @param ctx - The dispatch context containing tool calls and retrievables. * @param id - The artifact ID to resolve. * @param requires - The artifact class constructor to narrow by; only artifacts of this type are returned. * @returns The resolved artifact and its source (tool call or retrievable), or undefined if not found. */ export declare function resolveArtifactById(ctx: { turnToolCalls: Iterable<{ id: string; fromArtifactTool?: boolean; results: unknown; }>; turnRetrievables: Iterable<{ id: string; inline: boolean; content: unknown; }>; }, id: string, requires: SpooledArtifactConstructor): { artifact: SpooledArtifact; source: 'toolCall' | 'retrievable'; } | undefined; /** * Default serialiser for {@link @nhtio/adk!ArtifactTool} handler return values when a descriptor does not * provide its own. Exported for reuse by subclass `forgeTools` overrides. * * @param result - The artifact-method return value. * @returns A string suitable for inclusion in an LLM tool-call response. */ export declare const defaultSerialise: (result: unknown) => string; /** * A lazy, line-oriented view over an arbitrary backing store. * * @remarks * All I/O methods are async to remain compatible with both in-memory and streaming * {@link @nhtio/adk!SpoolReader} implementations. Token estimation delegates to * {@link @nhtio/adk!Tokenizable.estimateTokens} — the same backends used elsewhere in the ADK. * * The class is read-only by design: mutation of the underlying data is the responsibility of the * producer that created the {@link @nhtio/adk!SpoolReader}, not the consumer reading from this artifact. */ export declare class SpooledArtifact { #private; /** * The set of artifact-query methods this class surfaces via {@link SpooledArtifact.forgeTools}. * * @remarks * The base set covers the generic line-oriented operations every artifact supports: * `artifact_head`, `artifact_tail`, `artifact_grep`, `artifact_cat`, `artifact_byte_length`, * `artifact_line_count`, `artifact_estimate_tokens`. Each `toolMethods` array lists **only** * its own class's descriptors — subclasses do not concatenate inherited descriptors. The * subclass instead overrides {@link SpooledArtifact.forgeTools} to merge the base registry * (produced by `SpooledArtifact.forgeTools(ctx)`) with its own — see * {@link @nhtio/adk!SpooledJsonArtifact.forgeTools} and {@link @nhtio/adk!SpooledMarkdownArtifact.forgeTools} for * the canonical shape and the pattern downstream consumers should follow when building * their own `SpooledArtifact` subclasses. * * Tool names are absolute (not subclass-prefixed). Forged tools carry * `Tool.onCollision = 'replace'` so merging multiple subclasses' `forgeTools()` outputs is * silent — every same-named tool dispatches the same method on whatever artifact the * `callId` resolves to, so the overlap is behaviourally interchangeable. * * Frozen at module load. */ static toolMethods: ReadonlyArray; /** * @param reader - The backing store to read from. * @throws {@link @nhtio/adk!E_NOT_A_SPOOL_READER} when `reader` does not implement {@link @nhtio/adk!SpoolReader}. */ constructor(reader: SpoolReader); /** * Emit the backing reader's serialisable {@link ReaderDescriptor}, or throw if it cannot describe * itself. * * @remarks * `protected` so subclasses can build their own encode snapshots over the base reader (which is * otherwise private). Throws {@link @nhtio/adk!E_READER_NOT_DESCRIBABLE} when the reader has no * `describe()` (or returns `undefined`) — there is no serialisable handle to write. * * @returns The reader's tagged handle descriptor. * @throws {@link @nhtio/adk!E_READER_NOT_DESCRIBABLE} when the reader is not describable. */ protected readerDescriptor(): ReaderDescriptor; /** * Serialise this SpooledArtifact into an `@nhtio/encoder` snapshot — the reader **handle**, not the * bytes. * * @remarks * Emits the backing reader's {@link ReaderDescriptor}; decode re-binds the reader through the * registered resolver. Throws {@link @nhtio/adk!E_READER_NOT_DESCRIBABLE} when the reader cannot * describe itself. Subclasses override this to include their own discriminators (e.g. * {@link @nhtio/adk!SpooledJsonArtifact} adds `format`). * * @returns A snapshot consumed by {@link SpooledArtifact.[DECODE_METHOD]}. */ [ENCODE_METHOD](): AdkEncodableSnapshot; /** * Reconstruct a {@link SpooledArtifact} from an {@link SpooledArtifact.[ENCODE_METHOD]} snapshot. * * @remarks * Re-binds the reader via {@link @nhtio/adk!resolveSpoolReader}; throws * {@link @nhtio/adk!E_NO_READER_RESOLVER} when no resolver is registered for the descriptor's tag. * * @param data - The snapshot produced by {@link SpooledArtifact.[ENCODE_METHOD]}. * @returns A fresh {@link SpooledArtifact} backed by a freshly-resolved reader. */ static [DECODE_METHOD](data: AdkEncodableSnapshot): SpooledArtifact; /** * Returns the line at the given 0-based index, or `undefined` when out of range. * * @remarks * Protected so subclasses can scan the backing store line-by-line without allocating * intermediate arrays. Delegates directly to the {@link @nhtio/adk!SpoolReader}. * * @param index - 0-based line index. * @returns The raw line string, or `undefined` when out of range. */ protected line(index: number): Promise; /** * Returns `true` if `value` is a {@link SpooledArtifact} instance (including any subclass). * * @remarks * Uses the cross-realm-safe {@link @nhtio/adk!isInstanceOf} guard: `instanceof` first, then * `Symbol.hasInstance`, then a `constructor.name` fallback. Subclass instances (e.g. * {@link @nhtio/adk!SpooledJsonArtifact}) satisfy this guard because `instanceof` walks the prototype * chain. The fallbacks handle the dual-module-copy case where two distinct `SpooledArtifact` * classes coexist in the same realm (e.g. one bundled into a downstream library, one in the * consumer's `node_modules`). * * @param value - The value to test. * @returns `true` when `value` is a {@link SpooledArtifact} instance. */ static isSpooledArtifact(value: unknown): value is SpooledArtifact; /** * Returns `true` if `value` is a constructor function whose prototype chain includes * {@link SpooledArtifact} (including `SpooledArtifact` itself). * * @remarks * Used by {@link @nhtio/adk!Tool} to validate the optional `artifactConstructor` field. Performs an * `instanceof`-based check on the prototype chain; falls back to a duck-type test that looks * for the canonical SpooledArtifact instance methods on `value.prototype` for cross-realm * safety (constructors passed from a different module copy or VM context). * * @param value - The value to test. * @returns `true` when `value` is a constructor for `SpooledArtifact` or a subclass. */ static isSpooledArtifactConstructor(value: unknown): value is SpooledArtifactConstructor; /** * Returns the first `n` lines of the artifact. * * @remarks * If the artifact contains fewer than `n` lines, all available lines are returned. Matches the * behaviour of POSIX `head -n`. * * @param n - Number of lines to return. Defaults to 10. * @returns Array of line strings, without trailing newlines. */ head(n?: number): Promise; /** * Returns the last `n` lines of the artifact. * * @remarks * If the artifact contains fewer than `n` lines, all available lines are returned. Matches the * behaviour of POSIX `tail -n`. * * @param n - Number of lines to return. Defaults to 10. * @returns Array of line strings, without trailing newlines. */ tail(n?: number): Promise; /** * Returns all lines that match `pattern`. * * @remarks * Behaves like POSIX `grep`: each line is tested against the pattern and included in the result * when it matches. The pattern is applied as a JavaScript `RegExp`; flags (e.g. case- * insensitivity) should be encoded in the expression itself. * * Stateful flags (`g`, `y`) on the supplied `RegExp` would normally cause `pattern.test()` to * advance `lastIndex` across calls, producing skipped matches and order-dependent results. To * keep the per-line semantics stateless, `grep` resets `pattern.lastIndex` to `0` before each * line test. The forged `artifact_grep` tool also rejects `g` and `y` flags up-front at schema * validation time. * * @param pattern - The regular expression to test each line against. * @returns Array of matching line strings, in order. */ grep(pattern: RegExp): Promise; /** * Returns lines from the artifact, optionally bounded to a range. * * @remarks * Without arguments, returns all lines — equivalent to POSIX `cat`. With `start` and/or `end`, * behaves like `Array.prototype.slice`: `start` defaults to `0`, `end` defaults to the total * line count, and only lines in `[start, end)` are fetched from the backing store. For large * artifacts, prefer a bounded range or {@link SpooledArtifact.head} / {@link SpooledArtifact.tail}. * * @param start - 0-based start line index (inclusive). Defaults to `0`. * @param end - 0-based end line index (exclusive). Defaults to `lineCount()`. * @returns Array of line strings in the requested range. */ cat(start?: number, end?: number): Promise; /** * Returns the total byte length of the underlying data. * * @returns The byte length as reported by the {@link @nhtio/adk!SpoolReader}. */ byteLength(): Promise; /** * Returns whether producer-computed size metadata is available. * * @returns `true` when {@link SpooledArtifact._setSizeHints} has populated the cache. */ hasSizeHints(): boolean; /** * Returns the total number of lines in the artifact. * * @returns The line count as reported by the {@link @nhtio/adk!SpoolReader}. */ lineCount(): Promise; /** * Estimates tokens for the exact handle-body metadata rendered for this artifact. * * The fallback renderer is an interim core-safe implementation. Its output is deliberately * specified here for fan-in parity: the lines are the fixed prose and metadata strings below, * with one `\n` separator, and method entries formatted as `- name — description` (or `- name`). * The canonical renderer in `chat_common` should eventually delegate to this builder rather than * maintain a second copy. * * @param callId - The turn-local artifact identifier. * @param encoding - The token encoding used for estimation. * @param renderer - Optional renderer overriding the interim default. * @returns A synchronous token estimate. */ estimateHandleTokens(callId: string, encoding: TokenEncoding, renderer?: (input: { callId: string; artifact: unknown; byteLength: number; lineCount: number; estimatedTokens?: number; encoding?: string; }) => string): number; /** * Estimates the total token count of the artifact under `encoding`. * * @remarks * Reads the full byte-faithful content via {@link SpooledArtifact.asString} (which delegates to * {@link @nhtio/adk!SpoolReader.readAll}) and delegates to {@link @nhtio/adk!Tokenizable.estimateTokens}. The estimate * therefore reflects the actual source bytes — including trailing newlines and non-`\n` line * terminators that the line-based {@link SpooledArtifact.cat} view would otherwise discard or * misrepresent. * * @param encoding - The encoding identifier to use for counting. * @returns The estimated number of tokens. */ estimateTokens(encoding: TokenEncoding): Promise; /** * Returns the full artifact body as a single byte-faithful string. * * @remarks * Round-trip faithful to whatever bytes the {@link @nhtio/adk!SpoolReader} was constructed over — * preserves trailing newlines and non-`\n` line terminators that {@link SpooledArtifact.cat} * discards via its line-based view. This is the canonical primitive for "inline the artifact * content directly into a message" use cases. * * `asString()` and the static `forgeTools(ctx)` factory on each subclass are independent * alternatives — a consumer chooses per turn whether to inline the body in a message * (`await tc.results.asString()`) or hand the model query tools * (`SpooledArtifact.forgeTools(ctx)`). Neither calls the other; either works with neither. * * @returns The full content as a single string. */ asString(): Promise; /** * Forges a fresh {@link @nhtio/adk!ToolRegistry} of ephemeral {@link @nhtio/adk!ArtifactTool} instances that let the * LLM query artifacts already present in `ctx.turnToolCalls`. * * @remarks * Standard subclass extension pattern — each class owns only its own `toolMethods` and its * own `forgeTools`. The base `SpooledArtifact.forgeTools(ctx)` narrows the `callId` enum to * any `tc.results instanceof SpooledArtifact` (so subclass instances are included — that's * the whole point of inheritance) and dispatches the seven base methods (`head`, `tail`, * `grep`, `cat`, `byteLength`, `lineCount`, `estimateTokens`) on the resolved artifact. * Subclasses override `forgeTools` to call this static first and then register their own * tools on the returned registry — see {@link @nhtio/adk!SpooledJsonArtifact.forgeTools} and * {@link @nhtio/adk!SpooledMarkdownArtifact.forgeTools} for the canonical shape. There is no * `requiresSubclass` field, no helper indirection, and no `this`-based class narrowing — * just plain `instanceof ThisClass` at each subclass's own filter site. * * For each descriptor in this class's `toolMethods`, the factory: * * 1. Walks `ctx.turnToolCalls` to find `ToolCall`s whose `results instanceof SpooledArtifact`. * `ToolCall`s flagged `fromArtifactTool === true` are excluded — they carry a * {@link @nhtio/adk!Tokenizable}, not a `SpooledArtifact`, and including them would let the model * `artifact_grep` on a previous `artifact_grep` result (an infinite-recursion hazard with * no semantic value). * 2. Returns an empty registry if no compatible callIds are found — no point shipping tools * whose `callId` enum is empty. * 3. Otherwise mints an {@link @nhtio/adk!ArtifactTool} with `ephemeral: true` and `onCollision: 'replace'` * so multiple `Subclass.forgeTools(ctx)` outputs merge silently. The tool's `inputSchema` * includes a required `callId` field with `.valid(...compatibleIds)`, plus the descriptor's * own `argsSchema` fields. * * The handler resolves the artifact via `[...ctx.turnToolCalls].find(t => t.id === callId)`, * dispatches the descriptor's method, and serialises the return value (string → as-is; * string[] → newline-join; number → `String(n)`; otherwise `JSON.stringify(value, null, 2)`; * `descriptor.serialise` overrides the defaults). `grep` is special-cased: the handler * constructs `new RegExp(pattern, flags ?? '')` before invoking the artifact's `grep` method. * * The returned registry must be merged into the consumer's main registry and the main * registry must be bound to `ctx` via {@link @nhtio/adk!ToolRegistry.bindContext}: * * ```ts * const executor: DispatchExecutorFn = async (ctx) => { * const forged = SpooledArtifact.forgeTools(ctx) * const merged = ToolRegistry.merge([main, forged]) * main.bindContext(ctx) * const result = await llm.invoke({ tools: merged.all(), ... }) * ctx.ack() // ← ephemeral cleanup fires here * } * ``` * * @warning You **must** call `registry.bindContext(ctx)` on the registry hosting these tools, * or ephemeral cleanup will not run and the `callId` enum in subsequent executor calls will * be stale (excluding new tool calls produced in the meantime). * * @param ctx - The execution context whose `turnToolCalls` snapshot defines the `callId` enum. * @returns A fresh `ToolRegistry`. Empty when `turnToolCalls` contains no compatible artifacts. * * @see {@link @nhtio/adk!ToolRegistry.bindContext} * @see {@link @nhtio/adk!ToolRegistry.merge} * @see {@link @nhtio/adk!DispatchContext.onAck} */ static forgeTools(ctx: DispatchContext): ToolRegistry; }