---
url: 'https://adk.nht.io/the-loop/artifacts/shape.md'
description: >-
  What SpooledArtifact is, its POSIX-shaped query surface, and the closed set of
  subclass methods exposed through toolMethods.
---

# The SpooledArtifact shape

[Artifacts](../artifacts) covers the overview, the danger of inlining tool output, and the ephemeral `forgeTools` lifecycle in summary.

## What `SpooledArtifact` is

[`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) is a thin, read-only, line-oriented view over a [`SpoolReader`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/interfaces/SpoolReader). The `SpoolReader` is a structural interface — anything with the right shape qualifies, so the backing store can be an in-memory buffer, a streaming reader against a file on disk, a network-paged blob, whatever the tool that produced the artifact wanted to put behind it. The artifact's methods are all async because the backing store's are, and the artifact does not cache: each call goes through the reader, which is what lets a serious artifact sit on disk instead of in process memory.

The query surface is deliberately POSIX-shaped. [`SpooledArtifact.head`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#head)`(n)` and [`SpooledArtifact.tail`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#tail)`(n)` return the first and last `n` lines. [`SpooledArtifact.grep`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#grep)`(pattern)` runs a `RegExp` per line and returns the matches. [`SpooledArtifact.cat`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#cat)`(start?, end?)` returns lines from a half-open range, defaulting to the whole artifact. [`SpooledArtifact.byteLength`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#bytelength) and [`SpooledArtifact.lineCount`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#linecount) are the cheap structural measurements. [`SpooledArtifact.estimateTokens`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#estimatetokens)`(encoding)` delegates to [`Tokenizable.estimateTokens`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable#property-estimatetokens) on the full body. [`SpooledArtifact.asString`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#asstring) returns the byte-faithful contents as one string — round-trip exact to whatever the `SpoolReader` was constructed over, trailing newlines and all. The line-based methods discard line terminators by design; [`SpooledArtifact.asString`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#asstring) is the escape hatch when you need the bytes back unchanged.

::: tip Line-oriented is a deliberate choice, not the only one
Most tool output that gets large is text and most of that text is naturally line-broken. The base surface is for line-indexed text. When the artifact is not line-indexed text — when the structure is hierarchical, when "line" is the wrong slicing primitive, when the body needs to be parsed before it can be sliced sensibly — do not torture it into pretending to be. Write a subclass with the operations the shape actually deserves. The base class hands you the line-oriented operations; you write the ones your shape needs. The case that broke the other way — bytes the model can never read directly as text (images, audio, video, native-document payloads) — is what surfaced [`Media`](../primitives#media) as its own primitive: a subclass can't paper over the fact that "line" has no meaning for a PNG, and the provider already has native content blocks for rendering it. See [Sibling: `Media`](#sibling-media) below.
:::

`grep` has one subtlety worth knowing about: when the supplied `RegExp` carries stateful flags (`g`, `y`), `pattern.test()` mutates `lastIndex` across calls, which would produce skipped matches and order-dependent results. The artifact resets `pattern.lastIndex` to `0` before each line so the per-line semantics stay stateless. The forged `artifact_grep` tool that the model sees rejects `g` and `y` at schema-validation time for the same reason — the only legitimate way to express "global" matching in a per-line tool is with a different pattern, not a flag.

## Subclasses and the closed set of methods

The base [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) answers the line-oriented questions. Most real artifacts have richer structure that deserves better-typed queries, and that is what subclassing is for. [`SpooledJsonArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledJsonArtifact) adds `artifact_json_keys`, `artifact_json_get`, `artifact_json_filter`, and `artifact_json_pluck` (plus `artifact_json_type`, `artifact_json_length`, `artifact_json_slice`) — JSONPath-Plus expressions across the parsed body, with format auto-detection that tries strict JSON first, then JSON-Lines, then JSON5, before throwing. [`SpooledMarkdownArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledMarkdownArtifact) adds `artifact_md_frontmatter`, `artifact_md_headings`, `artifact_md_code_blocks`, `artifact_md_sections`, `artifact_md_links`, `artifact_md_images`, and `artifact_md_text` — structural queries over a Markdown parse with optional line-range constraints.

The discipline that holds all of this together is the [`SpooledArtifact.toolMethods`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#property-toolmethods) array on each class. It is a frozen list of [`ToolMethodDescriptor`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/ToolMethodDescriptor)s naming the methods the class is willing to surface as ephemeral tools, together with their argument schemas. Each class lists **only its own** descriptors — [`SpooledJsonArtifact.toolMethods`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledJsonArtifact#property-toolmethods) does not concatenate the base seven, and [`SpooledArtifact.toolMethods`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#property-toolmethods) does not know the JSON ones exist. The merging happens in [`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools), not in the descriptor array.

::: info `toolMethods` is metadata, not a method pipeline
A descriptor is the place to attach a tool name, description, args schema, and optional serialiser to one of the artifact's existing methods. `forgeTools` knows how to marshal arguments for a fixed, closed set of method names — the base seven and the JSON/Markdown ones in the bundled subclasses. Adding a descriptor for a brand new method that needs custom argument marshalling, branching, multi-step logic, or cross-artifact joins will not work — the marshalling lives in `forgeTools`, not the descriptor. For anything beyond "call this existing method," override `forgeTools` and mint the `ArtifactTool` directly.
:::

## 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, same separation of "framework owns the contract, implementor owns the storage," paired with a deliberately disjoint reader contract because text and binary have nothing useful to share at the surface. Do not choose by file size. Choose by affordance. A build log wants `grep`, `tail`, and line ranges: `SpooledArtifact`. A PNG has no "line 37": `Media`. Treating them as alternatives is how you build a handle that can query nothing useful. A tool author chooses which silo by which return type the handler produces; the ADK routes accordingly without inferring shape from MIME type or filename.

`Media` wraps a [`MediaReader`](../../api/) (`stream(): ReadableStream<Uint8Array>`, `byteLength(): number | undefined`) the same way `SpooledArtifact` wraps a `SpoolReader` — re-openable byte source, lazy delivery, the implementor decides whether the bytes live in an in-memory buffer, an OPFS file, an S3 object, or a signed URL. See [`Media`](../primitives#media) for the full primitive shape, the two-axis trust model, the `stash` register, and the relationship to `Message.attachments`. The design rationale — *why* the two reader contracts are disjoint, *why* bytes stream rather than buffer, *why* `stash` is a register that accretes through middleware rather than a typed slot — lives in [Primitives → Media](../primitives/media).
