---
url: 'https://adk.nht.io/the-loop/artifacts/extending.md'
description: >-
  The subclass extension pattern, ArtifactTool, and the things artifacts
  deliberately do not do.
---

# Extending artifacts

The subclass extension pattern, the [`ArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ArtifactTool) recursion-break, and the explicit non-goals of the artifact layer.

[Artifacts](../artifacts) covers the base [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) surface and the ephemeral lifecycle overview. [forgeTools(ctx) in depth](./forge-tools-in-depth) is the detailed factory walkthrough.

## Subclass extension pattern

Every [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) subclass follows one pattern. Your subclass follows the same pattern or it becomes the weird one future maintainers have to debug. Each subclass owns its own [`SpooledArtifact.toolMethods`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#property-toolmethods) array, listing only that class's descriptors. Each subclass overrides [`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools) to first call the base — `const base = SpooledArtifact.forgeTools(ctx)` — and then mint its own descriptors' tools against [`DispatchContext.turnToolCalls`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-turntoolcalls) narrowed via plain `instanceof ThisClass`, and merges them onto the base registry. The merge resolves the overlap on the base-method tool names through `onCollision: 'replace'`, which is silent because the dispatched method is the same in either case.

There is no `requiresSubclass` field on the descriptor, no helper indirection, no `this`-based class narrowing. The narrowing lives at each subclass's filter site, expressed as the most direct thing it could be: `isInstanceOf(tc.results, ThisClass.name, ThisClass)`. The bundled `SpooledJsonArtifact.forgeTools` and `SpooledMarkdownArtifact.forgeTools` are the worked examples; a new subclass copies the shape and reads as cleanly as the bundled ones do.

::: tip One subclass, one set of descriptors
A descriptor should be reachable on exactly one class — the most-derived class that handles the method correctly. Subclasses inherit the base seven through `forgeTools` calling its parent's factory, not through descriptor concatenation. Listing the same descriptor twice forges duplicate tools with the same name and the same handler. The `onCollision: 'replace'` merge keeps one, silently. That is dead code disguised as design intent, and future you will waste an afternoon looking for the distinction that never existed.
:::

## `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 exists because the artifact-query path needs to violate one rule the base `Tool` enforces: a normal tool's `handler` returns content that gets wrapped in a [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) (via the `artifactConstructor` resolver), but an `ArtifactTool`'s handler returns *the model-visible answer to a query against an existing artifact* — and wrapping that answer in another artifact would let the model `artifact_grep` on the grep result, spool yet another artifact, and so on. The fix at the type level is a different handler return type: `string` or [`Tokenizable`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable). The fix at the schema level is `artifactConstructor: validator.any().forbidden()` — an `ArtifactTool` is the one tool shape that may not declare one. The ADK wraps a bare-string return into a [`Tokenizable`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable) at the result-wrapping site, so [`ToolCall.results`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-results) for an `ArtifactTool` invocation is always a [`Tokenizable`](https://adk.nht.io/api/@nhtio/adk/common/classes/Tokenizable), never a [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact).

The recursion-break flag is [`ToolCall.fromArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-fromartifacttool). The ADK sets it to `true` on every call produced by an `ArtifactTool` handler, and [`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools) excludes those calls from the `compatibleIds` list before building the `callId` enum. The records still exist for persistence and observability — they just are not eligible forge targets, which is the entire reason the flag is on the call rather than buried inside the registry.

## What artifacts do not do

The artifact surface is what it is and nothing else. It does not authorise — any tool whose query needs gating, scope-checking, or human approval handles that in its handler with [Gates](../gates), not at the artifact layer. It does not deduplicate — two calls that produced equal byte sequences are two artifacts, and middleware that wants to canonicalise duplicates does so above the artifact. It does not impose a size policy — an artifact will hold whatever its [`SpoolReader`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/interfaces/SpoolReader) can read, and a battery that wants to refuse oversized outputs declares the policy in middleware. It does not auto-stream content into messages — `asString()` is the explicit "inline the body" operation, and the executor decides whether to use it.

::: danger The artifact layer is data shape, not policy
Everything that looks like "should the model be allowed to see this artifact at all" or "should the model be allowed to call this query right now" is a middleware question — gated tools, RBAC, quota windows, redaction. The artifact is what the answer is given against, not the place the answer is enforced.
:::

For the canonical executor wiring — including the `bindContext` line that makes the lifecycle automatic — see [Forging tools](../../assembly/byo-tools).
