---
url: 'https://adk.nht.io/the-loop/artifacts.md'
description: >-
  SpooledArtifact, the handle pattern, and the ctx-scoped forgeTools lifecycle
  that lets the model query large tool outputs.
---

# Artifacts

## LLM summary — Artifacts

* `SpooledArtifact` is a lazy, line-oriented view over a `SpoolReader`. Base methods: `head(n)`, `tail(n)`, `grep(pattern)`, `cat(start?, end?)`, `byteLength()`, `lineCount()`, `estimateTokens(encoding)`, `asString()`. Read-only; mutation belongs to whatever wrote the reader.
* A tool surfaces its output as an artifact by declaring `artifactConstructor` (a zero-arg resolver returning the `SpooledArtifact` subclass to wrap the handler's serialised return in). `Tool.executor()` returns the raw `string`/`Uint8Array`; the **consumer's executor** does the wrapping — `tool.artifactConstructor?.() ?? SpooledArtifact` — and the wrapped value ends up on `ToolCall.results`.
* `SpooledArtifact.toolMethods` is a frozen array of `ToolMethodDescriptor`s. Each class owns **only** its own descriptors — subclasses do not concatenate. `SpooledJsonArtifact` adds `artifact_json_*`; `SpooledMarkdownArtifact` adds `artifact_md_*`.
* `SpooledArtifact.forgeTools(ctx)` snapshots `ctx.turnToolCalls`, filters to `!fromArtifactTool && tc.results instanceof SpooledArtifact` (subclasses override `forgeTools` to additionally narrow with their own `isInstanceOf` check), builds one `ArtifactTool` per descriptor with `ephemeral: true`, `onCollision: 'replace'`, and a `callId` enum `.valid(...compatibleIds).required()`. Empty registry when nothing matches.
* Subclass extension: each subclass overrides `forgeTools` to call the base first, then merge its own descriptors' tools. Narrowing is plain `isInstanceOf(tc.results, ThisClass.name, ThisClass)` at each subclass's own filter site — no `requiresSubclass` field, no helper indirection.
* `ArtifactTool` extends `Tool`. Handler returns `string | Tokenizable` (bare strings get wrapped). Schema forbids `artifactConstructor` — `ArtifactTool` writes a `Tokenizable` into `ToolCall.results`, not another artifact.
* Recursion break: `ToolCall.fromArtifactTool = true` is set on calls produced by `ArtifactTool` handlers. `forgeTools` filters those out so the model cannot `artifact_grep` a previous `artifact_grep` result.
* Snapshot staleness is the design's central trade. `callId` enum is frozen at `forgeTools(ctx)` call time; calls produced later in the same dispatch are not in it. Re-forge per dispatch — bind lifecycle to `ctx.onAck` via `registry.bindContext(ctx)` so cleanup is automatic.
* Canonical executor wiring (mirrors `batteries/llm/openai_chat_completions/adapter.ts`): forge per artifact ctor, then `const merged = ToolRegistry.merge([ctx.tools, ...forged], { onCollision: 'replace' }); merged.bindContext(ctx)`, and pass `merged` (not `ctx.tools`) to provider rendering / `merged.get(name)` at the executor's tool-invocation site. `ctx.tools` is `readonly` on both `TurnContext` and `DispatchContext`, so `merge` returning a fresh registry is the design: the executor closes over `merged` for the duration of the iteration. Forge tools default to `onCollision: 'replace'`; ordinary `ToolRegistry.register`/`merge` uses `onCollision: 'throw'` by default and surfaces `E_TOOL_ALREADY_REGISTERED` on name clash. See [Forging tools](../assembly/byo-tools).

An artifact is the ADK's answer to a tool that produces more output than belongs in a context window. A build log, a generated file, a fetched document, a query result with thousands of rows — these are the outputs every honest agent eventually has to deal with, and inlining them into the message stream is the wrong answer to almost all of them. [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) is the alternative: a typed handle the model can hold, with a small library of query operations the model can use to look at the parts of the result it actually needs, leaving the rest of the bytes where they are.

::: danger Don't blow up the context window
Inlining every tool's full output into the message stream is the most common way a working agent silently turns into a broken one. Latency climbs, costs climb, the model's attention thins out across material it did not need, and the failure mode that surfaces first is "the agent got worse at the task" rather than "we exceeded a budget" — which makes it expensive to diagnose and easy to misattribute. This is not a theoretical concern; it is one of the most frequent failure modes across every agentic framework in production, including the well-established ones. The ADK's response is unconditional in shape but split across two layers: a [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) handler that returns `string` or `Uint8Array` carries no artifact yet — the consumer's executor is responsible for taking that return value and wrapping it via `tool.artifactConstructor?.() ?? SpooledArtifact` before it can become a [`ToolCall`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall) result. That executor wrap is the spool gate, and once it happens the rest of the loop sees a `SpooledArtifact` instead of raw bytes. There are two carve-outs — [`ArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ArtifactTool) results (which would otherwise recurse: an `artifact_grep` on a grep result, spooled, then grepped again) and [`Media`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media) / `Media[]` returns (the explicit-modality path for bytes the provider renders inline as a native content block, where "spool and forge handle tools" is the wrong shape). Both are documented under [What a Tool is](./tools/what-a-tool-is). From there you make an explicit choice: query the handle, summarise it, persist it, or deliberately inline it with [`SpooledArtifact.asString`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#asstring). What you do not get to do is accidentally pour a multi-megabyte log into the next prompt because nobody touched a flag. The executor wrap turns raw bytes into a handle-shaped artifact; abusing that handle is on you.
:::

## What `SpooledArtifact` is

A [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) is the text handle. It gives the model [`SpooledArtifact.head`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#head)/[`SpooledArtifact.tail`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#tail)/[`SpooledArtifact.grep`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#grep)/[`SpooledArtifact.cat`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#cat) instead of a wall of bytes. If the model needs the whole body, it asks for the whole body via [`SpooledArtifact.asString`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#asstring). Nothing gets dumped into the prompt by accident. The full POSIX-shaped surface is [`SpooledArtifact.head`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#head), [`SpooledArtifact.tail`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#tail), [`SpooledArtifact.grep`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#grep), [`SpooledArtifact.cat`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#cat), [`SpooledArtifact.byteLength`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#bytelength), [`SpooledArtifact.lineCount`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#linecount), [`SpooledArtifact.estimateTokens`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#estimatetokens), [`SpooledArtifact.asString`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#asstring); the reader is structural so the backing store can be in-memory, on disk, or paged across the network.

→ Continue reading: [What SpooledArtifact is](./artifacts/shape#what-spooledartifact-is)

## Subclasses and the closed set of methods

[`SpooledJsonArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledJsonArtifact) and [`SpooledMarkdownArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledMarkdownArtifact) add typed query surfaces over their parsed bodies. Each class owns its own `toolMethods` array; `forgeTools` is what does the merging, not the descriptor array.

→ Continue reading: [Subclasses and the closed set of methods](./artifacts/shape#subclasses-and-the-closed-set-of-methods)

## `forgeTools(ctx)` and the ephemeral lifecycle

[`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools) walks [`DispatchContext.turnToolCalls`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-turntoolcalls), mints one [`ArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ArtifactTool) per descriptor against the matching artifacts, and returns an ephemeral [`ToolRegistry`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry). [`ToolRegistry.bindContext`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#bindcontext) wires the cleanup so the forged tools are pruned on `ack`.

→ Continue reading: [forgeTools(ctx) in depth](./artifacts/forge-tools-in-depth)

## Subclass extension pattern

Each [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) subclass owns its own `toolMethods` array and overrides `forgeTools` to call the base first, then merge its own descriptors. Narrowing is plain `isInstanceOf(tc.results, ThisClass.name, ThisClass)` at each subclass's own filter site.

→ Continue reading: [Subclass extension pattern](./artifacts/extending#subclass-extension-pattern)

## `ArtifactTool`

[`ArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ArtifactTool) is the [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) subclass that backs every forged artifact-query tool. It returns `string` or [`Tokenizable`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable) rather than another artifact, and its schema forbids `artifactConstructor` to keep the recursion break clean.

→ Continue reading: [ArtifactTool](./artifacts/extending#artifacttool)

## What artifacts do not do

The artifact surface is data shape, not policy. It does not authorise, deduplicate, impose size policy, or auto-stream content into messages.

→ Continue reading: [What artifacts do not do](./artifacts/extending#what-artifacts-do-not-do)

## Sibling: `Media`

[`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) is the *line-indexed text* handle citizen. [`Media`](./primitives#media) is the *binary streaming* sibling — same handle-pattern posture, deliberately disjoint reader contract, because text and binary have nothing useful to share at the surface.

→ Continue reading: [Sibling: Media](./artifacts/shape#sibling-media)
