---
url: 'https://adk.nht.io/batteries/skills/artifacts.md'
description: >-
  What skill tools can return, how results are spooled into artifacts, and how
  the host binds reader kinds.
---

# Tool results

A skill tool may return `string`, `Uint8Array`, `Media`, or `Media[]`; anything else — including a prebuilt `SpooledArtifact` — fails as `E_SKILL_TOOL_BAD_RESPONSE`. Results are spooled behind an artifact whose class decides the reader tools. `SkillManagerConfig.artifactKinds` names constructors; `artifactBindings` maps skill id → tool name → kind. Resolution order is binding, descriptor `artifactKind`, then `'SpooledArtifact'`.

::: tip This is a featured battery
Raw output is the safe default. A descriptor may advise a kind; the consumer's explicit binding wins.
:::

A skill tool cannot hand back arbitrary runtime values. The accepted response set is small:

* `string`
* `Uint8Array`
* `Media`, or an array of `Media`

Anything else — `undefined`, a bare object, a function — fails response validation as `E_SKILL_TOOL_BAD_RESPONSE` before any artifact selection happens. For where that check sits in the wrapper, see [Executable capability](./capability).

A **prebuilt `SpooledArtifact` is also refused**, deliberately. Handing back an already-instantiated artifact would let third-party code bypass the deployer's configured binding. Choosing how output is read is the host's decision, not the plugin's.

### Anonymous bytes

Here is the honest limitation: **bytes arrive anonymous.** A `Uint8Array` carries no MIME type, no encoding, and no filename. To the host, a PDF and a WAV are indistinguishable — both are buffers. A tool that wants to return typed binary must construct and return a `Media` today.

That comes with a tax: constructing `Media` means importing `@nhtio/adk/common`, which is a real constraint on a tool that wants zero framework coupling. A framework-agnostic path is planned and **not yet built**. Until it exists, anonymous bytes stay raw bytes and typed binary requires the import.

## What happens to the result

The result is spooled behind an artifact, and the artifact class decides which reader tools the model gets over that payload on subsequent dispatches.

The default is raw `SpooledArtifact` — line-oriented access that makes no claim about structure. Line ranges, grep, byte slicing, token estimation; no assumption that the payload is markdown, tabular, or an image.

## Configuring it: the two registries

The host controls artifact assignment through two registries on `SkillManagerConfig`.

**`artifactKinds`** names constructors. Resolvers are resolved eagerly at manager construction, and may be a `SpooledArtifactConstructor` or a lazy function / default export. The built-in key `'SpooledArtifact'` is always present; optional artifact batteries are not imported for you.

**`artifactBindings`** is consumer-owned policy, mapping skill id → tool name → kind name. It keeps authority in host configuration rather than letting a skill smuggle a renderer into the process.

```ts
import { createSkillManager } from '@nhtio/adk/batteries/skills'

const manager = await createSkillManager({
  sources: [source],
  gate,
  artifactKinds: {
    report: () => ReportArtifact,
  },
  artifactBindings: {
    reports: { render: 'report' },
  },
})
```

Resolution order:

1. the consumer's explicit **`artifactBindings`**
2. the descriptor's advisory **`artifactKind`**
3. the raw fallback, **`SpooledArtifact`**

A fuller configuration makes the precedence concrete:

```ts
const manager = await createSkillManager({
  sources: [source],
  gate,
  artifactKinds: {
    report: () => ReportArtifact,
    image: ImageArtifact,
  },
  artifactBindings: {
    reports: { render: 'report' },
    charts: { preview: 'image' },
  },
})
```

For `reports` and `render`, the binding `report` wins over the descriptor's advisory `artifactKind`. If the named kind is not registered, loading fails with `E_SKILL_ARTIFACT_UNAVAILABLE`.

## Why the raw default matters

Forgetting to register a named kind or misspelling a binding produces `E_SKILL_ARTIFACT_UNAVAILABLE` at load time; it does not silently fall back to a renderer. A descriptor's advisory `artifactKind` is not authority when the host has an explicit binding. And a tool returning an unsupported value fails as `E_SKILL_TOOL_BAD_RESPONSE` before artifact selection happens at all.

Raw output is deliberately boring: available without optional batteries, executes no plugin-provided constructor, and gives adapters a stable baseline. Opt into a richer kind only when the host owns and tests that constructor.

## Protected reader namespace

Skill tools cannot claim names derived from the raw and markdown artifact method registries, including the readers core will forge for skill bodies. The manager preflights tool names against that reserved set before registration, **even when no artifact exists yet** — otherwise a skill could register `artifact_grep` in a quiet dispatch and be silently replaced by core the moment an artifact appears. A collision halts registration rather than letting a plugin shadow a host capability.

## Ownership classes

Artifact reads fall into three classes across the skill lifecycle:

* **Instruction set** — reads of a loaded skill's body artifact, reclaimed with the skill on unload.
* **Work** — findings, script-output reads, and anything the agent produced. Durable.
* **Backing bytes** — in the consumer's spool store, consumer-managed; the reader contract has no delete.

Deactivating a skill reclaims its instructions and its exported tools without discarding the work the agent produced while using it. For evicting large media results once they stop being relevant, that is what the [context batteries](../context/) are for — `subtractToFit` sheds media first when a dispatch is over budget.

## Adapter reality

`bedrock_converse` and `gemini_generate_content` coerce tool results through JSON, do not honour `artifactConstructor`, and forge no readers; for those providers imported output is sent inline. Other adapters spool results into true artifacts. The battery cannot document a renderer into existence when the provider flattens results before the host can intervene.
